PowerTraces

Traces and logs in your own database.

A free .NET library that writes OpenTelemetry traces and logs straight into SQL Server, PostgreSQL, MySQL, MongoDB or SQLite, and gives you a dashboard inside your app. No collector, no migrations, no account.

Free, including commercial use · .NET 9 and later · NuGet 3.0.0: View on NuGet

Pick your database

The tables are created on first start, in their own schema, so it can share the database your app already uses.

dotnet add package PowerTraces.MSSQL
dotnet add package PowerTraces.Dashboard
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.AspNetCore

using OpenTelemetry.Logs;
using OpenTelemetry.Trace;
using PowerTraces;
using PowerTraces.Dashboard;
using PowerTraces.MSSQL;

builder.Services.AddOpenTelemetry()
    .WithTracing(t => t
        .AddAspNetCoreInstrumentation()
        .AddPowerTracesMsSqlExporter(connectionString, o => o.ExcludePathPrefixes.Add("/otel")))
    .WithLogging(l => l.AddPowerTracesMsSqlExporter(connectionString));

builder.Services.AddPowerTracesMsSqlDashboard(connectionString);

var app = builder.Build();
app.MapPowerTracesDashboard("/otel", PowerTracesDatabaseProvider.SqlServer);

What you get inside your app

Screenshots from a sample orders API using the SQLite provider, at /otel.

Requests, error rate, latency and logs at a glance, with the busiest operations and how often they fail.
Overview: Requests, error rate, latency and logs at a glance, with the busiest operations and how often they fail.
A failed checkout: every span on a time axis, the critical path marked, and the selected span’s attributes beside it.
Trace detail: A failed checkout: every span on a time axis, the critical path marked, and the selected span’s attributes beside it.
Searching a field inside the request body. Each result shows which field matched.
Search: Searching a field inside the request body. Each result shows which field matched.
Warnings and errors with their values, linked to the trace they belong to.
Logs: Warnings and errors with their values, linked to the trace they belong to.

An OpenTelemetry exporter that writes to a database

Your app keeps using standard OpenTelemetry. Instead of sending spans and logs to a collector, PowerTraces writes them in batches, in the background, to the database you point it at. The dashboard reads the same tables.

  1. Your .NET app: ASP.NET Core, workers, anything that uses OpenTelemetry.
  2. PowerTraces exporter: Batches off the request path, and applies ignore rules before writing.
  3. Your database: Spans, logs, trace summaries and a search index, in their own schema.
  4. Dashboard at /otel: Embedded in the package. Nothing else to deploy.

Everything you need to debug one app

Get more out of every trace

Four things teams add in their first week. Each one is a few lines of standard OpenTelemetry in your own code.

Record request and response bodies

Add a small middleware that copies JSON bodies onto the request span as http.request.body and http.response.body. PowerTraces splits any JSON attribute into fields, so you can search a value inside a body.

app.UseBodyCapture();

public static class BodyCapture
{
    private const int MaxBody = 32 * 1024;

    public static IApplicationBuilder UseBodyCapture(this IApplicationBuilder app) => app.Use(async (context, next) =>
    {
        var activity = Activity.Current;
        if (activity is null) { await next(); return; }

        if (IsJson(context.Request.ContentType))
        {
            context.Request.EnableBuffering();
            using var reader = new StreamReader(context.Request.Body, leaveOpen: true);
            activity.SetTag("http.request.body", Limit(await reader.ReadToEndAsync()));
            context.Request.Body.Position = 0;
        }

        var original = context.Response.Body;
        using var buffer = new MemoryStream();
        context.Response.Body = buffer;
        try
        {
            await next();
        }
        finally
        {
            if (IsJson(context.Response.ContentType))
                activity.SetTag("http.response.body", Limit(System.Text.Encoding.UTF8.GetString(buffer.ToArray())));
            buffer.Position = 0;
            await buffer.CopyToAsync(original);
            context.Response.Body = original;
        }
    });

    private static bool IsJson(string? type) => type?.Contains("json", StringComparison.OrdinalIgnoreCase) == true;
    private static string Limit(string text) => text.Length > MaxBody ? text[..MaxBody] : text;
}

Trace your own services and repositories

Create an ActivitySource for each area of your code and start an activity in the methods you want to see. They appear as spans inside the request that called them.

builder.Services.AddOpenTelemetry()
    .WithTracing(t => t
        .AddSource("Shop.*")
        .AddAspNetCoreInstrumentation()
        .AddPowerTracesSqliteExporter(telemetry));

public sealed class OrderService(IOrderRepository orders)
{
    private static readonly ActivitySource Source = new("Shop.Orders");

