Observability Fundamentals

📖 21 min read

What Observability Means

A running service is a black box. You can’t pause it and inspect its memory in production, so everything you learn about its state comes from what it emits. The term comes from control theory, where Rudolf Kálmán defined a system as observable when its internal state can be worked out from its outputs. Software borrowed the idea with a practical twist. A service is observable to the extent that you can answer a question about its behavior that nobody anticipated, using the telemetry it already produces, without shipping new code to ask it. Charity Majors, who popularized this framing, puts the test as asking new questions of your data without shipping new code.

Monitoring Watches for Problems You Predicted

Google’s SRE book defines monitoring as collecting, processing, aggregating, and displaying quantitative data about a system. In practice that means dashboards and alerts built around conditions someone expected, such as an error rate above 1%, a disk above 90%, or a queue that keeps growing. Monitoring is excellent at the problems it was built for, and it tells you quickly that something is wrong.

It struggles with problems nobody predicted. A distributed system can fail through combinations no one planned for, like one tenant’s unusual payload that only slows one code path when a cache is cold. No dashboard was built for that, and an investigation limited to predefined charts stalls there. Observability is the property that lets the investigation continue. The two aren’t opposites. Monitoring is a practice that runs on the same telemetry an observable system produces.

  Monitoring Observability
Question it answers Is something wrong? Why is it wrong, including causes nobody anticipated?
Built from Dashboards and alerts defined in advance Telemetry detailed enough to slice by any attribute after the fact
Handles well Known problems: a full disk, a crashed process, a spike in 5xx responses New problems: a slowdown that affects one customer, one region, and one API version
Where it runs out Anything nobody wrote a check for Anything the telemetry never recorded

A Worked Example

The rest of this guide follows one request. A user presses “Pay Now” on a shopping site. The request reaches a checkout API, which reads the cart from its database, calls an orders service to create the order, and then calls a payments service, which charges the card through an external payment gateway. Today checkout is slow for some users, and the goal is to find out why.

Telemetry Signals

Every signal starts from the same raw material, the events in a running system. A request arrives, a query runs, a call times out. The signals differ in what they keep from each of those events and what they throw away, and that choice sets both what each can tell you and what each costs.

Logs, metrics, and traces are often called the three pillars of observability. OpenTelemetry, the vendor-neutral standard that graduated in the CNCF in May 2026, calls them signals, and adds baggage (key-value context carried along with a request) and profiles, which entered public alpha in March 2026.

Signal What it keeps from each event What it discards Cost grows with Best at
Logs A record of the individual event, with whatever detail the code wrote Whatever the code didn’t write, and records below the configured level Traffic, and how much each request logs Explaining what happened in one place at one moment
Metrics Only a count, sum, or distribution, aggregated over a time window The individual event and its context The number of distinct label combinations (the dimensions a metric is split by), not traffic Showing trends and triggering alerts cheaply
Traces Each operation’s timing and its parent, for one request across services Requests not chosen for the sample (see Sampling) Traffic, operations recorded per request, and the share of requests kept Showing where one request spent its time
Profiles Periodic samples of which code was on the CPU or allocating memory Which request the code was serving, in most tools Number of hosts profiled and the sampling rate, not traffic Showing which functions consume resources

Logs

A log is a timestamped record of one event, written by the code where the event happened. Logs can carry more detail than any other signal, because the developer chooses exactly what to write.

Structured Logs Are Queryable

An unstructured log is a line of text:

2026-09-24 14:47:03 WARN Payment attempt 1 for order 88213 timed out after 2000ms

Finding every timeout for one order means writing a text search that matches this exact wording, and it breaks when someone rewords the message. A structured log records the same event as named fields:

{
  "timestamp": "2026-09-24T14:47:03.412Z",
  "severity": "WARN",
  "body": "Payment attempt {Attempt} for order {OrderId} timed out after {ElapsedMs}ms",
  "attributes": { "Attempt": 1, "OrderId": 88213, "ElapsedMs": 2000 },
  "resource": { "service.name": "payments", "service.version": "4.2.0" },
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "span_id": "00f067aa0ba902b7"
}

Now a query can ask for every record where ElapsedMs exceeds 1500, grouped by service.version, without parsing prose. The resource block describes the process that wrote the record rather than the event itself. The trace and span IDs tie the record to a trace, the record of one request’s path through the system that Traces describes. The fields above map onto OpenTelemetry’s log data model, which also defines where the trace and span IDs go (see How the Signals Connect). Most logging libraries produce structured output from a message template with named placeholders. In .NET that is ILogger, covered in C# Logging with Microsoft.Extensions.Logging.

