.NET SDK

Full observability for .NET services — HTTP, databases, gRPC, and logging captured automatically via OpenTelemetry

Installation

dotnet add package Obtrace.Sdk

Targets .NET 6+ and .NET Standard 2.1.

Minimum Setup

Program.cs
using Obtrace.Sdk;
 
var client = new ObtraceClient(new ObtraceConfig
{
    ApiKey = Environment.GetEnvironmentVariable("OBTRACE_API_KEY")!,
    ServiceName = "order-api"
});

Two fields are required: ApiKey and ServiceName. Everything else has defaults.

Built on OpenTelemetry

The .NET SDK uses OpenTelemetry.Instrumentation.Http and OpenTelemetry.Instrumentation.AspNetCore under the hood. The AddObtrace() extension method configures everything via the standard .NET dependency injection system, including OTel exporters, resource attributes, and all available instrumentations.

What's Captured Automatically

After creating an ObtraceClient, the SDK configures OpenTelemetry and instruments your application. You don't need to write any extra code for this:

WhatHowNeeds code?
Console.WriteLine()Console.Out writes are intercepted and sent as info logsNo
Console.Error.WriteLine()Console.Error writes are intercepted and sent as error logsNo
Outbound HttpClient callsHttpClient instrumented via OpenTelemetry.Instrumentation.Http with spans and traceparent injectionNo
Inbound ASP.NET Core requestsInstrumented via OpenTelemetry.Instrumentation.AspNetCore when using AddObtrace()No
SQL Server / EF CoreSqlClient and Entity Framework instrumented via OTel when packages are installedNo
gRPCGrpc.Net.Client instrumented via OpenTelemetry.Instrumentation.GrpcNetClientNo
RedisStackExchange.Redis instrumented via OTel when the instrumentation package is installedNo
Shutdown flushIAsyncDisposable flushes remaining data on disposeNo

Console auto-capture is enabled by default (AutoCaptureConsole = true). Set AutoCaptureConsole = false to disable it.

Opting Out of Auto HTTP Instrumentation

var client = new ObtraceClient(new ObtraceConfig
{
    ApiKey = Environment.GetEnvironmentVariable("OBTRACE_API_KEY")!,
    ServiceName = "order-api",
    AutoInstrumentHttp = false
});

ASP.NET Core Integration with AddObtrace()

For ASP.NET Core apps, use the AddObtrace() extension method to register the client in DI, add the HTTP request middleware, and wire up shutdown:

Program.cs
using Obtrace.Sdk;
 
var builder = WebApplication.CreateBuilder(args);
 
builder.Services.AddObtrace(new ObtraceConfig
{
    ApiKey = Environment.GetEnvironmentVariable("OBTRACE_API_KEY")!,
    ServiceName = "order-api",
    Env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "production",
    ServiceVersion = Environment.GetEnvironmentVariable("OBTRACE_SERVICE_VERSION") ?? "1.0.0"
});
 
var app = builder.Build();
 
app.UseObtrace();
 
app.MapGet("/api/orders/{id}", (string id, ObtraceClient client) =>
{
    client.Log("info", "order.fetched", new Dictionary<string, object?>
    {
        ["order.id"] = id
    });
    return Results.Ok(new { id, status = "shipped" });
});
 
app.Run();

AddObtrace() configures OpenTelemetry via the .NET DI system: it registers the OTLP exporter, adds OpenTelemetry.Instrumentation.AspNetCore for inbound request spans, adds OpenTelemetry.Instrumentation.Http for outbound HttpClient spans, registers ObtraceClient as a singleton, and hooks into ApplicationStopping to flush on shutdown. If you have OpenTelemetry.Instrumentation.SqlClient, OpenTelemetry.Instrumentation.EntityFrameworkCore, OpenTelemetry.Instrumentation.GrpcNetClient, or OpenTelemetry.Instrumentation.StackExchangeRedis packages installed, they are also configured automatically.

Optional: Custom Telemetry

The automatic console and HTTP capture covers basic observability. For business-specific events, use the SDK methods directly.

Logging

Use client.Log() for structured events that represent something that happened. Logs are best for state transitions, errors, and audit trails.

client.Log("info", "order.created", new Dictionary<string, object?>
{
    ["order.id"] = orderId,
    ["order.total"] = 149.99,
    ["customer.tier"] = "premium"
});
 
client.Log("error", "payment.declined", new Dictionary<string, object?>
{
    ["order.id"] = orderId,
    ["payment.provider"] = "stripe",
    ["decline.code"] = "insufficient_funds"
});
 
client.Log("warn", "inventory.low", new Dictionary<string, object?>
{
    ["sku"] = "WIDGET-42",
    ["remaining"] = 3,
    ["reorder.threshold"] = 10
});

Levels: trace, debug, info, warn, error, fatal.

Metrics

Use client.Metric() for numerical measurements you want to track over time. Metrics are best for gauges, counters, and percentiles.

client.Metric("http.server.request.duration", 42.5, "ms", new Dictionary<string, object?>
{
    ["http.method"] = "POST",
    ["http.route"] = "/api/orders"
});
 
client.Metric("queue.depth", 847, "1", new Dictionary<string, object?>
{
    ["queue.name"] = "order-processing"
});
 
client.Metric("cache.hit_ratio", 0.92, "1", new Dictionary<string, object?>
{
    ["cache.name"] = "product-catalog"
});
 
