Be Software Architecture Wise
Be Wise Apps

Leer en español · Lire en français · Auf Deutsch lesen

Section 9.6 of 9.8 · Intermediate · 26 min read

Observability: logs, metrics and traces

Video lesson

5 min · narrated, with captions
Listen only (audio)
Read the transcript

Welcome to Section nine point six. When a system misbehaves in production, you need to find out why, quickly. This lesson is about building systems that let you do that.

A car dashboard shows a few dials and warning lights: speed, fuel, engine temperature. That's monitoring. It tells you about problems someone predicted. But when the car makes a strange noise that no light explains, the dashboard can't help. A mechanic plugs in a diagnostic computer and reads detailed records of what every part was doing, to work out a fault nobody anticipated. Observability means building software so that this kind of investigation is always possible: every part keeps clear records, and one journey can be followed from part to part.

Monitoring answers questions you knew to ask in advance: is the disk ninety per cent full? Observability is a property of the system: can you explain behaviour nobody predicted? An order might be slow because a new menu feature makes one query slow, but only for restaurants with more than five hundred items. No one wrote a dashboard for that. Observability lets you slice the telemetry by any attribute until the pattern appears. So it must be designed in, through consistent instrumentation and shared identifiers. It can't be bolted on during an incident.

Metrics are numbers aggregated over time, such as requests per second. They're cheap, and ideal for dashboards and alerts, but they can't tell you about one particular request. Logs record individual events. Write them as structured fields, with agreed names, rather than free text. And never log secrets or unnecessary personal data. Traces show one request's path through many services, and where its time went. And profiles show where the code itself spends processor time or memory.

A common metrics mistake is averaging percentiles. The menu service runs on two instances. Instance A is healthy, with a ninety ninth percentile of a hundred and eighty milliseconds. Instance B has a noisy neighbour. Three thousand of its ten thousand requests are slow, and its ninety ninth percentile is one thousand and eighty milliseconds. Average the two, and the dashboard shows six hundred and thirty milliseconds. But the slowest two hundred of all twenty thousand requests are on instance B. The true figure is one thousand and sixty milliseconds. The dashboard understates the tail by four hundred and thirty milliseconds. Percentiles can't be averaged. Instead, add up histogram buckets across servers, and read the percentile from the merged result.

Two short checklists cover most of what to measure. For anything that serves requests, measure rate, errors and duration. That's the RED method, from Tom Wilkie. For every resource, such as a processor, a disk or a connection pool, measure utilisation, saturation and errors. That's the USE method, from Brendan Gregg. Google's four golden signals, latency, traffic, errors and saturation, are in effect RED plus saturation. In practice, RED describes what users experience, and USE explains why. Rising payment latency might be explained by a connection pool that's full, with forty requests waiting.

Across services there's no single stack trace, so distributed tracing rebuilds one. Every request gets a trace identifier at the edge, and every unit of work becomes a span. This trace shows one place order request, which took nine hundred and forty milliseconds. Pricing took just seventy five milliseconds. The payment service took seven hundred and sixty, and inside it, the external card provider took seven hundred and twenty. That's where the time went: not in the platform's own services. Tracing depends on every service passing the trace context on, using the standard traceparent header. One service that drops it breaks every trace that passes through. OpenTelemetry is now the vendor-neutral standard for producing traces, metrics and logs.

Telemetry can cost more than the system it observes. Every distinct combination of metric labels is a separate time series. One request duration histogram on the order service already makes a hundred and twenty six thousand series. Add a label for each of forty thousand restaurants, and that becomes about five billion. Per restaurant questions belong in traces and logs. Finally, page people on symptoms users feel, such as a fast SLO burn, not on causes such as high CPU. Every page should be urgent, actionable and real.

The full section covers OpenTelemetry's Collector, trace sampling and dashboards that answer questions. Next, in Section nine point seven, we look at capacity planning and performance testing. See you there.

What you need to know

Monitoring and observability

Monitoring means collecting predefined measurements and checks and raising an alarm when they cross a threshold: disk 90% full, error rate above 1%, health check failing. It answers questions you knew to ask in advance. Observability is a property of the system: how well you can understand its internal state, including states nobody predicted, from the telemetry it already emits, without shipping new code to find out. The word comes from control theory, where Rudolf Kálmán defined it in 1960 as the ability to infer a system's internal state from its outputs.