Severity Levels Control Volume

Each record carries a severity. OpenTelemetry’s scale runs TRACE, DEBUG, INFO, WARN, ERROR, FATAL, and most libraries map their own names onto it. The level is how you decide, at run time and without redeploying, how much a service writes. Production services commonly keep INFO and above and turn on DEBUG only for a component under investigation, because DEBUG output can multiply log volume many times over.

Kinds of Logs

The same mechanism serves readers with different needs, and those needs set how long each kind is kept and who may read it.

Kind Records Typical reader
Application Business events, errors, and state changes inside the service Developers debugging behavior
Access One line per HTTP request or API call: method, path, status, duration Operators investigating traffic
Audit Who did what to which resource, and when Compliance and security reviewers, often under a mandated retention period
Security Sign-ins, authorization failures, policy changes Security operations
System Operating system, container runtime, and hardware events Platform operators

Where Logs Fall Short

  • Volume and cost. Log volume grows with traffic, and storing and indexing it is often one of the largest costs of observability.
  • Isolation. Each record describes one moment in one process. Without a shared ID, the log lines one request produced across several services can’t be pulled together.
  • Lost with the container. In Kubernetes, a container’s standard output is kept in rotated files on its node and removed when the pod is evicted or deleted, so logs have to be shipped off the node while they still exist.
  • Only what someone wrote. A code path with no log statement is invisible to logs, however long it takes.

Metrics

A metric is a number measured over time. http.server.request.duration for the checkout service, sampled across every request, becomes a series of values you can chart, compare with last week, and alert on.

A Series Per Label Combination

A metric carries labels (OpenTelemetry calls them attributes), such as http.request.method, http.route, and http.response.status_code. Attributes live at two levels. Resource attributes, such as service.name and service.version, describe the process producing the telemetry and are shared by everything it emits. The rest belong to one measurement, span, or log record. Each distinct combination of label values is its own time series. That is what makes metrics cheap. The checkout service can handle a million requests an hour and still produce only a few hundred series, because the metric stores aggregates per combination, not the requests themselves. It is also what makes metrics dangerous. A label whose values are unbounded, like a user ID, creates one series per user, and storage and query cost grow with it. The number of distinct values a label can take is its cardinality, and keeping it low is the main discipline metrics ask for.

High-cardinality attributes still have a home. A span (one timed operation within a trace) or a structured log record is stored once, so its storage cost barely changes whether its customer.id attribute has ten possible values or ten million. Customer, tenant, and order IDs belong there, and they are what make a question like “is this slowdown one customer?” answerable after the fact.

Instrument Types

OpenTelemetry’s metric instruments cover four shapes of measurement. Other systems, such as Prometheus, use similar types under similar names.

Instrument Records Checkout example How you usually read it
Counter A total that only goes up Payments attempted As a rate: attempts per second over the last five minutes
UpDownCounter A total that goes up and down Requests currently in flight As the current value
Gauge A value read at a point in time, where adding values from several processes means nothing Product cache hit ratio As the current value, or its maximum over a window
Histogram How many values fell into each range of sizes Request duration As percentiles, such as the p99 latency, which 99% of requests stay under

Counter, UpDownCounter, and Gauge also have asynchronous forms. Instead of the code recording every change, a callback reads the current value each time metrics are collected for export, which suits values like CPU time that are cheaper to read than to track.

Histograms Keep the Shape of Latency

Record latency as a histogram. Averages hide the slow requests users complain about, and percentiles from separate servers can’t be combined, but histogram counts can be merged across every instance, as long as they share bucket boundaries, and percentiles computed from the result. Performance Engineering covers why the tail of the distribution matters and how to set percentile targets.

What to Measure First

Three widely used checklists give a starting set, so a new service doesn’t begin with a blank dashboard.

Checklist Applies to Measures
Four golden signals (Google SRE book) Any user-facing service Latency, traffic, errors, saturation (how full the most constrained resource is)
RED method (Tom Wilkie) Each request-serving service Rate, errors, duration
USE method (Brendan Gregg) Each resource: CPU, memory, disks, connection and thread pools Utilization, saturation (work queued waiting for the resource), errors

The golden signals and RED look at a service from the outside, as its callers experience it. USE looks at the resources inside it. Business metrics like orders placed and payments declined belong alongside them, because a deploy can leave every technical metric green while orders quietly stop.

