Opentelemetry

codewithmukesh/dotnet-claude-kit/skills/opentelemetry

by codewithmukesh23300897f4d1No license754 starsListed Oct 8, 2026Updated Oct 8, 2026Repository updated 2 months ago

OpenTelemetry observability for .NET 10 applications. Covers traces, metrics, and logs using the OpenTelemetry SDK with OTLP export. Includes custom ActivitySource, IMeterFactory metrics, resource configuration, and Aspire Dashboard integration. Load this skill when setting up distributed tracing, custom metrics, OTLP export, or when the user mentions "OpenTelemetry", "OTLP", "traces", "spans", "Activity", "ActivitySource", "metrics", "IMeterFactory", "Meter", "Counter", "Histogram", "Gauge", "telemetry", "observability", "distributed tracing", "OTEL", or "Aspire Dashboard".

Instructions only

OpenTelemetry

Core Principles

  1. Three pillars, one setup — Configure traces, metrics, and logs through a single AddOpenTelemetry() call. Use UseOtlpExporter() for cross-cutting export to any OTLP-compatible backend.
  2. Use IMeterFactory for metrics — Never create Meter instances with new. The factory manages lifetime through DI and prevents leaks.
  3. Null-safe activities — StartActivity() returns null when no listener is attached. Always use ?. when setting tags or events.
  4. Environment variables over code — Use OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_SERVICE_NAME so deployments control telemetry routing without code changes.
  5. Low-cardinality metric tags — Keep metric tag combinations under ~1000 per instrument. Use span attributes or logs for high-cardinality data like user IDs or request IDs.

Patterns

Full Setup with All Three Signals

csharp
// Program.csvar builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenTelemetry()    .ConfigureResource(resource => resource        .AddService(            serviceName: builder.Environment.ApplicationName,            serviceVersion: "1.0.0"))    .WithTracing(tracing => tracing        .AddAspNetCoreInstrumentation()        .AddHttpClientInstrumentation()        .AddEntityFrameworkCoreInstrumentation()        .AddSource("MyApp.Orders"))    .WithMetrics(metrics => metrics        .AddAspNetCoreInstrumentation()        .AddHttpClientInstrumentation()        .AddRuntimeInstrumentation()        .AddMeter("MyApp.Orders"))    .WithLogging()             // no per-signal exporter here —    .UseOtlpExporter();        // UseOtlpExporter covers all three signals
// UseOtlpExporter replaces per-signal AddOtlpExporter calls. Never combine// the two — mixing them throws NotSupportedException (see Anti-patterns).

The OTLP endpoint defaults to http://localhost:4317 (gRPC). Override via:

OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4317OTEL_SERVICE_NAME=MyApp.Api

Custom Metrics with IMeterFactory

Register a metrics class as a singleton. IMeterFactory handles Meter disposal through DI.

csharp
public sealed class OrderMetrics{    private readonly Counter<int> _ordersCreated;    private readonly Histogram<double> _orderDuration;    private readonly UpDownCounter<int> _activeOrders;    private readonly Gauge<double> _queueDepth;
    public OrderMetrics(IMeterFactory meterFactory)    {        var meter = meterFactory.Create("MyApp.Orders");
        _ordersCreated = meter.CreateCounter<int>(            "myapp.orders.created", "{orders}", "Number of orders created");
        _orderDuration = meter.CreateHistogram<double>(            "myapp.orders.duration", "s", "Order processing duration",            advice: new InstrumentAdvice<double>            {                HistogramBucketBoundaries = [0.01, 0.05, 0.1, 0.5, 1, 5, 10]            });
        _activeOrders = meter.CreateUpDownCounter<int>(            "myapp.orders.active", "{orders}", "Currently active orders");
        _queueDepth = meter.CreateGauge<double>(            "myapp.orders.queue_depth", "{items}", "Current queue depth");    }
    public void OrderCreated() => _ordersCreated.Add(1);    public void RecordDuration(double seconds) => _orderDuration.Record(seconds);    public void OrderStarted() => _activeOrders.Add(1);    public void OrderCompleted() => _activeOrders.Add(-1);    public void SetQueueDepth(double depth) => _queueDepth.Record(depth);}
