Phase 2: Observability

Correlating traces with logs and metrics

Intermediate ~3 min read
Think of it this way A friendly analogy. Read this if the technical version feels dense. Show Hide

Imagine you're baking a super special cake for a big party. It's not just one person making it; it's a whole team in a big kitchen, with different people mixing, baking, and frosting. Sometimes, things go wrong – maybe the cake turns out a bit flat, or it doesn't taste right. When that happens, you want to figure out why so you can fix it next time and make sure all the future cakes are perfect!

In our big kitchen, we have different ways to keep track of what's happening. We have logs, which are like little sticky notes or a diary entry every time something specific happens: "Oops, dropped an egg!" or "Oven temperature is too low." Then we have metrics, which are like charts and measurements: "This cake baked for 40 minutes," or "We used 5 cups of flour for all the cakes today." And finally, we have the trace, which is the full story of one specific cake: from when you first mixed the batter, all the way through baking, cooling, and decorating. The problem is, these pieces of information often get written down in different notebooks or on different charts, making it hard to connect them.

This is where the idea of "correlating" comes in! It's like giving every single cake its own special, secret "Cake ID" number. And for each big step in making that cake (like mixing, baking, or frosting), you give it a "Step ID." So, whenever someone writes a sticky note (a log) or takes a measurement (a metric) for that specific cake, they also write down its "Cake ID" and the "Step ID" for what they were doing at that moment. It's like saying, "This note about too much salt? That was for Cake #123, during the mixing step."

With these special IDs, if Cake #123 turns out flat, you can instantly pull up all the sticky notes (logs) and measurements (metrics) that belong only to Cake #123. You might quickly see a note that says "Oven temperature dropped suddenly for Cake #123 during baking!" or a measurement showing "Baking time for Cake #123 was only 20 minutes." This means you can quickly find out exactly what went wrong with a particular cake, instead of sifting through hundreds of notes and measurements for all the cakes. It helps you become a super detective for finding problems and making sure future cakes are perfect!

In a distributed system, observability data often lives in three separate pillars: logs, metrics, and traces. While each provides valuable insights, their true power emerges when you can correlate them. Correlating traces with logs and metrics means linking these distinct data types together using common identifiers, allowing you to seamlessly navigate from a high-level overview (metrics) to a specific sequence of operations (traces) and granular events or errors within those operations (logs).

The practical implementation of correlation heavily relies on propagating unique identifiers, primarily trace_id and span_id. When an application generates a log entry, the trace_id and the span_id of the currently active span should be injected into that log. This is best achieved through structured logging, where these IDs become dedicated fields. Similarly, custom metrics emitted within the context of a trace can be tagged with the trace_id and span_id. Modern tracing libraries and observability platforms often automate much of this injection, ensuring that your logs and metrics carry the necessary contextual baggage to be linked back to the originating trace.

For an SRE, this correlation is a game-changer. Imagine an alert firing based on a spike in error rates (metric). With correlation, you can immediately jump from that metric dashboard to the relevant traces showing which service calls failed. From those traces, you can then drill down into specific spans to find the exact log lines (including error messages, stack traces, and request details) that correspond to the failure point. This unified view drastically reduces the Mean Time To Resolution (MTTR) by eliminating context switching between disparate tools and providing a complete narrative of a request's journey and any issues it encountered.

Key Takeaways

  • Correlation links traces, logs, and metrics for a comprehensive view of system behavior.
  • The primary identifiers for correlation are trace_id and span_id.
  • Embed trace_id and span_id in logs via structured logging and in metrics via tags/labels.
  • It enables seamless navigation between observability tools, such as clicking a trace to view related logs or vice-versa.
  • Significantly reduces Mean Time To Resolution (MTTR) by speeding up root cause analysis.

Code Example

python
import logging
from opentelemetry import trace

# Assume OpenTelemetry is configured and context is propagated automatically
# In a real application, your tracing library would provide the current context.
current_span = trace.get_current_span()

trace_id = format(current_span.context.trace_id, "02x") if current_span.context.is_valid else "0"
span_id = format(current_span.context.span_id, "02x") if current_span.context.is_valid else "0"

# Configure a logger (often done once at app startup)
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# Example of structured logging with trace context
logger.info(
    "Processing payment request",
    extra={
        "trace_id": trace_id,
        "span_id": span_id,
        "user_id": "user-123",
        "amount": 99.99
    }
)

# Expected log output (format may vary based on logger config):
# INFO:__main__:Processing payment request {'trace_id': '...', 'span_id': '...', 'user_id': 'user-123', 'amount': 99.99}

How this code works

This code demonstrates how to link traditional log messages directly to the context of a distributed trace. Its primary job is to embed unique identifiers, specifically a trace_id and span_id, into application logs, making it straightforward to correlate system events across different services. The opentelemetry library, through trace.get_current_span(), retrieves the current_span representing the ongoing operation. From this span, the trace_id and span_id are extracted. The format function ensures these IDs are presented as hexadecimal strings, and importantly, the is_valid check handles the subtle case where no active trace context exists, gracefully defaulting to "0" instead of failing to extract the IDs.

After obtaining the trace identifiers, the code sets up standard Python logging using logging.basicConfig and logger = logging.getLogger(__name__). The core of the correlation happens when logger.info is called. Instead of just a message, an extra dictionary is passed. This dictionary is crucial for structured logging, allowing the trace_id and span_id to be included alongside other relevant business data like user_id and amount. By adding these trace context identifiers to every log entry, engineers can easily query log aggregators for all messages related to a specific trace_id during troubleshooting, effectively bridging the gap between distributed traces and detailed log information.