OpenTelemetry: The Standard Framework for Logs, Metrics, and Traces
A practical guide to OpenTelemetry — the vendor-neutral, CNCF-graduated standard for instrumenting applications with logs, metrics, and distributed traces — covering the three pillars of observability, the .NET SDK, context propagation across the async and messaging patterns covered elsewhere in this series, and how it fits into a complete observability stack.
Table of Contents
- Introduction
- Why OpenTelemetry Exists
- The Three Pillars: Traces, Metrics, and Logs
- Core Concepts
- Instrumenting a .NET Application
- Automatic vs. Manual Instrumentation
- Context Propagation Across Service Boundaries
- Context Propagation Through Messaging
- The Collector
- Sampling
- Correlating Logs, Metrics, and Traces
- Backends: Where the Data Actually Goes
- Common Pitfalls
- Quick Reference Table
- Conclusion
Introduction
OpenTelemetry (often shortened to "OTel") is a vendor-neutral, open-source observability framework — a single set of APIs, SDKs, and data formats for generating and exporting traces, metrics, and logs, regardless of which backend (Prometheus, Jaeger, Datadog, Azure Monitor, or any other) eventually stores and visualizes that data. It's the direct technical answer to the observability gaps this series has flagged repeatedly — the correlation IDs needed for event-driven tracing (Event-Driven Architecture guide), the health checks and metrics needed for background services (Background Services guide), and the security event logging needed for OWASP-aware systems (OWASP Top 10 guide) — unified under one consistent instrumentation standard.
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddOtlpExporter())
.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddOtlpExporter());
A few lines of configuration, and an ASP.NET Core application starts emitting standardized traces and metrics for every incoming request and every outgoing HTTP call — instrumented once, exportable to whichever backend an organization chooses, without the application code needing to know or care which specific product is actually consuming that data.
1. Why OpenTelemetry Exists
The problem: every vendor had its own proprietary instrumentation
Before OpenTelemetry, instrumenting an application for observability typically meant picking a specific vendor's SDK (a proprietary APM agent, a vendor-specific tracing library) and having that choice baked into the application's code — switching observability vendors later meant re-instrumenting the entire codebase, and using multiple tools simultaneously (a tracing vendor and a separate metrics vendor) meant maintaining two entirely separate sets of instrumentation.
OpenTelemetry's answer: instrument once, export anywhere
Application code → OpenTelemetry API/SDK (vendor-neutral) → Exporter → [Prometheus | Jaeger | Datadog | Azure Monitor | ...]
OpenTelemetry separates the instrumentation (what your code does to generate observability data) from the backend (where that data ultimately goes) via a pluggable exporter model — the application code calls the same OpenTelemetry APIs regardless of which backend is currently configured, and switching backends (or sending data to more than one simultaneously) is a configuration change, not a re-instrumentation project.
A merger of two prior projects
OpenTelemetry formed from the merger of OpenTracing and OpenCensus (two earlier, competing standardization efforts) under the Cloud Native Computing Foundation — it's now a CNCF-graduated project with broad industry backing, and has become the de facto standard that most observability vendors (including ones with their own historical proprietary agents) now support natively as an ingestion format, precisely because standardizing on it benefits the entire ecosystem rather than locking customers into one vendor's specific tooling.
2. The Three Pillars: Traces, Metrics, and Logs
Traces: the path of a single request through a distributed system
Trace: "Place Order" (250ms total)
├── Span: HTTP POST /orders (250ms)
│ ├── Span: OrderService.CreateAsync (180ms)
│ │ ├── Span: SQL INSERT INTO Orders (40ms)
│ │ └── Span: HTTP call to InventoryService (120ms)
│ └── Span: PublishEvent OrderPlaced (15ms)
A trace represents the full journey of a single logical operation (an incoming HTTP request, say) across every service and component it touches, composed of nested spans (Section 3) — this is the tool for answering "what happened, in what order, and where did the time actually go" for one specific request, directly extending the tracing needs flagged in this series' Event-Driven Architecture and gRPC guides.
Metrics: aggregated, numerical measurements over time
http_server_request_duration_seconds{route="/orders", method="POST"} — histogram
http_server_active_requests{route="/orders"} — gauge
orders_placed_total — counter
Metrics are numerical measurements aggregated over time — counts, durations, rates — optimized for dashboards, alerting thresholds, and answering "how is the system behaving in aggregate right now, and how does that compare to an hour ago" rather than "what happened to this one specific request." This connects directly to the health-check and DORA-metric discussions covered in this series' Background Services and CI/CD Pipelines guides.
Logs: discrete, timestamped events with context
2026-08-01T14:32:01Z [Information] OrderService: Order 1001 created for customer 42
2026-08-01T14:32:01Z [Warning] InventoryService: Stock low for product 17 (3 remaining)
Logs are the most familiar pillar — discrete, timestamped records of specific events, often carrying rich unstructured or semi-structured context. OpenTelemetry's logging support is the newest and historically least mature of the three pillars (traces and metrics reached stability first), but it's now a first-class part of the specification, with the specific goal of correlating log entries directly with the trace/span that was active when they were emitted (Section 10).
Why "three pillars," and why they need to work together
Each pillar answers a different question well and answers the others' questions poorly — metrics tell you that p99 latency spiked at 2pm, but not why; traces tell you exactly what happened in one specific slow request, but aggregating across thousands of traces to spot a trend is impractical; logs give rich detail about a specific event, but have no inherent structure connecting them to the broader request they were part of. OpenTelemetry's real value is treating these as three views into the same underlying instrumented behavior, correlated together (Section 10), rather than three entirely separate systems a developer has to mentally stitch together by hand.
3. Core Concepts
Spans: the building block of a trace
using var activity = MyActivitySource.StartActivity("ProcessOrder");
activity?.SetTag("order.id", orderId);
activity?.SetTag("order.total", order.Total);
// ... do the work ...
A span represents a single unit of work with a start time, an end time, and contextual attributes — .NET's built-in System.Diagnostics.Activity class is OpenTelemetry's span implementation for .NET (a deliberate design choice: OpenTelemetry didn't invent a new tracing primitive for .NET, it standardized around one already built into the runtime), which is why span creation in .NET code looks like ordinary Activity usage rather than a separate, OpenTelemetry-specific API.
Trace context: the thread tying spans together
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
│ └────────── trace ID ──────────┘ └──── span ID ────┘ flags
version
The W3C Trace Context standard (which OpenTelemetry adopted rather than inventing its own) defines how trace identity propagates across process and service boundaries via the traceparent HTTP header — every span within the same logical operation shares the same trace ID, while each individual span gets its own unique span ID, and a span's "parent span ID" links it back to whatever span caused it to start, building the nested tree structure shown in Section 2.
Resources: identifying what produced the telemetry
builder.Services.AddOpenTelemetry().ConfigureResource(resource => resource
.AddService(serviceName: "order-api", serviceVersion: "1.4.2"));
A resource describes the entity producing telemetry — the service name, version, deployment environment, host — attached to every trace, metric, and log the application emits, so a backend receiving telemetry from dozens of services can distinguish which service, instance, and version actually produced any given piece of data.
Instrumentation libraries vs. the API/SDK
OpenTelemetry API — the interfaces application code and libraries code against
OpenTelemetry SDK — the actual implementation: processing, sampling, exporting
Instrumentation library — pre-built code that automatically instruments a specific framework/library (ASP.NET Core, HttpClient, EF Core)
This layered structure is what allows a library author to add OpenTelemetry instrumentation to their package without depending on any specific SDK or backend — they code against the API, and whichever application eventually uses that library brings its own SDK configuration and exporter choice.
4. Instrumenting a .NET Application
Full setup: traces, metrics, and logs together
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource => resource.AddService("order-api"))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddEntityFrameworkCoreInstrumentation()
.AddSource("OrderApi.Custom")
.AddOtlpExporter())
.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation()
.AddOtlpExporter());
builder.Logging.AddOpenTelemetry(logging =>
{
logging.IncludeFormattedMessage = true;
logging.IncludeScopes = true;
logging.AddOtlpExporter();
});
This single configuration block, added once at startup, instruments incoming ASP.NET Core requests, outgoing HttpClient calls, EF Core database queries, .NET runtime metrics (GC, thread pool), and structured logs — all exported via OTLP (the OpenTelemetry Protocol, the standard wire format) to whatever collector or backend is configured to receive it (Section 8).
Custom spans for application-specific logic
private static readonly ActivitySource MyActivitySource = new("OrderApi.Custom");
public async Task<Order> PlaceOrderAsync(CreateOrderRequest request)
{
using var activity = MyActivitySource.StartActivity("PlaceOrder");
activity?.SetTag("customer.id", request.CustomerId);
var order = await _repository.CreateAsync(request);
activity?.SetTag("order.id", order.Id);
return order;
}
Automatic instrumentation (Section 5) covers framework-level operations (HTTP requests, database calls) automatically, but genuinely meaningful business logic — "place an order," "process a refund" — benefits from explicit custom spans with business-relevant tags, giving traces the semantic richness needed to actually answer "why was this specific order slow," not just "why was some HTTP request slow."
Custom metrics
private static readonly Meter OrderMeter = new("OrderApi.Metrics");
private static readonly Counter<int> OrdersPlacedCounter = OrderMeter.CreateCounter<int>("orders.placed");
public async Task<Order> PlaceOrderAsync(CreateOrderRequest request)
{
var order = await _repository.CreateAsync(request);
OrdersPlacedCounter.Add(1, new KeyValuePair<string, object?>("customer.tier", request.CustomerTier));
return order;
}
.WithMetrics(metrics => metrics.AddMeter("OrderApi.Metrics"))
Custom business metrics (orders placed, revenue processed, cache hit rate) use .NET's built-in System.Diagnostics.Metrics API — again, OpenTelemetry standardizing around an existing .NET primitive rather than introducing a parallel one — registered with the SDK via AddMeter so they're exported alongside the automatically-collected framework metrics.
5. Automatic vs. Manual Instrumentation
What automatic instrumentation covers
tracing.AddAspNetCoreInstrumentation() // every incoming HTTP request becomes a span automatically
.AddHttpClientInstrumentation() // every outgoing HttpClient call becomes a span automatically
.AddEntityFrameworkCoreInstrumentation() // every EF Core database command becomes a span automatically
Instrumentation libraries exist for nearly every commonly used .NET framework and library — ASP.NET Core, HttpClient, EF Core, gRPC, Redis clients, and many message broker clients — automatically wrapping their operations in spans with sensible default tags (HTTP method, status code, route; SQL command text; and so on), with zero manual span-creation code needed for the framework-level operations they cover.
What automatic instrumentation can't know
Automatic instrumentation genuinely doesn't and can't know your business's specific concepts — "this is a premium customer's order," "this operation is part of the refund workflow, not the order workflow" — that context only comes from explicit custom spans and tags (Section 4) added deliberately at the points in your code where that business meaning actually exists.
The practical default: layer both
The right approach for essentially every production .NET application is enabling the relevant automatic instrumentation for the frameworks actually in use (near-zero cost, broad, consistent coverage) and adding custom spans/metrics specifically at the business-logic boundaries that matter most for understanding and diagnosing the system — not choosing one over the other.
6. Context Propagation Across Service Boundaries
The problem: a trace needs to survive crossing into another process
Service A (creates the trace) → HTTP call → Service B (needs to know it's part of the SAME trace, not a new one)
Without deliberate propagation, each service would generate its own independent trace for what's actually one logical, end-to-end operation — losing exactly the cross-service visibility that makes distributed tracing valuable in the first place (directly the problem flagged, without a specific solution named, in this series' Event-Driven Architecture guide's observability section).
Automatic propagation via HTTP headers
GET /inventory/check HTTP/1.1
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
When both the calling and receiving services use OpenTelemetry's automatic instrumentation for HttpClient and ASP.NET Core respectively, the W3C Trace Context header (Section 3) is propagated automatically, entirely transparently to application code — Service B's incoming request span is automatically linked as a child of Service A's outgoing call span, with no manual header-passing code required, connecting directly to the gRPC and REST guides' service-to-service call patterns covered elsewhere in this series.
Propagation across gRPC
tracing.AddGrpcClientInstrumentation();
The same automatic propagation applies to gRPC calls (covered in this series' gRPC guide) via the equivalent instrumentation package — trace context flows through gRPC's own metadata mechanism analogously to how it flows through HTTP headers for REST calls.
7. Context Propagation Through Messaging
Why this is genuinely harder than synchronous HTTP/gRPC propagation
As covered in this series' Event-Driven Architecture guide, an asynchronous message doesn't have the same natural "caller waits for response" structure a synchronous HTTP call does — trace context needs to be explicitly carried through the message itself (typically as message headers/properties), since there's no ambient request context automatically flowing across an asynchronous, potentially much-later-processed boundary.
Propagating trace context through message brokers
// Publishing: inject the current trace context into the message
var propagator = Propagators.DefaultTextMapPropagator;
var contextToInject = new Dictionary<string, string>();
propagator.Inject(new PropagationContext(Activity.Current?.Context ?? default, Baggage.Current),
contextToInject, (carrier, key, value) => carrier[key] = value);
message.ApplicationProperties["traceparent"] = contextToInject["traceparent"];
// Consuming: extract the trace context and start a new span linked as a CHILD of the original trace
var parentContext = propagator.Extract(default, message.ApplicationProperties,
(carrier, key) => carrier.TryGetValue(key, out var value) ? new[] { value } : Array.Empty<string>());
using var activity = MyActivitySource.StartActivity("ProcessOrderEvent", ActivityKind.Consumer, parentContext.ActivityContext);
This is the concrete implementation of the correlation-ID propagation concept covered generally in this series' Event-Driven Architecture guide, using OpenTelemetry's standardized context propagation mechanism specifically — the RabbitMQ, Kafka, and Azure Service Bus guides in this series each reference message headers/properties as the carrier for this exact kind of context; OpenTelemetry provides the standardized format and API for actually doing it consistently, rather than every team inventing its own ad-hoc correlation header scheme.
Messaging-specific instrumentation libraries
tracing.AddSource("OpenTelemetry.Instrumentation.Kafka") // where available, per-broker instrumentation packages exist
Instrumentation libraries for specific message brokers (with varying levels of maturity across the ecosystem) can automate much of the header injection/extraction shown above — worth checking for the specific broker and client library in use before hand-rolling the propagation code, since a well-maintained instrumentation package handles edge cases (message batching, consumer group semantics) more robustly than a manual first attempt typically would.
8. The Collector
Why a separate Collector process, rather than exporting directly from every application
Application → OTLP → Collector → [batches, filters, routes] → Backend(s)
The OpenTelemetry Collector is a standalone, vendor-agnostic process that receives telemetry (via OTLP) from applications and processes/routes it before forwarding to one or more backends — rather than every single application needing direct network access to and configuration for a specific backend (or several), applications only need to know how to talk to a nearby Collector, which centralizes backend configuration, buffering, retry logic, and data transformation.
A basic Collector configuration
receivers:
otlp:
protocols:
grpc:
http:
processors:
batch:
exporters:
otlp/tempo:
endpoint: tempo:4317
prometheus:
endpoint: 0.0.0.0:8889
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp/tempo]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [prometheus]
This configuration receives OTLP telemetry, batches it for efficiency, and routes traces to one backend (Tempo) and metrics to another (Prometheus) — a single point of configuration for "where does our telemetry actually go," changeable without touching a single application's code or requiring a redeployment of every service.
Deploying the Collector
# As a sidecar (one Collector per pod, per this series' Kubernetes/Helm guide)
# or as a cluster-wide DaemonSet/Deployment, receiving from every application in the cluster
Common deployment patterns include running the Collector as a sidecar alongside each application instance (simplest network path, more resource overhead per instance) or as a shared, cluster-wide deployment (more efficient resource usage, one additional network hop) — the right choice depends on scale and the specific Kubernetes/container orchestration patterns already covered in this series' Kubernetes/Helm and Docker guides.
9. Sampling
Why you (usually) don't want every single trace
At genuinely high request volume, capturing and exporting a full, detailed trace for every single request can become a substantial cost and storage burden in its own right — sampling deliberately captures only a subset of traces, trading complete coverage for a sustainable data volume.
Head-based sampling: decide at the very start of a trace
tracing.SetSampler(new TraceIdRatioBasedSampler(0.1)); // sample 10% of traces
The simplest approach — a sampling decision is made when a trace begins (often based on a random ratio, or a rate-limiting rule) and that decision propagates to every subsequent span in the trace, ensuring a trace is either fully captured or not captured at all, never partially. The downside: this decision is made before anything interesting (like an error) has actually happened, so a purely random sample might miss the very traces you'd most want to have captured — the ones involving a failure.
Tail-based sampling: decide after seeing the whole trace
Collector buffers all spans for a trace briefly, THEN decides whether to keep it —
e.g., "always keep traces containing an error, or exceeding 1 second, sample everything else at 5%"
Tail-based sampling (typically implemented at the Collector level, since it requires seeing the complete trace before deciding) lets you keep traces that are actually interesting — ones with errors, or unusually high latency — at a much higher rate than routine, successful, fast traces, giving a far better cost-to-diagnostic-value ratio than pure random sampling, at the cost of requiring the Collector to buffer and correlate all of a trace's spans before making the keep/discard decision.
A sensible default posture
Start with a conservative head-based sampling rate for genuinely high-volume production services (to control cost), and layer in tail-based sampling at the Collector specifically to ensure errors and slow requests are essentially always captured regardless of the baseline sampling rate — the pattern most production observability setups converge on once volume genuinely warrants it.
10. Correlating Logs, Metrics, and Traces
The payoff of using one unified framework for all three pillars
2026-08-01T14:32:01Z [Error] OrderService: Payment failed for order 1001
TraceId: 4bf92f3577b34da6a3ce929d0e0e4736
SpanId: 00f067aa0ba902b7
Because OpenTelemetry's logging integration automatically attaches the currently-active trace and span ID to every log entry, a developer investigating an error log can jump directly from that log line to the exact distributed trace it occurred within — seeing the full request path across every service involved, not just the one log line from one service — closing the "logs and traces feel like two disconnected systems" gap that plagued observability setups before this kind of automatic correlation was standard.
Exemplars: linking metrics back to specific traces
http_server_request_duration_seconds_bucket{le="1.0"} 45 # exemplar: trace_id=4bf92f35...
Some metrics backends support exemplars — a sampled reference from a metric data point (say, a slow bucket in a latency histogram) back to a specific trace ID that contributed to it — letting you go from "p99 latency spiked" directly to "here's an actual example trace from that spike" without needing to separately search traces by timestamp and hope you find a representative one.
Why this matters more than any single pillar in isolation
The genuinely differentiated value OpenTelemetry provides isn't "you can now collect traces" or "you can now collect metrics" in isolation (plenty of older, single-purpose tools already did each of those) — it's that a single, consistently-applied instrumentation framework makes moving fluidly between all three views of the same underlying system behavior possible, which is a substantially more powerful diagnostic capability than the sum of three separately-instrumented, uncorrelated tools.
11. Backends: Where the Data Actually Goes
OpenTelemetry defines the instrumentation standard, not the storage/visualization layer
Traces → Jaeger, Tempo, Zipkin, Azure Monitor / Application Insights, Datadog, Honeycomb
Metrics → Prometheus, Azure Monitor, Datadog, Grafana Cloud
Logs → Loki, Azure Monitor, Elasticsearch, Datadog
OpenTelemetry deliberately doesn't include a storage or visualization backend itself — it's the instrumentation and transport standard, with a wide ecosystem of backends (open-source and commercial) accepting OTLP as an ingestion format. This is precisely the vendor-neutrality benefit from Section 1 made concrete: an organization can start with an open-source stack (Prometheus + Grafana + Tempo, all commonly deployed together in Kubernetes environments per this series' Kubernetes/Helm guide) and later migrate to a commercial APM product, or vice versa, changing only the Collector's export configuration.
Azure Monitor / Application Insights as a first-party .NET-adjacent option
builder.Services.AddOpenTelemetry()
.UseAzureMonitor(); // Azure Monitor Distro — configures OTLP export to Application Insights automatically
For teams already deep in the Azure ecosystem (per this series' Azure Compute guide), Azure Monitor's OpenTelemetry Distro provides a streamlined path to sending standard OpenTelemetry data directly into Application Insights, combining OpenTelemetry's vendor-neutral instrumentation with Azure's own first-party observability backend and its tight integration with the rest of the Azure Compute guide's services (App Service, Functions, AKS).
Grafana's LGTM stack as a common open-source pairing
Loki (logs), Grafana (visualization), Tempo (traces), and Mimir/Prometheus (metrics) form a widely used, fully open-source observability stack that accepts OpenTelemetry data natively — a common choice for teams wanting to avoid vendor lock-in entirely, particularly in Kubernetes-native environments already using the GitOps and Helm patterns covered elsewhere in this series to deploy and manage the stack itself.
12. Common Pitfalls
| Pitfall | Why it hurts | Better approach |
|---|---|---|
| Instrumenting only one pillar (usually just logs) and calling it "observability" | Misses the correlation value that makes OpenTelemetry genuinely powerful | Instrument traces, metrics, and logs together from the start where feasible |
| No context propagation through messaging | An event-driven chain of processing becomes untraceable as a single logical operation | Explicitly inject/extract trace context through message headers, per Section 7 |
| Capturing 100% of traces at high volume with no sampling strategy | Unsustainable storage/ingestion cost at scale | Apply head-based sampling for volume control, tail-based sampling to still capture errors/slow requests |
| Relying entirely on automatic instrumentation with no custom spans | Traces show framework-level detail but no business-meaningful context | Add custom spans/tags at genuinely important business-logic boundaries |
| Exporting directly from every application straight to a backend, with no Collector | Backend configuration scattered across every service; harder to change backends later | Route through an OpenTelemetry Collector for centralized configuration and processing |
| Treating OpenTelemetry setup as a one-time task | Instrumentation coverage silently degrades as new services/endpoints are added without matching instrumentation | Include instrumentation coverage review as part of the same CI/CD quality discipline covered elsewhere in this series |
| Logging sensitive data (tokens, passwords) into trace attributes or log messages | Telemetry backends become a sensitive-data exposure surface, echoing this series' JWT Validation and Secret Management guides | Apply the same "never log secrets" discipline to telemetry attributes as to ordinary application logs |
Quick Reference Table
| Concept | Purpose |
|---|---|
| Trace | The end-to-end path of one logical operation across services |
| Span | A single unit of work within a trace, with start/end time and tags |
| Metric | Aggregated numerical measurement over time (counter, gauge, histogram) |
| Log | A discrete, timestamped event, correlated with the active trace/span |
W3C Trace Context (traceparent) |
The standard header format propagating trace identity across boundaries |
| Resource | Metadata identifying which service/instance produced a piece of telemetry |
| OTLP | The OpenTelemetry Protocol — the standard wire format for exporting telemetry |
| Collector | A standalone process centralizing telemetry receipt, processing, and export routing |
| Head-based sampling | Sampling decision made at trace start, applied uniformly |
| Tail-based sampling | Sampling decision made after seeing the full trace, favoring errors/slow requests |
| Exemplar | A link from a metric data point back to a specific representative trace |
Conclusion
OpenTelemetry's core contribution is standardization — one consistent way to instrument traces, metrics, and logs, decoupled from whichever specific backend an organization chooses today or migrates to later, built on top of primitives (Activity, Meter) already native to .NET rather than a separate, parallel API surface to learn. The genuine payoff isn't any single pillar in isolation — it's the ability to move fluidly between a metric spike, a representative trace, and the specific log lines within it, across every service a request or event touched, which is precisely the observability capability this series has flagged as necessary throughout its background processing, messaging, and event-driven architecture guides, but left to "instrument this somehow" until now.
Getting it right in practice comes down to a consistent set of habits: instrument all three pillars together rather than just logs, propagate context deliberately across both synchronous calls and asynchronous messages, sample thoughtfully once volume demands it (never losing the errors and outliers that matter most), and route everything through a Collector to keep backend choice a configuration decision rather than a re-instrumentation project. Get that right, and diagnosing a problem across a distributed, partially event-driven system — the kind of system this series has spent considerable effort helping you build — becomes tractable rather than a matter of guesswork stitched together from disconnected logs.
Found this useful? Feel free to star the repo, open an issue with corrections, or share the exemplar link that took you straight from a latency spike to the exact trace that explained it.
Top comments (0)