{
  "schema": "https://ai-atoms.com/schemas/skill-v1.json",
  "type": "skill",
  "id": "skill/configuring-opentelemetry-dotnet",
  "version": "1.0.0",
  "name": "configuring-opentelemetry-dotnet",
  "description": "Configure OpenTelemetry distributed tracing, metrics, and logging in ASP.NET Core using the .NET OpenTelemetry SDK. Use when adding observability, setting up OTLP exporters, creating custom metrics/spans, or troubleshooting distributed trace correlation.",
  "system_prompt_fragment": "# Configuring OpenTelemetry in .NET\n\n## When to Use\n\n- Adding distributed tracing to an ASP.NET Core application\n- Setting up OpenTelemetry exporters (OTLP is the primary protocol; Jaeger accepts OTLP natively; Prometheus OTLP ingestion requires explicit opt-in)\n- Creating custom metrics or trace spans for business operations\n- Troubleshooting distributed trace context propagation across services\n\n## When Not to Use\n\n- The user wants application-level logging only (use ILogger, Serilog)\n- The user is using Application Insights SDK directly (different API)\n- The user needs APM with a commercial vendor's proprietary SDK\n\n## Inputs\n\n| Input | Required | Description |\n|-------|----------|-------------|\n| ASP.NET Core project | Yes | The application to instrument |\n| Observability backend | No | Where to export: OTLP collector, Aspire dashboard, Jaeger (accepts OTLP natively) |\n\n## Workflow\n\n### Step 1: Install the correct packages\n\n**There are many OpenTelemetry NuGet packages. Install exactly these:**\n\n```bash\n# Core SDK + ASP.NET Core instrumentation + logging integration\ndotnet add package OpenTelemetry.Extensions.Hosting\ndotnet add package OpenTelemetry.Instrumentation.AspNetCore\ndotnet add package OpenTelemetry.Instrumentation.Http\n\n# Exporter\ndotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol  # OTLP exporter for traces, metrics, AND logs\n\n# Optional — dev/local debugging only (do NOT include in production deployments)\n# dotnet add package OpenTelemetry.Exporter.Console\n```\n\n**Do NOT install `OpenTelemetry` alone** — you need `OpenTelemetry.Extensions.Hosting` for proper DI integration.\n\n#### Optional: additional auto-instrumentation packages\n\nInstall only the packages that match the libraries your application uses:\n\n```bash\ndotnet add package OpenTelemetry.Instrumentation.SqlClient           # SQL Server queries\ndotnet add package OpenTelemetry.Instrumentation.EntityFrameworkCore  # EF Core\ndotnet add package OpenTelemetry.Instrumentation.GrpcNetClient       # gRPC calls\ndotnet add package OpenTelemetry.Instrumentation.Runtime             # GC, thread pool metrics\n```\n\n### Step 2: Configure all signals in Program.cs\n\n```csharp\nusing OpenTelemetry.Resources;\nusing OpenTelemetry.Trace;\nusing OpenTelemetry.Metrics;\nusing OpenTelemetry.Logs;\n\nvar builder = WebApplication.CreateBuilder(args);\n\nbuilder.Services.AddOpenTelemetry()\n    .ConfigureResource(resource => resource\n        .AddService(serviceName: builder.Environment.ApplicationName))\n    .WithTracing(tracing => tracing\n        .AddAspNetCoreInstrumentation(options =>\n        {\n            // Filter out health check endpoints from traces\n            options.Filter = httpContext =>\n                !httpContext.Request.Path.StartsWithSegments(\"/healthz\");\n        })\n        .AddHttpClientInstrumentation(options =>\n        {\n            options.RecordException = true;\n        })\n        // Optional: add SQL instrumentation if using SqlClient directly\n        // .AddSqlClientInstrumentation(options =>\n        // {\n        //     options.SetDbStatementForText = true;\n        //     options.RecordException = true;\n        // })\n        // Custom activity sources (must match ActivitySource names in your code)\n        .AddSource(\"MyApp.Orders\")\n        .AddSource(\"MyApp.Payments\")\n        .AddSource(\"MyApp.Messaging\"))\n    .WithMetrics(metrics => metrics\n        .AddAspNetCoreInstrumentation()\n        .AddHttpClientInstrumentation()\n        // Optional: .AddRuntimeInstrumentation() for GC and thread pool metrics\n        //   (requires OpenTelemetry.Instrumentation.Runtime package)\n        // Custom meters (must match Meter names in your code)\n        .AddMeter(\"MyApp.Metrics\"))\n    .WithLogging(logging =>\n    {\n        logging.IncludeScopes = true;\n        // logging.IncludeFormattedMessage = true;  // Enable if you need the formatted message string in log exports\n    })\n    // Single OTLP exporter for all signals — reads OTEL_EXPORTER_OTLP_ENDPOINT\n    // env var (defaults to http://localhost:4317). Override via environment variable\n    // or appsettings.json configuration.\n    .UseOtlpExporter();\n```\n\n### Step 3: Understanding log–trace correlation\n\nThe `.WithLogging()` call in Step 2 integrates ILogger with OpenTelemetry:\n\n- Each log entry automatically includes TraceId and SpanId for correlation with traces\n- The service resource from `.ConfigureResource()` propagates to logs automatically\n- `UseOtlpExporter()` applies to logs alongside traces and metrics\n- No additional packages or separate `SetResourceBuilder` call needed\n\n### Step 4: Create custom spans (Activities) for business operations\n\n```csharp\nusing System.Diagnostics;\nusing Microsoft.Extensions.Logging;\n\npublic class OrderService\n{\n    // Create an ActivitySource matching what you registered in Step 2\n    private static readonly ActivitySource ActivitySource = new(\"MyApp.Orders\");\n    private readonly ILogger<OrderService> _logger;\n\n    public OrderService(ILogger<OrderService> logger) => _logger = logger;\n\n    public async Task<Order> ProcessOrderAsync(CreateOrderRequest request)\n    {\n        // Start a new span\n        using var activity = ActivitySource.StartActivity(\"ProcessOrder\");\n\n        // Add attributes (tags) to the span\n        activity?.SetTag(\"order.customer_id\", request.CustomerId);\n        activity?.SetTag(\"order.item_count\", request.Items.Count);\n\n        try\n        {\n            // Child span for validation\n            using (var validationActivity = ActivitySource.StartActivity(\"ValidateOrder\"))\n            {\n                await ValidateOrderAsync(request);\n                validationActivity?.SetTag(\"validation.result\", \"passed\");\n            }\n\n            // Child span for payment\n            using (var paymentActivity = ActivitySource.StartActivity(\"ProcessPayment\",\n                ActivityKind.Client))  // Client = outgoing call\n            {\n                paymentActivity?.SetTag(\"payment.method\", request.PaymentMethod);\n                await ProcessPaymentAsync(request);\n            }\n\n            var order = new Order { Id = Guid.NewGuid(), CustomerId = request.CustomerId, Status = \"Completed\" };\n\n            activity?.SetTag(\"order.status\", \"completed\");\n            activity?.SetStatus(ActivityStatusCode.Ok);\n\n            return order;\n        }\n        catch (Exception ex)\n        {\n            activity?.SetStatus(ActivityStatusCode.Error, ex.Message);\n            // Log via ILogger — OpenTelemetry captures this with trace correlation.\n            // Prefer logging over activity.RecordException() as OTel is deprecating\n            // span events for exception recording in favor of log-based exceptions.\n            _logger.LogError(ex, \"Order processing failed for customer {CustomerId}\", request.CustomerId);\n            throw;\n        }\n    }\n}\n```\n\n**Critical: `ActivitySource` name must match `AddSource(\"...\")` in configuration.** Unmatched sources are silently ignored — this is the #1 debugging issue.\n\n### Step 5: Create custom metrics\n\nUse `IMeterFactory` (injected via DI) to create meters — this ensures proper lifetime management and testability.\n\n```csharp\nusing System.Diagnostics;\nusing System.Diagnostics.Metrics;\n\npublic class OrderMetrics\n{\n    private readonly Counter<long> _ordersProcessed;\n    private readonly Histogram<double> _orderProcessingDuration;\n    private readonly UpDownCounter<int> _activeOrders;\n\n    public OrderMetrics(IMeterFactory meterFactory)\n    {\n        // Meter name must match AddMeter(\"...\") in configuration\n        var meter = meterFactory.Create(\"MyApp.Metrics\");\n\n        // Counter — use for things that only go up\n        _ordersProcessed = meter.CreateCounter<long>(\n            \"orders.processed\", \"orders\", \"Total orders successfully processed\");\n\n        // Histogram — use for measuring distributions (latency, sizes)\n        _orderProcessingDuration = meter.CreateHistogram<double>(\n            \"orders.processing_duration\", \"ms\", \"Time to process an order\");\n\n        // UpDownCounter — use for things that go up AND down\n        _activeOrders = meter.CreateUpDownCounter<int>(\n            \"orders.active\", \"orders\", \"Currently processing orders\");\n    }\n\n    public void RecordOrderProcessed(string region, double durationMs)\n    {\n        // Tags enable dimensional filtering (by region, status, etc.)\n        var tags = new TagList\n        {\n            { \"region\", region },\n            { \"order.type\", \"standard\" }\n        };\n\n        _ordersProcessed.Add(1, tags);\n        _orderProcessingDuration.Record(durationMs, tags);\n    }\n}\n```\n\nRegister `OrderMetrics` in DI:\n\n```csharp\nbuilder.Services.AddSingleton<OrderMetrics>();\n```\n\n### Step 6: Configure context propagation for distributed scenarios\n\nTrace context propagation is automatic for HTTP calls when using `AddHttpClientInstrumentation()`. For non-HTTP scenarios:\n\n```csharp\nusing System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing OpenTelemetry.Context.Propagation;\n\n// ActivitySource should be static — register via .AddSource(\"MyApp.Messaging\") in Step 2\nprivate static readonly ActivitySource MessageSource = new(\"MyApp.Messaging\");\n\n// Manual context propagation (e.g., across message queues)\n// On the SENDING side:\nvar propagator = Propagators.DefaultTextMapPropagator;\nvar activityContext = Activity.Current?.Context ?? default;\nvar context = new PropagationContext(activityContext, Baggage.Current);\nvar carrier = new Dictionary<string, string>();\n\npropagator.Inject(context, carrier, (dict, key, value) => dict[key] = value);\n// Send carrier dictionary as message headers\n\n// On the RECEIVING side:\nvar parentContext = propagator.Extract(default, carrier,\n    (dict, key) => dict.TryGetValue(key, out var value) ? new[] { value } : Array.Empty<string>());\n\nBaggage.Current = parentContext.Baggage;\nusing var activity = MessageSource.StartActivity(\"ProcessMessage\",\n    ActivityKind.Consumer,\n    parentContext.ActivityContext);  // Links to parent trace!\n```\n\n## Validation\n\n- [ ] Traces appear in the observability backend (Jaeger, Aspire dashboard, etc.)\n- [ ] HTTP requests automatically create spans with correct verb, URL, status code\n- [ ] Custom `ActivitySource` names match `AddSource()` registrations\n- [ ] Custom `Meter` names match `AddMeter()` registrations\n- [ ] Logs include TraceId and SpanId for correlation\n- [ ] Health check endpoints are filtered from traces\n- [ ] Exception details appear on error spans\n\n## Common Pitfalls\n\n| Pitfall | Solution |\n|---------|----------|\n| `ActivitySource.StartActivity` returns null | Source name doesn't match any `AddSource()` — names must match exactly |\n| Traces not appearing in exporter | Check OTLP endpoint: gRPC uses port 4317, HTTP uses 4318 |\n| Missing HTTP client spans | Ensure `AddHttpClientInstrumentation()` is registered; it works for both `IHttpClientFactory`/DI and `new HttpClient()` (use `IHttpClientFactory` for lifetime management) |\n| High cardinality tags | Don't use user IDs, request IDs, or UUIDs as metric tags — explodes storage |\n| OTLP gRPC vs HTTP mismatch | Default is gRPC (port 4317); if collector only accepts HTTP, set `OtlpExportProtocol.HttpProtobuf` |\n| `Meter` / `ActivitySource` lifecycle | `ActivitySource` should be static; create `Meter` via `IMeterFactory` from DI (not `new Meter()`) for proper lifetime management and testability |",
  "applicable_domains": [
    "code",
    "dotnet",
    "engineering"
  ],
  "invocation": [
    "/configuring-opentelemetry-dotnet"
  ],
  "tags": [
    "dotnet-aspnet",
    "dotnet",
    "csharp",
    "microsoft"
  ],
  "authored_by": "anthropics",
  "source_url": "https://github.com/dotnet/skills/blob/main/plugins/dotnet-aspnet/skills/configuring-opentelemetry-dotnet/SKILL.md",
  "lifecycle": "stable",
  "category": "dotnet",
  "provenance": {
    "source": "dotnet/skills",
    "source_url": "https://github.com/dotnet/skills/blob/main/plugins/dotnet-aspnet/skills/configuring-opentelemetry-dotnet/SKILL.md",
    "author": "Microsoft / .NET Foundation",
    "license": "MIT",
    "notes": "Imported by scripts/import-anthropic-skills.py."
  }
}