agentleFS
zdanovichnickzdanovichnick/dotnet-pilot/skills/opentelemetry/SKILL.md

opentelemetry

Skill5 starsChanged 6 days agoMITView on GitHub

OpenTelemetry SDK for .NET — tracing, custom spans, metrics, .NET Aspire integration, and OTLP exporter configuration.

4 other projects use a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

4 rules. 0 are common, 1 is distinctive, and 3 are very specific to this project.

  • 0

    Common

    Most of the 3 edited versions we read keep these.

  • 1

    Distinctive

    This author's own choices. Worth a look.

  • 3

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Use OpenTelemetry semantic conventions where they exist.

    line 96
  2. Distinctive:

    Do not tag spans with PII (email, name, card number, SSN).

    line 105
  3. Very specific:

    Use IMeterFactory for DI-friendly meter creation.

    line 117
  4. Very specific:

    When using .NET Aspire, OTEL is pre-configured via AddServiceDefaults() in the ServiceDefaults project.

    line 152

“Common” is measured: kept by at least 6 in 10 of the 3 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

OpenTelemetry for .NET

Reference material for adding distributed tracing and metrics to ASP.NET Core services.

NuGet Packages

<!-- Core -->
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.*" />

<!-- Tracing instrumentation -->
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.*" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.*" />
<!-- Prerelease only (no stable 1.x yet) — a plain "1.*" range never resolves a beta -->
<PackageReference Include="OpenTelemetry.Instrumentation.EntityFrameworkCore" Version="1.*-*" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.*" />

<!-- Exporter -->
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.*" />

Setup — Program.cs

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddAspNetCoreInstrumentation(options =>
        {
            options.RecordException = true;
            options.Filter = ctx => ctx.Request.Path != "/health"; // exclude health checks
        })
        .AddEntityFrameworkCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddSource("MyApp.Orders")  // register custom ActivitySource names
        .AddOtlpExporter())
    .WithMetrics(metrics => metrics
        .AddAspNetCoreInstrumentation()
        .AddRuntimeInstrumentation()
        .AddMeter("MyApp.Orders")   // register custom Meter names
        .AddOtlpExporter());

The EF instrumentation no longer has SetDbStatementForText (removed in 1.13.0-beta.1): SQL text is always captured and sanitized for SQL-like providers. Parameter values are opt-in via OTEL_DOTNET_EXPERIMENTAL_EFCORE_ENABLE_TRACE_DB_QUERY_PARAMETERS=true — keep that off in production, parameters carry PII.

Custom Spans (Activity)

ActivitySource is .NET's API for creating custom spans. One source per logical module.

// Register as singleton in DI:
builder.Services.AddSingleton(new ActivitySource("MyApp.Orders"));
// Usage in a handler or service:
public class ProcessOrderHandler(IOrderRepository orders, ActivitySource activitySource)
{
    public async Task<Order> HandleAsync(ProcessOrderCommand cmd, CancellationToken ct)
    {
        using var activity = activitySource.StartActivity("ProcessOrder");
        activity?.SetTag("order.id", cmd.OrderId.ToString());
        activity?.SetTag("order.customer_id", cmd.CustomerId.ToString());

        try
        {
            var order = await orders.GetByIdAsync(cmd.OrderId, ct)
                ?? throw new NotFoundException($"Order {cmd.OrderId} not found.");

            activity?.SetTag("order.status", order.Status.ToString());

            // business logic...

            return order;
        }
        catch (Exception ex)
        {
            activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
            activity?.AddException(ex); // .NET 9+; RecordException is obsolete
            throw;
        }
    }
}

Span Tag Conventions

Use OpenTelemetry semantic conventions where they exist. For business attributes, use <domain>.<attribute> format:

AttributeExample value
order.id"ord_abc123"
order.status"Pending"
customer.tier"Premium"
payment.method"CreditCard"

Do not tag spans with PII (email, name, card number, SSN).

Child Spans

