diff --git a/src/dotnet/skills/configuring-opentelemetry-dotnet/SKILL.md b/src/dotnet/skills/configuring-opentelemetry-dotnet/SKILL.md new file mode 100644 index 0000000000..642f47f677 --- /dev/null +++ b/src/dotnet/skills/configuring-opentelemetry-dotnet/SKILL.md @@ -0,0 +1,264 @@ +```skill +--- +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. +--- + +# Configuring OpenTelemetry in .NET + +## When to Use + +- Adding distributed tracing to an ASP.NET Core application +- Setting up OpenTelemetry exporters (OTLP, Jaeger, Prometheus) +- Creating custom metrics or trace spans for business operations +- Troubleshooting distributed trace context propagation across services + +## When Not to Use + +- The user wants application-level logging only (use ILogger, Serilog) +- The user is using Application Insights SDK directly (different API) +- The user needs APM with a commercial vendor's proprietary SDK + +## Inputs + +| Input | Required | Description | +|-------|----------|-------------| +| ASP.NET Core project | Yes | The application to instrument | +| Observability backend | No | Where to export: Jaeger, Prometheus, OTLP collector, Aspire | + +## Workflow + +### Step 1: Install the correct packages + +**There are many OpenTelemetry NuGet packages. Install exactly these:** + +```bash +# Core SDK + ASP.NET Core instrumentation +dotnet add package OpenTelemetry.Extensions.Hosting +dotnet add package OpenTelemetry.Instrumentation.AspNetCore +dotnet add package OpenTelemetry.Instrumentation.Http + +# Exporter (pick one or more) +dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol # OTLP (recommended) +dotnet add package OpenTelemetry.Exporter.Console # Dev/debugging + +# Optional: additional auto-instrumentation +dotnet add package OpenTelemetry.Instrumentation.SqlClient # SQL Server +dotnet add package OpenTelemetry.Instrumentation.EntityFrameworkCore # EF Core +dotnet add package OpenTelemetry.Instrumentation.GrpcNetClient # gRPC +``` + +**Do NOT install `OpenTelemetry` alone** — you need `OpenTelemetry.Extensions.Hosting` for proper DI integration. + +### Step 2: Configure tracing in Program.cs + +```csharp +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; +using OpenTelemetry.Metrics; +using OpenTelemetry.Logs; + +var builder = WebApplication.CreateBuilder(args); + +// Define the service resource (appears in all telemetry) +var serviceName = "MyOrderService"; +var serviceVersion = "1.0.0"; + +builder.Services.AddOpenTelemetry() + .ConfigureResource(resource => resource + .AddService(serviceName: serviceName, serviceVersion: serviceVersion)) + .WithTracing(tracing => tracing + // Auto-instrumentation sources + .AddAspNetCoreInstrumentation(options => + { + // Filter out health check endpoints from traces + options.Filter = httpContext => + !httpContext.Request.Path.StartsWithSegments("/healthz"); + }) + .AddHttpClientInstrumentation(options => + { + // Enrich outgoing HTTP spans with request/response details + options.RecordException = true; + }) + .AddSqlClientInstrumentation(options => + { + options.SetDbStatementForText = true; // Capture SQL text + options.RecordException = true; + }) + // Custom activity sources (for your own spans) + .AddSource("MyOrderService.Orders") + .AddSource("MyOrderService.Payments") + // Exporter + .AddOtlpExporter(options => + { + options.Endpoint = new Uri("http://localhost:4317"); // gRPC endpoint + // For HTTP: options.Protocol = OtlpExportProtocol.HttpProtobuf; + // options.Endpoint = new Uri("http://localhost:4318/v1/traces"); + })) + .WithMetrics(metrics => metrics + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation() // GC, thread pool metrics + // Custom meters + .AddMeter("MyOrderService.Metrics") + .AddOtlpExporter()); +``` + +### Step 3: Add OpenTelemetry logging integration + +```csharp +// Connect ILogger to OpenTelemetry +builder.Logging.AddOpenTelemetry(logging => +{ + logging.IncludeScopes = true; + logging.IncludeFormattedMessage = true; + logging.AddOtlpExporter(); +}); +``` + +**This correlates logs with traces automatically** — each log entry gets the current TraceId and SpanId. + +### Step 4: Create custom spans (Activities) for business operations + +```csharp +using System.Diagnostics; + +public class OrderService +{ + // Create an ActivitySource matching what you registered in Step 2 + private static readonly ActivitySource ActivitySource = new("MyOrderService.Orders"); + + public async Task ProcessOrderAsync(CreateOrderRequest request) + { + // Start a new span + using var activity = ActivitySource.StartActivity("ProcessOrder"); + + // Add attributes (tags) to the span + activity?.SetTag("order.customer_id", request.CustomerId); + activity?.SetTag("order.item_count", request.Items.Count); + + try + { + // Child span for validation + using (var validationActivity = ActivitySource.StartActivity("ValidateOrder")) + { + await ValidateOrderAsync(request); + validationActivity?.SetTag("validation.result", "passed"); + } + + // Child span for payment + using (var paymentActivity = ActivitySource.StartActivity("ProcessPayment", + ActivityKind.Client)) // Client = outgoing call + { + paymentActivity?.SetTag("payment.method", request.PaymentMethod); + await ProcessPaymentAsync(request); + } + + activity?.SetTag("order.status", "completed"); + activity?.SetStatus(ActivityStatusCode.Ok); + + return order; + } + catch (Exception ex) + { + // Record the exception on the span + activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + activity?.RecordException(ex); + throw; + } + } +} +``` + +**Critical: `ActivitySource` name must match `AddSource("...")` in configuration.** Unmatched sources are silently ignored — this is the #1 debugging issue. + +### Step 5: Create custom metrics + +```csharp +using System.Diagnostics.Metrics; + +public class OrderMetrics +{ + // Meter name must match AddMeter("...") in configuration + private static readonly Meter Meter = new("MyOrderService.Metrics"); + + // Counter — use for things that only go up + private static readonly Counter OrdersProcessed = + Meter.CreateCounter("orders.processed", "orders", + "Total orders successfully processed"); + + // Histogram — use for measuring distributions (latency, sizes) + private static readonly Histogram OrderProcessingDuration = + Meter.CreateHistogram("orders.processing_duration", "ms", + "Time to process an order"); + + // UpDownCounter — use for things that go up AND down + private static readonly UpDownCounter ActiveOrders = + Meter.CreateUpDownCounter("orders.active", "orders", + "Currently processing orders"); + + // ObservableGauge — use for point-in-time values (queue depth, etc.) + // Note: registered once, callback invoked on each collection + private static readonly ObservableGauge QueueDepth = + Meter.CreateObservableGauge("orders.queue_depth", () => GetQueueDepth()); + + public void RecordOrderProcessed(string region, double durationMs) + { + // Tags enable dimensional filtering (by region, status, etc.) + var tags = new TagList + { + { "region", region }, + { "order.type", "standard" } + }; + + OrdersProcessed.Add(1, tags); + OrderProcessingDuration.Record(durationMs, tags); + } +} +``` + +### Step 6: Configure context propagation for distributed scenarios + +Trace context propagation is automatic for HTTP calls when using `AddHttpClientInstrumentation()`. For non-HTTP scenarios: + +```csharp +// Manual context propagation (e.g., across message queues) +// On the SENDING side: +var propagator = Propagators.DefaultTextMapPropagator; +var context = new PropagationContext(Activity.Current!.Context, Baggage.Current); +var carrier = new Dictionary(); + +propagator.Inject(context, carrier, (dict, key, value) => dict[key] = value); +// Send carrier dictionary as message headers + +// On the RECEIVING side: +var parentContext = propagator.Extract(default, carrier, + (dict, key) => dict.TryGetValue(key, out var value) ? new[] { value } : Array.Empty()); + +Baggage.Current = parentContext.Baggage; +using var activity = ActivitySource.StartActivity("ProcessMessage", + ActivityKind.Consumer, + parentContext.ActivityContext); // Links to parent trace! +``` + +## Validation + +- [ ] Traces appear in the observability backend (Jaeger, Aspire dashboard, etc.) +- [ ] HTTP requests automatically create spans with correct verb, URL, status code +- [ ] Custom `ActivitySource` names match `AddSource()` registrations +- [ ] Custom `Meter` names match `AddMeter()` registrations +- [ ] Logs include TraceId and SpanId for correlation +- [ ] Health check endpoints are filtered from traces +- [ ] Exception details appear on error spans + +## Common Pitfalls + +| Pitfall | Solution | +|---------|----------| +| `ActivitySource.StartActivity` returns null | Source name doesn't match any `AddSource()` — names must match exactly | +| Traces not appearing in exporter | Check OTLP endpoint: gRPC uses port 4317, HTTP uses 4318 | +| Missing HTTP client spans | `AddHttpClientInstrumentation()` only works with `IHttpClientFactory`-created clients | +| High cardinality tags | Don't use user IDs, request IDs, or UUIDs as metric tags — explodes storage | +| OTLP gRPC vs HTTP mismatch | Default is gRPC (port 4317); if collector only accepts HTTP, set `OtlpExportProtocol.HttpProtobuf` | +| `Meter` and `ActivitySource` not static | Must be static — creating per-request wastes memory and may lose data | +``` diff --git a/src/dotnet/tests/configuring-opentelemetry-dotnet/eval.yaml b/src/dotnet/tests/configuring-opentelemetry-dotnet/eval.yaml new file mode 100644 index 0000000000..4bed938dfe --- /dev/null +++ b/src/dotnet/tests/configuring-opentelemetry-dotnet/eval.yaml @@ -0,0 +1,40 @@ +scenarios: + - name: "Add OpenTelemetry tracing and metrics to an ASP.NET Core API" + prompt: | + I need to add distributed tracing and custom metrics to my ASP.NET Core 8 API. We use an OTLP-compatible collector (Jaeger). I want to: + 1. Auto-instrument incoming HTTP requests and outgoing HttpClient calls + 2. Create custom spans for my order processing business logic using ActivitySource + 3. Add a custom counter metric for orders processed using System.Diagnostics.Metrics.Meter + + Show me the full setup including package installation, Program.cs configuration, and how to create the custom spans and metrics in my service class. I want to make sure my custom traces and metrics actually show up in Jaeger — last time I tried, the auto-instrumentation worked but my custom spans were invisible. + assertions: + - type: "output_matches" + pattern: "(OpenTelemetry\\.Extensions\\.Hosting|AddOpenTelemetry)" + - type: "output_matches" + pattern: "(ActivitySource|StartActivity)" + - type: "output_matches" + pattern: "(AddSource|AddMeter)" + - type: "output_matches" + pattern: "(Counter|Histogram|CreateCounter|CreateHistogram)" + rubric: + - "Installed the correct packages: OpenTelemetry.Extensions.Hosting, OpenTelemetry.Instrumentation.AspNetCore, OpenTelemetry.Exporter.OpenTelemetryProtocol" + - "Configured AddOpenTelemetry() with .WithTracing() and .WithMetrics() in Program.cs" + - "Added AddAspNetCoreInstrumentation() and AddHttpClientInstrumentation() for auto-instrumentation" + - "Registered custom ActivitySource names via AddSource() matching the source name used in the service class" + - "Created custom spans using ActivitySource.StartActivity() with proper using/dispose pattern" + - "Created custom metrics using Meter.CreateCounter() or similar, with the Meter name matching AddMeter()" + - "Explicitly warned that ActivitySource/Meter names must match AddSource/AddMeter registrations exactly or custom telemetry will be silently dropped" + expect_tools: ["bash"] + timeout: 120 + + - name: "OpenTelemetry skill should not activate for simple logging question" + prompt: "How do I configure Serilog structured logging in my ASP.NET Core app to write to a file and console?" + assertions: + - type: "output_not_contains" + value: "OpenTelemetry" + - type: "output_not_matches" + pattern: "(ActivitySource|StartActivity|AddOpenTelemetry|OTLP)" + rubric: + - "Did NOT suggest OpenTelemetry for a Serilog logging question" + - "Provided Serilog-specific configuration guidance" + timeout: 60