Where Metrics Fall Short

A metric tells you that checkout’s p99 latency tripled at 14:46 and that the rise came from the payments service. It can’t tell you which requests were slow or why, because aggregation already discarded the individual requests. It also only covers what someone decided to measure in advance, with the labels they chose, so a question about an attribute that wasn’t a label has no answer in metrics.

Traces

A trace records one request’s path through the system. It is how you see that checkout spent three seconds waiting on payments, and that payments spent those three seconds waiting on the gateway.

Spans Form a Tree

A trace is a tree of spans. A span is one timed operation, such as handling an HTTP request, running a query, or calling another service. Every span in the tree carries the same trace ID, and each span except the first (the root) records the ID of the span that caused it, its parent. OpenTelemetry’s span model defines these fields:

Field Holds
Name The operation, such as POST /payments
Span context The trace ID, this span’s ID, trace flags (including whether the trace is sampled), and trace state for vendor-specific values
Parent span ID The span that caused this one. Empty on the root span
Start and end timestamps When the operation began and finished
Kind Server, client, internal, producer, or consumer, which tells the backend how spans across services fit together
Attributes Key-value context, such as the HTTP method, status code, or database table
Events Timestamped points within the span, such as an exception being thrown
Links References to spans in other traces, used when one operation relates to several requests, as in batch processing
Status Unset (the default), Ok, or Error

A tracing backend draws the tree as a waterfall, with one bar per span positioned on a shared timeline and indented under its parent.

Chart

A Checkout Trace as a Waterfall

One slow checkout request's spans on a shared timeline.

Waterfall view of one checkout trace across three services Seven spans on a timeline from 0 to 3.4 seconds, indented under their parents. POST /checkout in the checkout service takes 3,400 ms. Under it, SELECT cart takes 40 ms, POST /orders in the orders service takes 180 ms with an INSERT order child of 120 ms, and POST /payments in the payments service takes 3,100 ms. Under payments, the first POST /charge client span to the external payment gateway runs for 2,000 ms and ends in Error with a timeout, and a retried POST /charge succeeds in 1,000 ms. SPAN SERVICE 0 1 s 2 s 3 s POST /checkout checkout 3,400 ms SELECT cart checkout, client 40 ms POST /orders orders 180 ms INSERT order orders 120 ms POST /payments payments 3,100 ms POST /charge (1st) payments, client 2,000 ms · Error: timeout POST /charge (retry) payments, client 1,000 ms · Ok checkout orders payments span status Error

Between the three services, the figure shows only the server side of each call, and a full trace also records a client span in the caller for each outgoing call. SELECT cart and the two POST /charge spans are client spans, because the database and the external gateway send no spans of their own. Reading the waterfall answers questions no metric could. The orders call finished in under 200 ms. Almost all of checkout’s time sits under POST /payments, and inside it the first gateway call ran for two seconds, ended in Error, and was retried. The trace has found where the time went. It hasn’t yet said why the first call timed out.

Context Propagation Carries the Trace Across Services

A trace only spans services if each call carries the trace ID and the caller’s span ID with it. This is context propagation. For HTTP the standard carrier is the W3C Trace Context traceparent header:

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01

The four fields are the format version, the 32-hex-character trace ID, the 16-hex-character ID of the calling span, and flags whose last bit marks the trace as sampled. The receiving service reads the header, starts its server span as a child of that calling span, and sends a new header on every call it makes in turn. Message queues carry the same context in message headers. When any hop drops the context, such as a proxy that strips unknown headers or a background job that starts work without it, the trace splits into two unrelated traces at that point.

Flow

Trace Context Propagation

The traceparent header carried from hop to hop, and dropped.

The traceparent header carried across services, and a hop that drops it Top: checkout sends POST /payments with a traceparent header carrying trace ID 4bf9…4736 and its client span ID 00f0…02b7. Payments starts a server span whose parent is 00f0…02b7 in the same trace, then calls the payment gateway with a new traceparent header carrying the same trace ID and its own client span ID b7ad…3331. Bottom: the same call passes through a proxy that strips unknown headers, so payments receives no traceparent and starts a new trace 9e1c…77a0, and one request is recorded as two unrelated traces. CONTEXT PROPAGATED checkout trace 4bf9…4736 client span 00f0…02b7 traceparent 00-4bf9…4736-00f0…02b7-01 payments trace 4bf9…4736 new server span, parent 00f0…02b7 client span b7ad…3331 traceparent 00-4bf9…4736-b7ad…3331-01 payment gateway external sends no spans CONTEXT DROPPED checkout trace 4bf9…4736 traceparent proxy strips unknown headers no traceparent payments starts trace 9e1c…77a0 One request, recorded as two unrelated traces.