    public async Task<Order> PlaceAsync(Cart cart, CancellationToken ct)
    {
        using var activity = Source.StartActivity("OrderService.Place");
        activity?.SetTag("cart.items", cart.Items.Count);
        return await orders.InsertAsync(cart, ct);
    }
}

public sealed class OrderRepository(IOrdersDb db) : IOrderRepository
{
    private static readonly ActivitySource Source = new("Shop.Data");

    public async Task<Order> InsertAsync(Cart cart, CancellationToken ct)
    {
        using var activity = Source.StartActivity("OrderRepository.Insert", ActivityKind.Client);
        activity?.SetTag("db.system.name", "postgresql");
        var order = await db.SaveAsync(cart, ct);
        activity?.SetTag("order.id", order.Id);
        return order;
    }
}

Record every SQL command, from ADO.NET or EF Core

Add the instrumentation for your database driver. Each command becomes a span with its SQL text in db.query.text, under the code that ran it. EF Core runs through the same drivers, so it is covered too.

# SQL Server (Microsoft.Data.SqlClient, also under EF Core)
dotnet add package OpenTelemetry.Instrumentation.SqlClient
# PostgreSQL (Npgsql, also under EF Core)
dotnet add package Npgsql.OpenTelemetry
# MySQL and MariaDB: MySqlConnector traces itself, nothing to install
# Any other EF Core provider
dotnet add package OpenTelemetry.Instrumentation.EntityFrameworkCore --prerelease
builder.Services.AddOpenTelemetry()
    .WithTracing(t => t
        .AddAspNetCoreInstrumentation()
        .AddSqlClientInstrumentation(o => o.RecordException = true)
        .AddNpgsql()
        .AddSource("MySqlConnector")
        .AddPowerTracesSqliteExporter(telemetry));

Mark a trace as failed

In a catch, call RecordExceptionWithSource: it records the exception and marks the span as failed. For failures that aren't exceptions, set the span's status to Error. If any span in a trace fails, the whole trace counts as failed in the trace list, the overview and the error filters.

using System.Diagnostics;
using PowerTraces;

public sealed class PaymentService(IPaymentGateway gateway)
{
    private static readonly ActivitySource Source = new("Shop.Payments");

    public async Task ChargeAsync(decimal amount, CancellationToken ct)
    {
        using var activity = Source.StartActivity("PaymentService.Charge");
        try
        {
            await gateway.ChargeAsync(amount, ct);
        }
        catch (Exception ex)
        {
            activity.RecordExceptionWithSource(ex);
            throw;
        }
    }

    public void Declined(string reason)
    {
        Activity.Current?.SetStatus(ActivityStatusCode.Error, reason);
        Activity.Current?.SetTag("payment.decline_reason", reason);
    }
}

One search box, down to a field inside a request body

Search any word, value or field across span names, attributes, events and JSON bodies. Each result shows which field matched.

Fast on millions of spans

The trace list reads a summary kept up to date on write, and search uses an index written next to the spans, so cost follows the matches, not the table size.

Built for each engine

Using another database? Implement IStorageWriter and IDashboardStore. The built-in providers use the same public interfaces.

Start free. Move to PowerTraces when one app becomes many.

.NET libraryPowerTraces
PriceFree, including commercial useFree trial, then paid
Where data livesYour databaseOur cloud, or your servers on-prem
Traces and logsYesYes
MetricsNoYes
Languages.NET.NET, Node.js, browser and any OpenTelemetry SDK
Many services in one viewApps sharing a databaseYes, across teams and workspaces
SLOs, alerts and revenue impactNoYes

Questions

Is it really free?

Yes. Use it in any app, including commercial ones, and ship it inside your product. No account or key is needed. The license doesn't allow changing the library's code; configure it with its options or extend it through its public interfaces instead.

Will it slow my app down?

Spans and logs are exported in batches by OpenTelemetry's batch processor, off the request path. The library's own database calls are never traced.

Can it use the database my app already has?

Yes. Its tables live in their own schema, so they never mix with yours. You can also give it a separate database.

Can several apps write to the same database?

Yes. Table creation is lock-guarded, and ignore rules and retention are stored in the database, so every app follows them.

How do I protect the dashboard?

Pass a user name and password to MapPowerTracesDashboard, or mount it behind your own authentication.

How do I move to PowerTraces later?

Both use OpenTelemetry, so your instrumentation stays as it is. Replace the exporter lines with the PowerTraces.cs file from Get started in PowerTraces, and call builder.AddPowerTraces().

Outgrowing one database?

PowerTraces brings every service, language and signal into one place, with SLOs, alerts and the business impact of each error.

Start free