The distinction matters because distributed systems mostly fail in new ways. A food-delivery order can be slow because one restaurant's tablet app retries aggressively, because one payment provider's endpoint in one region is degraded, or because a new menu feature makes one query slow only for restaurants with more than 500 items. No one wrote a dashboard for any of these. Monitoring tells you that checkout is slow; observability lets you ask for whom, where and why by slicing the telemetry by any attribute until the pattern appears.

Practitioners disagree about how far the distinction goes. One view, common among tool vendors, treats observability as an umbrella over three pillars: logs, metrics and traces. Charity Majors, Liz Fong-Jones and George Miranda (Observability Engineering, O'Reilly, 2022) argue that the pillars framing misses the point, and that observability requires rich, high-cardinality, structured events that can be queried in arbitrary combinations. Both camps agree on the architectural consequence: observability is a quality attribute that must be designed in, through consistent instrumentation, shared identifiers and context propagation, because it cannot be bolted on during an incident.

Kinds of telemetry

SignalWhat it isBest atMain cost or limit
MetricsNumeric time series, aggregated per interval and labelled by a few dimensionsCheap long-term trends, dashboards, alerting on SLIsPre-aggregated: cannot answer questions about individual requests; labels must be low-cardinality
LogsTimestamped records of discrete events, ideally structuredDetail about one event: an error, a decision, an audit entryVolume and cost grow with traffic; free text is hard to query
TracesTrees of timed spans showing one request's path through servicesFinding where time goes and which hop failed in a distributed callMust be sampled at scale; useless if any hop drops the context
ProfilesSamples of where code spends CPU time or allocates memoryExplaining why one service is slow or expensive at the code levelNewer tooling; continuous profiling adds a little overhead
Wide eventsOne richly attributed record per request per serviceAd hoc slicing by any attribute (customer, version, region)Needs a store built for high-cardinality queries

Structured logging

A log line such as Payment failed for order 81F3 after 3001ms is easy to write and hard to use: every query needs a fragile regular expression, and every developer phrases things differently. Structured logging writes each entry as key-value fields, usually JSON, with agreed names: timestamp, level, service, event name, business identifiers (order, restaurant, courier), outcome, duration and the trace and span identifiers that link the line to its trace. Queries become precise (all card_authorisation_failed events for provider cardco in the last hour, grouped by http_status), and logs from different services can be joined.

A few rules keep logs useful and affordable. Log events, not prose; use a fixed event name per kind of event. Use levels consistently, and do not log at error level for conditions nobody needs to act on. Never log secrets, tokens, full card numbers or unnecessary personal data; telemetry pipelines are copied, shipped to third parties and kept for months, so they are a common source of data leaks, and personal data in logs falls under the GDPR like any other copy (Section 8.7). Security logging and detection, a related but different purpose with its own retention and integrity needs, is covered in Section 8.8.

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
             version-trace id (16 bytes)-parent span id (8 bytes)-flags (01 = sampled)

{"ts": "2026-10-22T12:31:07.412Z", "level": "error",
 "service": "payment-service", "event": "card_authorisation_failed",
 "order_id": "ord_81F3", "provider": "cardco", "http_status": 504,
 "duration_ms": 3001, "attempt": 2,
 "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736", "span_id": "00f067aa0ba902b7"}
A W3C traceparent header and a structured log entry from the payment service. The shared trace_id lets a query jump from this log line to the whole trace.

Metrics and why percentiles cannot be averaged

Most metric systems, such as Prometheus (a CNCF graduated project) and the OpenTelemetry metrics model, offer the same few types. A counter only increases, such as total requests served; you read it as a rate (requests per second over the last five minutes), which also survives restarts. A gauge is a current value that rises and falls: queue length, connections in use, memory. A histogram counts observations into buckets (for example requests under 50 ms, under 100 ms, under 250 ms and so on) plus a total count and sum, from which percentiles are estimated. Prometheus also has a summary type that computes quantiles inside each process.

The distinction between histograms and summaries is not a detail. As Section 2.4 noted, a percentile is a property of a whole distribution, and the percentile of a combined distribution cannot be computed from the percentiles of its parts. Averaging p99 values across servers, or across minutes, produces a number that is not a percentile of anything. Bucket counts, by contrast, can simply be added: sum each bucket across all servers, then estimate the percentile from the merged histogram. That is why fleet-wide latency SLIs should be built on histograms (or on counts of requests under a threshold, as in Section 9.5), never on per-instance summaries.

Worked example

Worked example: averaging two p99s

The menu service runs on two instances, each handling 10,000 requests in a five-minute interval. Instance A is healthy: all its requests take 20 to 300 ms, and its p99 is 180 ms. Instance B has a noisy neighbour: 7,000 requests take 20 to 300 ms, but 3,000 are spread evenly between 500 and 1,100 ms.

B's p99 (its slowest 100 of 10,000)1,100 - 600 × (100 / 3,000) = 1,080 ms
Average of the two p99s(180 + 1,080) / 2 = 630 ms
True p99 of all 20,000 requestsThe slowest 200 are all on B: 1,100 - 600 × (200 / 3,000) = 1,060 ms
Error from averaging630 ms reported against 1,060 ms experienced: the dashboard understates the tail by 430 ms
With histogramsAdd A's and B's bucket counts, then read the 99th percentile from the merged buckets: about 1,060 ms, limited only by bucket width

Averaged percentiles can be wrong in either direction, and the error grows when instances carry different traffic.

The same applies over time: the p99 for an hour is not the average of twelve five-minute p99s.

What to measure: RED, USE and the golden signals

Three short checklists cover most of what a service should expose. The RED method, proposed by Tom Wilkie in 2015, applies to anything that serves requests: Rate (requests per second), Errors (failed requests per second) and Duration (the distribution of response times). The USE method, from Brendan Gregg, applies to resources: for every CPU, disk, network link, connection pool and queue, check Utilisation (how busy it is), Saturation (how much work is waiting) and Errors. The four golden signals from the Google SRE book's chapter on monitoring distributed systems are latency, traffic, errors and saturation: in effect RED plus saturation.

In practice, RED describes what users experience and USE explains why. If the payment service's duration rises (RED), USE on its resources might show a connection pool at 100% utilisation with 40 requests waiting (saturation), which in turn points to the slow card provider holding connections open. Measure latency separately for successes and failures: a fast error is not good latency, and a timeout is both an error and a slow response.

Three checklists compared

ChecklistApplies toSignalsTypical question it answers
RED (Wilkie)Request-driven servicesRate, errors, durationIs the order service serving users well right now?
USE (Gregg)Resources: CPU, memory, disks, pools, queuesUtilisation, saturation, errorsWhich resource is the bottleneck?
Four golden signals (SRE book)User-facing systemsLatency, traffic, errors, saturationIs the system healthy, and how close is it to its limits?

Distributed tracing

In a monolith a stack trace shows where a slow request spent its time. Across services there is no single stack. Distributed tracing rebuilds one: every request gets a trace identifier at the edge, every unit of work becomes a span (with a name, start time, duration, status, attributes and the identifier of its parent span), and a tracing backend assembles the spans into a tree. Google's Dapper paper (Sigelman and colleagues, 2010) described the approach at scale: low overhead, instrumentation in shared libraries rather than in application code, and sampling so that only a fraction of requests is recorded.

Tracing depends entirely on context propagation: each service must pass the trace context to the next. The W3C Trace Context specification (a W3C Recommendation since 2020) standardises this for HTTP with a traceparent header holding a version, a 16-byte trace ID, the 8-byte ID of the calling span and flags such as whether the trace is sampled, plus an optional tracestate header for vendor data. For messaging, the same values travel in message headers, and the consumer's span refers back to the producer's. A single service that drops the header (an old library, a hand-written HTTP client, a batch job) breaks every trace that passes through it, which is why propagation belongs in shared platform libraries, not in each team's code.

Recording every span of every request is expensive at scale, so traces are sampled. Head-based sampling decides at the start of the request (keep 1 in 100) and is cheap, but it throws away most of the rare slow and failed requests you care about. Tail-based sampling buffers spans until the request finishes and then keeps all errors, all requests slower than a threshold and a small fraction of the rest; it needs more memory in the collector but keeps the interesting traces.

A trace across the food-delivery services

A trace waterfall for one Place order request: the gateway span contains the order-service span, whose payment span is dominated by a 720 ms call to the external card providerTrace 4bf92f35...: one Place order request (each bar is a span)0 ms250 ms500 ms750 ms1000 msapi-gatewayPOST /orders940 msorder-serviceplace_order918 mspricing-serviceGET /quote75 mspayment-serviceauthorise760 mscard providerPOST /auth (external)720 msorders databaseINSERT order32 msbrokerpublish OrderPlaced20 msTime since the gateway received the request
The waterfall shows at once that 720 of the 940 ms are spent waiting for the external card provider, not in the platform's own services.

OpenTelemetry: one standard for instrumentation

Until about 2019, each tracing and monitoring vendor had its own agents and libraries, so changing vendor meant re-instrumenting every service. OpenTelemetry (OTel) was formed in 2019 by merging two earlier open-source projects, OpenTracing and OpenCensus, under the Cloud Native Computing Foundation (CNCF), and graduated as a CNCF project in May 2026. It provides language APIs and SDKs, automatic instrumentation for common frameworks and clients, the OTLP wire protocol, the Collector, and semantic conventions that fix attribute names such as http.request.method and http.response.status_code so that telemetry from different teams and languages lines up. Traces, metrics and logs are stable; profiles were added as a fourth signal and entered public alpha in 2026.

The architectural value is the separation of instrumentation from backend. Services emit OTLP to a Collector, usually running beside each node or as a central gateway; the Collector batches, samples, removes sensitive attributes, adds metadata such as the deployment version, and exports to whichever stores the organisation uses. Changing vendor, or sending the same data to two backends during a migration, becomes a Collector configuration change instead of a code change in two hundred services. A platform team typically provides the SDK setup, propagation and Collector as a paved road, so that product teams only add business attributes.

Telemetry pipeline

Services instrumented with OpenTelemetry pass trace context to each other and export telemetry to a collector, which processes it and sends it to metric, trace and log stores used by dashboards, alerts and queriesSERVICES (OTel SDK)order-servicetraces, metrics, logspayment-servicetraces, metrics, logsdispatch-servicetraces, metrics, logsHTTP + traceparentevent + trace contextOTLPCollectorreceivebatch, sampleredact, enrichexportMetric storetime seriesTrace storespans by trace IDLog storestructured eventsFeeds dashboards, alerts, queries
Instrumentation lives in the services, processing in the Collector and storage in replaceable backends.
from opentelemetry import trace
import structlog

tracer = trace.get_tracer("order-service")
log = structlog.get_logger()

def place_order(order):
    with tracer.start_as_current_span("place_order") as span:
        span.set_attribute("restaurant.id", order.restaurant_id)
        span.set_attribute("order.item_count", len(order.items))
        quote = pricing.quote(order)       # instrumented HTTP client adds
        payment.authorise(order, quote)    # the traceparent header itself
        log.info("order_placed", order_id=order.id,
                 total_pence=quote.total_pence)
        return order.id
Manual instrumentation with the OpenTelemetry Python API and a structured logger. Auto-instrumented HTTP clients create child spans and propagate context without extra code.

Cardinality and cost

Telemetry can easily cost more than the system it observes. The main driver for metrics is cardinality: every distinct combination of label values is a separate time series that must be stored and indexed. Labels with a few values (HTTP method, status class, region) are fine. Labels with unbounded values (user ID, order ID, full URL with identifiers in it, error message text) multiply series without limit and can bring a metrics system down.

Worked example

Worked example: counting time series

The order service exports a request-duration histogram with 12 buckets. Prometheus stores each bucket as a series, plus a sum and a count, so 14 series per label combination.

Route and method pairs60
Status classes (1xx to 5xx)5
Instances30
Series for this one metric60 × 5 × 30 × 14 = 126,000
Add a region label (5 regions)126,000 × 5 = 630,000 series
Add restaurant_id (40,000 restaurants)126,000 × 40,000 = about 5 billion potential series: unworkable

Per-restaurant or per-customer questions belong in traces, logs or wide events, where high cardinality is expected, not in metric labels.

Exemplars, which attach a sample trace ID to a histogram bucket, link a latency spike on a cheap metric to an expensive, detailed trace.

Other cost levers are sampling traces (tail-based sampling keeps the useful ones), shorter retention for detailed data and longer retention for aggregates, dropping debug logs in production by default, and charging telemetry back to the teams that produce it so that its cost is visible. Treat telemetry volume as a design parameter with a budget, like any other resource.

Alerting on symptoms, not causes

The SRE book's guidance is that pages should be about symptoms users feel, not about the many possible causes. High CPU, a full disk on one replica or a restarted container are causes: often harmless, sometimes already handled by automation, and never a complete list of what can go wrong. A burning SLO, failing checkouts or a stalled courier-position pipeline are symptoms: they matter whatever their cause. Symptom-based alerts are fewer, rarely false and cover failures nobody predicted; cause-based signals belong on dashboards that help diagnose a symptom once it is paged.

Every page should be urgent, actionable and real. Alert fatigue is a reliability risk in itself: on-call engineers who are woken by meaningless alerts learn to dismiss them and miss the one that matters. A practical rule is to page on SLO burn rates (Section 9.5), send slower burns and resource warnings to a ticket queue, and link every page to a runbook and a dashboard.

Decision guide

Decision guide: should this alert page a human?

Apply these questions to every alert rule before it is allowed to wake someone, and again whenever it fires without anyone needing to act.

  1. Does the condition mean users are being hurt now, or will be soon (an SLO burning fast, a journey failing)?
    YesContinue to the next question.
    NoDo not page. Put it on a dashboard for diagnosis, or open a ticket if someone should look in working hours.
  2. Does it need a human to act, rather than an automatic response such as a restart or scale-out?
    YesContinue to the next question.
    NoAutomate the response and alert only if the automation fails.
  3. Must the action happen before the next working day?
    YesPage, and link the alert to a runbook and the relevant dashboard.
    NoSend it to the team's ticket queue instead of the pager.
  4. Has this alert paged in the last month without anyone needing to do anything?
    YesFix the threshold or the signal, or delete the alert: a page nobody acts on teaches people to ignore pages.
    NoKeep it, and review it at the next on-call handover.

Dashboards that answer questions

A wall of fifty graphs answers no question quickly. Good dashboards are designed like any user interface: for a named audience and a specific question. A service overview answers is this service healthy? with its SLOs, remaining error budget and RED metrics at the top, and the USE metrics of its key resources below. A journey dashboard for the on-sale answers are fans getting tickets? with admissions, seat holds, payments and failures per minute end to end. Diagnostic dashboards are reached by drilling down, not shown first.

Keep dashboards in version control as code, give each an owner, show deployments and feature-flag changes as annotations on the graphs (most incidents follow a change), and delete dashboards nobody has opened for months. During an incident, the first useful view is usually the user-facing journey, the second the trace of a failing request, and only then the resource metrics.

Check your understanding

Why does the p99 for a whole fleet have to be computed from merged histograms rather than from each instance's p99?

Because a percentile of combined data is not a function of the parts' percentiles; bucket counts add exactly, so the merged histogram gives a true (bucket-limited) estimate, while averaged p99s can be badly wrong.

What breaks if one service in a call chain does not forward the traceparent header?

Every trace that passes through it is split: spans after that hop start a new trace, so the end-to-end picture and the link between cause and symptom are lost.

Why is order_id a poor metric label but a good span attribute?

As a label it creates a new time series per order, exploding cardinality and cost; as a span or log attribute it is stored once per event, where high cardinality is expected and useful for queries.

Which of these should page someone: CPU at 95% on one instance, or checkout SLO burning at rate 20?

The SLO burn, because it is a symptom users feel; the CPU reading is a possible cause that belongs on a diagnostic dashboard.

Next · 9.7Capacity and testingHow to plan capacity before demand arrives and test that it is really there: forecasting, headroom and lead times, load, stress, soak, spike and breakpoint tests, open and closed workloads, coordinated omission and performance budgets.Continue →

Sources and further reading

Have a question about this section? ArchitectBot, the course’s AI study assistant, answers from the course first.

Ask: “We have logs from twenty services but cannot tell why some orders are slow. What should we add first: tracing, better logs or more metrics?”