Sampling Keeps Tracing Affordable

Trace volume grows with traffic and with the number of spans per request, as log volume does, so most systems keep only a sample of traces. With head sampling, the service that starts the trace decides whether to keep it and records the decision in the sampled flag, so downstream services that honor the flag make the same choice and the trace is kept or dropped whole. The decision is made before anyone knows whether the request will fail. Tail sampling waits until the trace is complete and keeps it by outcome, such as every error and every slow request, at the cost of buffering whole traces before deciding.

Where Traces Fall Short

  • Every hop must be instrumented. An uninstrumented service appears only as its caller’s client span, like the payment gateway in the waterfall, with no detail about what happened inside it.
  • The request you want may not have been kept. Sampling means a specific customer’s failed request might not have a trace.
  • Timing, not explanation. A trace shows that a call took two seconds and failed. The reason is usually in the logs that the code wrote during that span.

Profiles

Traces stop at the span. A profile goes inside the process and shows which functions consumed CPU time or allocated memory. A profiler interrupts the process many times a second, records the call stack it finds, and aggregates the stacks. The usual way to read the result is a flame graph.

Chart

A CPU Flame Graph

The checkout service's CPU samples, stacked by call path.

CPU flame graph for the checkout service Stacked boxes, each a function sitting on top of the function that called it, with width proportional to the share of CPU samples. At the bottom, all samples. Above it, garbage collection takes 8 percent and a thread pool worker 92 percent. The worker runs CheckoutController.Post at 88 percent. Post calls Cart.Validate at 12 percent, JsonSerializer.Serialize at 46 percent, and TaxCalculator.Compute at 22 percent. Above those, Regex.Match takes 9 percent under Cart.Validate, PropertyInfo.GetValue 16 percent and Utf8JsonWriter.WriteString 28 percent under JsonSerializer.Serialize, and TaxRateCache.Lookup 15 percent under TaxCalculator.Compute. all samples (100%) GC 8% ThreadPool worker 92% CheckoutController.Post 88% Cart.Validate JsonSerializer.Serialize 46% TaxCalculator.Compute 22% Regex.Match PropertyInfo.GetValue Utf8JsonWriter.WriteString TaxRateCache.Lookup width = share of CPU samples, sorted by name (not a timeline) caller below, callee above application code framework and runtime

Each box is a function, stacked on the function that called it, and its width is the share of samples in which it appeared. The x-axis is not time. Boxes are usually sorted alphabetically, and wide boxes are where the resource goes. This one is a CPU profile of the checkout service on an ordinary day, separate from the slow-payment investigation, and it shows that serializing the response takes more CPU than the business logic does.

Continuous Profiling

Profiling has long been an ad hoc tool. You attach a profiler to one process, reproduce the problem, and detach. Continuous profiling samples production processes all the time, at a low enough rate to leave running, and stores the results, so you can look at what the code was doing at the moment an incident happened instead of trying to reproduce it later. Tools like Grafana Pyroscope and Parca do this today. Profiles differ by what triggers a sample. CPU profiles sample running code, allocation profiles sample memory allocations, and lock profiles sample time spent waiting for contended locks. Performance Engineering covers what each kind finds.

OpenTelemetry’s profiles signal entered public alpha in March 2026. Its profiler runs as part of the OpenTelemetry Collector (a standalone telemetry agent, covered under Instrumentation) and uses eBPF, a Linux kernel feature for running observation code safely, to sample every process on a host without code changes. The project cautions against using it for critical production workloads while it is in alpha, and backend support is still arriving.

How the Signals Connect

Each signal answers part of the question. The investigation moves fast only when you can jump from one signal to the matching data in the next without searching by timestamp and guessing.

Trace IDs Join Logs to Traces

When code writes a log inside a span, the logging library can stamp the record with the current trace ID and span ID, as in the structured log above. From the failed gateway span in the waterfall, the backend can then show exactly the log lines written during that span, in every service, and nothing else.

Exemplars Join Metrics to Traces

A metric data point, most usefully a latency histogram’s, can carry an exemplar, which is the trace and span ID of one request it counted. The latency chart’s spike then links directly to a trace of a request that was actually slow, instead of leaving you to search for one.