using var parent = activitySource.StartActivity("CreateOrder");
using var validationSpan = activitySource.StartActivity("ValidateOrder", ActivityKind.Internal);
// validationSpan is automatically a child of parent due to Activity.Current

Custom Metrics

Use IMeterFactory for DI-friendly meter creation. One Meter per logical module.

// Register metrics class as singleton:
builder.Services.AddSingleton<OrderMetrics>();
public sealed class OrderMetrics
{
    private readonly Counter<long> _ordersCreated;
    private readonly Histogram<double> _orderProcessingDuration;
    private readonly UpDownCounter<long> _pendingOrders;

    public OrderMetrics(IMeterFactory factory)
    {
        var meter = factory.Create(new MeterOptions("MyApp.Orders") { Version = "1.0" });
        _ordersCreated = meter.CreateCounter<long>("orders.created", "orders", "Total orders created");
        _orderProcessingDuration = meter.CreateHistogram<double>("orders.processing_duration", "ms", "Order processing time");
        _pendingOrders = meter.CreateUpDownCounter<long>("orders.pending", "orders", "Current pending orders");
    }

    public void RecordOrderCreated(string tier)
        => _ordersCreated.Add(1, new TagList { { "customer.tier", tier } });

    public void RecordProcessingDuration(double ms, string status)
        => _orderProcessingDuration.Record(ms, new TagList { { "order.status", status } });

    public void IncrementPending() => _pendingOrders.Add(1);
    public void DecrementPending() => _pendingOrders.Add(-1);
}

.NET Aspire Integration

When using .NET Aspire, OTEL is pre-configured via AddServiceDefaults() in the ServiceDefaults project. Do not manually call AddOpenTelemetry() in projects that call AddServiceDefaults() — it's already wired.

// AppHost project — resource registration
var api = builder.AddProject<Projects.MyApp_Api>("myapp-api");

// API project — ServiceDefaults handles OTEL, health checks, service discovery
builder.AddServiceDefaults(); // replaces manual AddOpenTelemetry() setup
app.MapDefaultEndpoints();    // maps /health and /alive

ServiceDefaults reads OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_SERVICE_NAME from the environment. Aspire injects these automatically when running via the AppHost.

You still need to register custom ActivitySource and Meter names:

// In ServiceDefaults ConfigureOpenTelemetry or in the service itself:
.AddSource("MyApp.Orders")
.AddMeter("MyApp.Orders")

OTLP Exporter Configuration

Via environment variables (preferred — works with Aspire and Docker):

{
  "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4317",
  "OTEL_SERVICE_NAME": "myapp-api",
  "OTEL_SERVICE_VERSION": "1.0.0"
}

Via code (for explicit control):

.AddOtlpExporter(options =>
{
    options.Endpoint = new Uri("http://localhost:4317");
    options.Protocol = OtlpExportProtocol.Grpc; // or HttpProtobuf
})

Correlation with Serilog

Serilog 3.1+ stamps TraceId and SpanId from Activity.Current onto every log event natively — Serilog.Enrichers.Span is deprecated and no longer needed. Text sinks show them only when the output template includes {TraceId} / {SpanId}; structured formatters and the OTLP sink carry them automatically.

Do / Don't

DoDon't
Register ActivitySource as singletonCreate new ActivitySource(...) per request
Tag spans with business-meaningful IDs (order ID, user tier)Tag spans with PII
Use SetStatus(Error) + AddException in catch blocksSwallow exceptions without marking the span failed
Exclude health check endpoints from tracingTrace /health and /alive — creates noise
Keep EF query-parameter capture off in productionSet OTEL_DOTNET_EXPERIMENTAL_EFCORE_ENABLE_TRACE_DB_QUERY_PARAMETERS outside dev
Use ActivityKind.Internal for internal spansLeave ActivityKind unset on child spans

See Also

  • skills/logging/SKILL.md — Serilog setup; this file owns log↔trace correlation
  • .NET Aspire docs: AddServiceDefaults() source in ServiceDefaults/Extensions.cs

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.