When it breaks at 3am, you can find out why.
Most observability is three dashboards nobody opens and logs you grep in a panic. We wire in traces, metrics, and logs that share one trace id, so a slow request isn't a hunch: you open the trace, see the exact span that took 800ms, and read the log line that explains it, in seconds instead of an outage.
Drive it: switch the request to slow or error, click the span that stands out, and read the log line that explains it, all joined by one trace id.
click a span to inspect · slowest: postgres 750ms
the span, its attributes, and its logs, joined by one trace id
Three signals, one trace id.
What you just explored is the end of this. Each service is instrumented once, a collector gathers the spans, metrics, and logs off the hot path, and everything is stamped with the same trace id, so the three signals stay joined instead of scattered across three tools that don't talk.
One agent per service ships spans, metrics, and logs to a collector, off the request path.
Traces for where, metrics for how much, logs for what, each stored for what it's good at.
Every signal carries the trace id, so a spike, its trace, and its logs all line up.
Search across all three in one place, so debugging is a query, not a grep across boxes.
Page on the user-facing SLO, with the offending trace already attached to the alert.
Instrument once, correlate everywhere.
The reason the trace, the attributes, and the logs lined up in the console is this wrapper. It opens a span, so child calls nest under it automatically, and the logger reads the current trace id, so every line is already tagged. No manual plumbing per call, and nothing to forget.
1// instrument.ts — one wrapper makes a request a trace2import { trace, log } from "@/telemetry";34app.post("/checkout", trace("checkout", async (span, req) => {5 span.set("order.id", req.body.orderId); // searchable attribute67 log.info("charging card", { amount }); // log carries the trace id8 const res = await payments.charge(amount); // a child span, automatically9 span.set("http.status", res.status);1011 return res; // duration recorded on exit12}));The path a request took and the one span that hurt. You start here, at the symptom.
Rates, errors, and latency, cheap to keep at scale, so trends and alerts don't cost a fortune.
The line of detail, structured and tied to its trace, so it's the answer, not a haystack.
One trace id is the join key. It's what turns three separate tools into one answer.
Signals you can act on, not just admire.
A wall of graphs isn't observability. Being able to answer a question you didn't pre-plan a dashboard for is. These are the properties that make it that.
Trace, metric, and log share a trace id, so you jump from a latency spike to the exact request and its logs.
High-cardinality data goes to traces and logs, not metric labels, so dashboards stay fast and cheap to keep.
Tail sampling keeps every error and slow trace and drops the boring ones, so you see what matters without the bill.
Page on the latency and error rate the user feels, not CPU, with the trace linked so on-call starts at the cause.
Long cheap metrics, shorter rich traces and logs, so you keep the trend and the detail where each is worth it.
OpenTelemetry instrumentation, so your data isn't hostage to one backend you can't afford to leave.
A dashboard tells you something is wrong. The point is answering why without a redeploy.
Anyone can bolt on a metrics dashboard and feel covered, until the incident that isn't on any of the graphs. Then it's SSH in, add a log line, redeploy, and hope it happens again. Real observability is being able to ask a new question of data you already have: which requests were slow, what did that one do, why did it fail. We instrument for that, because the goal isn't more charts, it's a shorter path from alarm to answer.
Observability that shortens the path from alarm to answer.
Traces, metrics, and logs joined by one trace id, tail-sampled to keep the bad ones, alerting on the SLO with the trace attached, so on-call starts at the cause.