S

super-dev — full-stack

@super-dev.app · Full-stack · ★ open to work
12.8k subscribers·47 videos·Active since 2017

Full-stack .NET / Angular dev with a DevOps streak on Azure Cloud. Also fluent in Flutter + Firebase. I build products, ship them, and keep them healthy.

Back to articles
DEVOPS
DEVOPS

.NET observability with OpenTelemetry

Article 6 of 6 — Azure DevOps from scratch
Prerender, Container Apps, secrets, CI/CD, Docker, observability: ship to Azure without a server or the pain.

When a request crosses three services and turns slow, logs alone don't tell you where . Modern observability rests on three correlated signals — traces, metrics, logs — and OpenTelemetry is its vendor-neutral standard: you instrument once and export to any backend (Jaeger, Prometheus, Azure Monitor) without rewriting code.

Three signals, one API

OpenTelemetry unifies the three pillars of observability. Traces follow a request end to end through a series of spans correlated by a trace_id . Metrics aggregate counters and histograms (request rate, p95 latency). Logs add the textual context, now attached to the current trace_id . .NET exposes these concepts natively through System.Diagnostics.Activity (the spans) and System.Diagnostics.Metrics .

C#
1var builder = WebApplication.CreateBuilder(args);
2
3builder.Services.AddOpenTelemetry()
4 .ConfigureResource(r => r.AddService("api-super-dev"))
5 .WithTracing(t => t
6 .AddAspNetCoreInstrumentation()
7 .AddHttpClientInstrumentation()
8 .AddOtlpExporter())
9 .WithMetrics(m => m
10 .AddAspNetCoreInstrumentation()
11 .AddRuntimeInstrumentation()
12 .AddOtlpExporter());

Auto-instrumentation vs manual spans

Auto-instrumentation covers the essentials for free: AddAspNetCoreInstrumentation creates a span per incoming request, and AddHttpClientInstrumentation propagates the context on outbound calls — cross-service correlation happens on its own through the W3C standard traceparent headers. For business logic, you add manual spans to measure a precise operation and attach domain attributes to it.

C#
1private static readonly ActivitySource Source = new("SuperDev.Orders");
2
3public async Task<Order> PlaceOrderAsync(Cart cart)
4{
5 using var activity = Source.StartActivity("place-order");
6 activity?.SetTag("order.items", cart.Items.Count);
7 activity?.SetTag("order.total", cart.Total);
8
9 var order = await _repository.SaveAsync(cart);
10 activity?.SetTag("order.id", order.Id);
11
12 return order;
13}

Attributes ( SetTag ) turn a trace into a debugging tool: you filter by order.total > 1000 or pinpoint the exact span whose latency blew up.

The OTLP exporter and the Collector

OTLP (OpenTelemetry Protocol) is the common transport format. Instead of exporting straight to a backend, you send everything to the Collector : an intermediate process that receives, transforms (batching, sampling, scrubbing sensitive attributes) and redistributes to one or more destinations. The app knows only one endpoint; switching backends becomes a config change on the Collector side, not a redeploy.

YAML
1receivers:
2 otlp:
3 protocols:
4 grpc:
5 endpoint: 0.0.0.0:4317
6processors:
7 batch:
8 timeout: 5s
9exporters:
10 prometheus:
11 endpoint: 0.0.0.0:8889
12 otlp/jaeger:
13 endpoint: jaeger:4317
14service:
15 pipelines:
16 traces:
17 receivers: [otlp]
18 processors: [batch]
19 exporters: [otlp/jaeger]
20 metrics:
21 receivers: [otlp]
22 processors: [batch]
23 exporters: [prometheus]

The app points at the Collector via OTEL_EXPORTER_OTLP_ENDPOINT , a standard environment variable. The OpenTelemetry documentation covers the sampling ( ParentBased , TraceIdRatioBased ) that is essential in production so you don't drown under trace volume.

Instrumenting with OpenTelemetry decouples your code from your monitoring tool. The day you migrate from Jaeger to Azure Monitor, you don't touch a single line of the app : you just swap the Collector's exporter.
super-dev — portfolio.app
// More in DEVOPS
>_
A GitHub Actions → Azure CI/CD pipeline, from scratch
8 min • 1.9k reads
Multi-stage Docker images for .NET + Angular
9 min • 2.6k reads