// Registrationbuilder.Services.AddSingleton<OrderMetrics>();

Multi-Dimensional Metric Tags

Three or fewer tags are allocation-free. For more, use TagList.

csharp
// Allocation-free (3 or fewer tags)_ordersCreated.Add(1,    new KeyValuePair<string, object?>("order.type", "standard"),    new KeyValuePair<string, object?>("payment.method", "credit_card"));
// 4+ tags — use TagList to avoid allocationsvar tags = new TagList{    { "order.type", "standard" },    { "payment.method", "credit_card" },    { "region", "us-east" },    { "priority", "high" }};_ordersCreated.Add(1, tags);

Custom ActivitySource for Distributed Tracing

csharp
public sealed class OrderService(ILogger<OrderService> logger){    private static readonly ActivitySource Source = new("MyApp.Orders");
    public async Task<Order> ProcessOrderAsync(CreateOrderRequest request, CancellationToken ct)    {        using var activity = Source.StartActivity("ProcessOrder", ActivityKind.Internal);        activity?.SetTag("order.customer_id", request.CustomerId);
        try        {            await ValidateOrder(request, ct);            activity?.AddEvent(new ActivityEvent("OrderValidated"));
            var order = await SaveOrder(request, ct);            activity?.SetTag("order.id", order.Id.ToString());            activity?.SetStatus(ActivityStatusCode.Ok);            return order;        }        catch (Exception ex)        {            activity?.SetStatus(ActivityStatusCode.Error, ex.Message);            activity?.RecordException(ex);            throw;        }    }}

Register the source: .AddSource("MyApp.Orders") in the tracing builder.

Aspire Dashboard for Local Development

Run the standalone Aspire Dashboard without Aspire orchestration:

bash
docker run --rm -it -p 18888:18888 -p 4317:18889 \    mcr.microsoft.com/dotnet/aspire-dashboard:latest

Then point your app at it:

OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317

Dashboard UI is at http://localhost:18888.

Source-Generated Logging with OTel

For maximum performance, use [LoggerMessage] — eliminates boxing and allocations.

csharp
public partial class OrderService(ILogger<OrderService> logger){    [LoggerMessage(Level = LogLevel.Information,        Message = "Processing order {OrderId} for customer {CustomerId}")]    partial void LogOrderProcessing(Guid orderId, Guid customerId);}

OpenTelemetry logging automatically includes TraceId and SpanId when an Activity is current.

Anti-patterns

Don't Create Meters Per Request

csharp
// BAD — new Meter per request causes memory leakspublic void HandleRequest(){    var meter = new Meter("MyApp");    meter.CreateCounter<int>("requests").Add(1);}
// GOOD — singleton via IMeterFactorypublic class MyMetrics(IMeterFactory meterFactory){    private readonly Counter<int> _requests =        meterFactory.Create("MyApp").CreateCounter<int>("myapp.requests");    public void RequestHandled() => _requests.Add(1);}

Don't Skip Null Checks on Activity

csharp
// BAD — NullReferenceException when no listener is attachedusing var activity = source.StartActivity("Work");activity.SetTag("key", "value");
// GOOD — null-safeactivity?.SetTag("key", "value");

Don't Use High-Cardinality Metric Tags

csharp
// BAD — unbounded cardinality causes memory explosion in collectors_counter.Add(1, new("request.id", Guid.NewGuid().ToString()));_counter.Add(1, new("user.id", userId));
// GOOD — low-cardinality dimensions only_counter.Add(1, new("http.method", "GET"), new("http.status_code", 200));

Don't Mix UseOtlpExporter with AddOtlpExporter