client.Metric("db.pool.active_connections", 14, "1");

The unit parameter follows OTLP conventions: "ms" for milliseconds, "By" for bytes, "1" for dimensionless values.

Tracing / Spans

Use client.Span() for operations with duration that you want to visualize in a trace waterfall. Spans are best for database queries, HTTP calls, queue processing, and any work that takes measurable time.

client.Span("db.query SELECT orders", attrs: new Dictionary<string, object?>
{
    ["db.system"] = "postgresql",
    ["db.statement"] = "SELECT * FROM orders WHERE customer_id = $1",
    ["db.operation"] = "SELECT",
    ["db.sql.table"] = "orders"
});
 
client.Span("http.client POST /payments", attrs: new Dictionary<string, object?>
{
    ["http.method"] = "POST",
    ["http.url"] = "https://api.stripe.com/v1/charges",
    ["http.status_code"] = 201,
    ["payment.amount"] = 149.99
});
 
client.Span("queue.publish order.created", attrs: new Dictionary<string, object?>
{
    ["messaging.system"] = "rabbitmq",
    ["messaging.destination"] = "order.created",
    ["messaging.message.payload_size_bytes"] = 1024
});

Framework Integration

ASP.NET Core (Manual)

If you prefer manual setup over AddObtrace():

Program.cs
using Obtrace.Sdk;
 
var builder = WebApplication.CreateBuilder(args);
 
var client = new ObtraceClient(new ObtraceConfig
{
    ApiKey = Environment.GetEnvironmentVariable("OBTRACE_API_KEY")!,
    ServiceName = "order-api",
    Env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "production",
    ServiceVersion = Environment.GetEnvironmentVariable("OBTRACE_SERVICE_VERSION") ?? "1.0.0"
});
builder.Services.AddSingleton(client);
 
var app = builder.Build();
 
app.Use(async (context, next) =>
{
    var sw = System.Diagnostics.Stopwatch.StartNew();
    await next();
    sw.Stop();
 
    var route = context.GetEndpoint()?.DisplayName ?? context.Request.Path.Value ?? "unknown";
    var method = context.Request.Method;
 
    client.Span($"http.server {method} {route}", attrs: new Dictionary<string, object?>
    {
        ["http.method"] = method,
        ["http.route"] = route,
        ["http.status_code"] = context.Response.StatusCode
    });
 
    client.Metric("http.server.request.duration", sw.Elapsed.TotalMilliseconds, "ms", new Dictionary<string, object?>
    {
        ["http.method"] = method,
        ["http.route"] = route,
        ["http.status_code"] = context.Response.StatusCode
    });
});
 
app.MapGet("/api/orders/{id}", (string id) =>
{
    client.Log("info", "order.fetched", new Dictionary<string, object?>
    {
        ["order.id"] = id
    });
    return Results.Ok(new { id, status = "shipped" });
});
 
app.Lifetime.ApplicationStopping.Register(() =>
{
    client.FlushAsync().GetAwaiter().GetResult();
});
 
app.Run();

Worker Service / Background Jobs

Worker.cs
public class OrderProcessor : BackgroundService
{
    private readonly ObtraceClient _client;
 
    public OrderProcessor(ObtraceClient client) => _client = client;
 
    protected override async Task ExecuteAsync(CancellationToken token)
    {
        while (!token.IsCancellationRequested)
        {
            var batch = await DequeueBatch();
 
            _client.Span("queue.process order-batch", attrs: new Dictionary<string, object?>
            {
                ["batch.size"] = batch.Count,
                ["messaging.system"] = "rabbitmq"
            });
 
            foreach (var order in batch)
            {
                try
                {
                    await ProcessOrder(order);
                    _client.Metric("orders.processed", 1, "1");
                }
                catch (Exception ex)
                {
                    _client.Log("error", "order.processing.failed", new Dictionary<string, object?>
                    {
                        ["order.id"] = order.Id,
                        ["error.type"] = ex.GetType().Name,
                        ["error.message"] = ex.Message
                    });
                }
            }
 
            await _client.FlushAsync();
        }
    }
}

Configuration Reference

PropertyTypeRequiredDefaultDescription
ApiKeystringYesObtrace API key
ServiceNamestringYesStable name for this service
TenantIdstring?Nofrom keyTenant identifier
ProjectIdstring?Nofrom keyProject identifier
AppIdstring?NoApplication identifier within the project
EnvstringNo"production"Deployment environment
ServiceVersionstring?NoVersion string for this deployment
AutoCaptureConsoleboolNotrueIntercept Console.Out and Console.Error as logs
AutoInstrumentHttpboolNotrueInstrument outbound HttpClient calls with spans and trace propagation
ValidateSemanticMetricsboolNofalseWarn on non-standard metric names
DebugboolNofalseEnable verbose logging to stdout
MaxQueueSizeintNo2048Maximum buffered items before dropping
FlushIntervalMsintNo5000Auto-flush interval in milliseconds

Validation Checklist

After deploying, verify:

  • ServiceName, Env, and ServiceVersion are stable across restarts and match what you see in the Obtrace UI
  • At least one real request path emits both a log and a span
  • No 401 or 403 errors appear in stdout when Debug = true
  • FlushAsync or ShutdownAsync is called before process exit (use await using, AddObtrace(), or register with ApplicationStopping)
  • Outbound HttpClient calls show auto-generated spans
  • Metrics use OTLP-standard units (ms, By, 1) not custom strings