Span IDs Join Profiles to Traces

Some profilers tag each stack sample with the trace and span that were active when it was taken, and OpenTelemetry’s profile format has fields for them. A slow span can then open a flame graph of only the code that ran during it. Support varies by tool and runtime, so confirm it before counting on it.

Resource Attributes Join Everything to Its Source

Every signal from a process carries the same resource attributes, such as service.name, service.version, and the deployment environment. They let you filter all four signals the same way, and they make questions like “did this start with version 4.2.0?” answerable in each one.

One Investigation, Signal by Signal

Put together, the slow checkout investigation reads like this:

Step Signal What it shows
1 Metric An alert fires because checkout’s p99 latency tripled at 14:46
2 Metric, by resource attribute The rise is in the payments service’s latency, and only since service.version 4.2.0 deployed
3 Trace, through an exemplar The first gateway call times out after 2 s and the retry succeeds
4 Logs, by trace ID Logs from the failed span show version 4.2.0 opening a new gateway connection for every charge, and connection setup timing out
5 Metric, by resource attribute Payments CPU utilization stays low while its open gateway connections climb, so the fix is in how it connects, not in how fast its code runs

No single signal would have got there. The metric knew when and where but not why, the trace knew which call but not the cause, and the logs had the cause but no way to be found among millions of other lines without the trace ID.

Instrumentation

Telemetry exists only where something produces it. Instrumentation is the code, written or generated, that records spans, metrics, and logs as a service runs.

Automatic and Manual Instrumentation

Automatic instrumentation, also called zero-code instrumentation, hooks into frameworks and libraries a service already uses, such as its HTTP server and client, database driver, and message queue client. For the libraries it supports, it produces spans and metrics for every inbound request and outbound call, and propagates context between them, without changes to application code. It is the fastest way to get the waterfall above.

It can’t know the business meaning of the work, though. Manual instrumentation adds what only the code knows, such as a span around the fraud check, a counter of payments declined by reason, an attribute recording the payment method. Most services use automatic instrumentation for the plumbing and add manual instrumentation for the operations that matter to the business.

OpenTelemetry Separates Instrumenting From Where Data Goes

Observability vendors have long shipped their own agents and SDKs, so changing vendors meant re-instrumenting every service. OpenTracing and OpenCensus were earlier vendor-neutral attempts, and they merged in 2019 to form OpenTelemetry, which splits the job into parts:

  • The API is what instrumentation code calls to create spans, record metrics, and emit logs. Libraries can depend on it safely, because with no SDK configured its calls do nothing.
  • The SDK is configured once per service. It decides sampling, batching, and which exporters (the plugins that send telemetry out) deliver it where.
  • OTLP is the wire protocol for sending all signals to a backend or to the OpenTelemetry Collector, an optional process that receives, filters, and forwards telemetry.
  • Semantic conventions fix the attribute names, such as http.request.method and db.system.name, so that every backend recognizes an HTTP span or a database call regardless of which library produced it.

Switching backends then means changing exporter configuration, not code.

Keep Secrets and Personal Data Out

Telemetry is copied to other systems, kept for weeks or months, and read by more people than the production database is. Passwords, tokens, full card numbers, and personal data don’t belong in log messages, span attributes, or metric labels. Mask or drop them where the telemetry is produced, because removing them after they reach a backend usually means finding every copy.

Key Takeaways

  • Observability is a property, monitoring is a practice. Monitoring alerts on problems you predicted. Observability is whether your telemetry can explain the ones you didn’t.
  • Each signal keeps something different from each event. Logs keep detail, metrics keep aggregates, traces keep causality and timing, and profiles keep which code used the resources.
  • Structure logs from the start. Named fields make logs queryable, and a trace ID in each record makes them findable.
  • Keep metric labels bounded. Every distinct label combination is a separate series, and an unbounded label multiplies cost. Put customer and order IDs on spans and logs instead.
  • Record latency as a histogram. Percentiles computed from merged histograms are correct across a fleet, and averages hide the slow requests.
  • Traces depend on propagation. One hop that drops the traceparent context splits the trace in two.
  • The signals are most useful joined. Trace IDs link logs to spans, exemplars link metric spikes to traces, and shared resource attributes let you slice everything the same way.
  • Instrument once with OpenTelemetry. Automatic instrumentation covers the plumbing, manual instrumentation adds business meaning, and the backend becomes a configuration choice.

Found this guide helpful? Share it with your team:

Share on LinkedIn