csharp
// BAD — throws NotSupportedException at runtimebuilder.Services.AddOpenTelemetry()    .UseOtlpExporter()    .WithTracing(t => t.AddOtlpExporter());
// GOOD — use one approachbuilder.Services.AddOpenTelemetry().UseOtlpExporter();

Don't Forget to Register Custom Sources

csharp
// BAD — activities silently dropped (no listener registered)var source = new ActivitySource("MyApp.Custom");using var activity = source.StartActivity("Work"); // null!
// GOOD — register in the tracing builderotel.WithTracing(t => t.AddSource("MyApp.Custom"));otel.WithMetrics(m => m.AddMeter("MyApp.Custom"));

Decision Guide

ScenarioRecommendation
Full observability setupAddOpenTelemetry() with all three signals + UseOtlpExporter()
Custom business metricsIMeterFactory + singleton metrics class
Custom trace spansActivitySource + StartActivity()
Local development backendAspire Dashboard standalone container
Production backendOTel Collector as intermediary to Grafana/Datadog/etc.
Sampling in productionOTEL_TRACES_SAMPLER=parentbased_traceidratio with 10% ratio
High-performance logging[LoggerMessage] source generator
Metric tag cardinalityMax ~1000 combinations per instrument
Environment configurationOTEL_* env vars (also work via appsettings.json)

Source and attribution

Source:codewithmukesh/dotnet-claude-kitinskills/opentelemetryat commit2330089

License: No license

Content belongs to its original authors. SourceWeft indexes it from a public repository.

Report or request removal

More from codewithmukesh/dotnet-claude-kit

Wrap Up

codewithmukesh

Captures end-of-session work, pending tasks and learnings into a handoff file, and reloads it at session start.

Productivity & Workflow754updated 2 months ago

Workflow Mastery

codewithmukesh

Claude Code workflow mastery for .NET developers. Covers parallel execution with git worktrees, plan mode strategy, verification loops, auto-formatting hooks, permission setup for dotnet CLI, prompting techniques, subagent patterns, and context discipline — token budget management, MCP-first navigation, lazy loading, and subagent isolation — all adapted for the .NET ecosystem. Load this skill when setting up Claude Code for a .NET project, optimizing workflows, running parallel sessions, when context is running low or sessions feel sluggish, when exploring a large codebase efficiently, or when the user mentions "productivity", "workflow", "parallel", "worktree", "plan mode", "permissions", "hooks", "10x", "setup Claude Code", "speed up development", "context", "tokens", "budget", "running out of context", "too many files", or "large codebase". Inspired by tips from Boris Cherny (creator of Claude Code) and the Anthropic team.

Awaiting classification754updated 2 months ago

Vertical Slice

codewithmukesh

Guides .NET developers in structuring applications with Vertical Slice Architecture, covering feature folders, endpoint grouping and handler patterns.

Software Development754updated 2 months ago

Testing

codewithmukesh

Testing strategy for .NET 10 applications. Covers xUnit v3, WebApplicationFactory for integration tests, Testcontainers for real database testing, Verify for snapshot testing, and the AAA pattern. Load this skill when writing tests, setting up test infrastructure, reviewing test coverage, or when the user mentions "test", "xUnit", "WebApplicationFactory", "Testcontainers", "integration test", "unit test", "bUnit", "snapshot test", "Verify", "test coverage", "AAA pattern", "WireMock", or "FakeTimeProvider".

Awaiting classification754updated 2 months ago

Tdd

codewithmukesh

Guided test-driven development workflow for .NET 10 using xUnit v3, WebApplicationFactory, Testcontainers, and Verify snapshots. Follows the strict red-green-refactor cycle. Use when: "TDD", "test-driven", "let's TDD this", "red green refactor", "write the test first", or when building a feature with clear acceptance criteria.

Awaiting classification754updated 2 months ago

Spec

codewithmukesh

Turns a vague feature idea into an agreed, persisted specification file through structured questioning rounds.

Productivity & Workflow754updated 2 months ago