Fundamentalsinstrumentationtelemetrycollectorexporteroverhead

Instrumentation: From Code to Signal

Telemetry does not appear; it is emitted by code, batched by a client, shipped to a collector and stored by a backend — and every hop can drop data, add latency or cost money. Knowing the path is what lets you trust the dashboard, and notice when it lies.

Follow the diagnosis

Frame the diagnosis

Performance work starts from a symptom and a signal — never from a resource dashboard.

Diagnostic question
Where does this number come from, what does it cost to produce, and what happens to it under load?
Symptom
A dashboard that went flat during the incident — and nobody can say whether traffic stopped, or the telemetry pipeline did.
Signal
Pipeline health itself: exporter queue depth, dropped spans, collector rejections, ingestion lag. The misleading signal is a smooth chart, which looks identical whether the data is complete or silently sampled down to nothing.
SymptomSignalMeasurementHypothesisEvidenceRoot CauseChangeValidationRegression Check

The path a number takes

A metric on a screen has travelled a long way. Application code calls an instrumentation API. The SDK aggregates in process — incrementing a counter, adding to a histogram bucket, buffering a span. A background exporter batches and ships on an interval. A collector receives, optionally processes (sampling, filtering, redacting, enriching) and forwards. A backend ingests, indexes and stores. A query engine reads it back when someone opens a dashboard.

Every hop is a place data can be lost, and most losses are silent by design — telemetry that blocks the application is worse than telemetry that drops. When the exporter queue fills, spans are dropped. When the collector is overloaded, batches are rejected. When ingestion is rate-limited, data arrives late or not at all. None of that produces an error in your application; it produces a chart that looks calm.

This is why pipeline health signals belong on the dashboard next to the service signals. "Spans dropped per second" and "collector queue depth" are the difference between "the incident resolved at 14:20" and "the telemetry stopped at 14:20". During a real outage those are the same picture, and they mean opposite things.

Every hop can drop, delay or cost
CPU + allocationsmemorydrops when fullegress $ + rejectionsingestion lagApplication codeSDK: aggregate in processBuffer / exporter queueCollector: sample, redact, enrichBackend: ingest + indexQuery / dashboard
UserLLMAgentToolDataDecisionHumanGuardrail

Automatic and manual instrumentation do different jobs

Automatic instrumentation hooks known libraries — the HTTP server, the database driver, the HTTP client, the queue consumer — and produces spans and metrics without application code changes. It is the fastest route to a useful trace and it covers exactly the boundaries that matter most: the ones where a process talks to something else.

What it cannot know is your domain. It will tell you POST /checkout took 1.8s and that a payments HTTP call took 1.6s of it. It will not tell you the request was for a high-value cart, in the EU, on the new pricing path, retried twice. Those attributes come from manual instrumentation, and they are what makes a trace answer questions later (see Observability Is Not a Dashboard).

The practical pattern is: automatic for coverage, manual for meaning. Turn on auto-instrumentation for framework and client boundaries, then add a small number of high-value attributes and a few spans around domain operations that have no library boundary — a pricing calculation, a rules evaluation, a batch of work inside one handler. Adding a span per function is a common overreaction that produces expensive noise.

Choosing between them
AutomaticManual
CoversFramework and library boundaries: HTTP in/out, DB, cache, queueDomain operations and business context
EffortConfiguration; often zero code changeCode in every place you want detail
Answers"Which component consumed the time""Which *kind* of request, for whom, on which path"
Fails atAnything without a known library boundary; in-process workCoverage — humans forget the boundaries they did not think about
Cost riskSpan explosion on chatty libraries (ORMs, retry wrappers)High-cardinality attributes and sensitive data leaking into telemetry
Use forBaseline coverage on every serviceThe handful of attributes that make traces answerable

What instrumentation costs

Instrumentation is not free and pretending otherwise leads to the second-order incident where telemetry causes the outage. There are four distinct costs: in-process CPU and allocation to record data, memory to buffer it, network egress to ship it, and storage plus query cost at the backend. They scale differently — CPU with call frequency, storage with cardinality and retention — so the same change can be cheap in one dimension and ruinous in another.

The classic self-inflicted failure is a high-cardinality metric label. Adding user_id to a counter with 50,000 users does not add one series; it multiplies existing series by 50,000. Metrics backends fall over from this regularly, and the failure often takes down monitoring for *other* teams sharing the backend (see Cardinality: The Label That Took Down Monitoring). The corresponding trace failure is logging full request bodies as span attributes: cheap in CPU, brutal in storage, and a data-protection problem on top.

Sampling is the main lever, and it belongs at different places for different signals. Head sampling decides at the start of a request and is cheap but blind — it drops slow requests as readily as fast ones. Tail-based sampling decides after the trace completes, keeping errors and slow traces, at the cost of buffering every trace in the collector (see Sampling Without Throwing Away the Evidence). For logs, level and rate limits are the equivalent lever (see The Log Bill and What It Is Buying).

Instrumentation with the costs made explicit
1// Auto-instrumentation gives the boundaries for free.
2// Manual instrumentation adds the few attributes that make traces answerable.
3
4const span = tracer.startSpan('pricing.evaluate')
5span.setAttributes({
6 // Low cardinality, bounded set — safe as a metric label too.
7 'pricing.path': isNewEngine ? 'v2' : 'legacy',
8 'cart.tier': tierOf(cart), // 'small' | 'medium' | 'large' — bucketed, not raw
9 // High cardinality: fine on a span (sampled), NEVER on a metric label.
10 'tenant.id': ctx.tenantId,
11 // Bounded numeric context beats dumping the whole object.
12 'cart.item_count': cart.items.length,
13})
14// NOT: span.setAttribute('cart', JSON.stringify(cart))
15// -> unbounded size, may contain personal data, expensive to store and index
16
17try {
18 return evaluate(cart)
19} finally {
20 span.end()
21}
22
23// The metric next to it stays deliberately low-cardinality:
24pricingDuration.record(ms, { path: isNewEngine ? 'v2' : 'legacy' })
25// 2 label values -> 2 series. Adding tenant.id here would mean
26// 2 x (number of tenants) series, forever.

Key points

  • Telemetry travels code → SDK → buffer → collector → backend → query, and every hop can silently drop data under load.
  • A flat chart during an incident may mean the traffic stopped or the pipeline did — pipeline health signals are what distinguish them.
  • Automatic instrumentation buys coverage at library boundaries; manual instrumentation buys domain meaning. Use both, sparingly.
  • Costs scale differently: CPU with call frequency, storage with cardinality and retention. A change can be cheap in one and ruinous in the other.
  • High-cardinality context belongs on sampled spans, never on metric labels.

Follow the diagnosis

The causal chain, hop by hop — and the readings that invite the wrong conclusion.

  1. 1
    Load → SDK: request volume rises, so span and metric emission rate rises proportionally.
  2. 2
    SDK → exporter queue: the queue fills faster than the exporter drains it over the network.
  3. 3
    Queue → drops: the SDK discards data rather than blocking the application — correct behaviour, invisible by default.
  4. 4
    Drops → dashboard: charts thin out or flatten exactly when load is highest, which is exactly when the data was needed.
  5. 5
    Team → conclusion: the incident is misread as "traffic dropped" or "it resolved itself", and the real behaviour at peak is never seen.
What this evidence makes people conclude — wrongly
  • "The chart went flat, so the problem stopped." It is equally consistent with the pipeline dropping data under the load that caused the problem.
  • "Instrumentation overhead is negligible." Usually true for boundaries, sometimes false for hot paths — a span per iteration of a tight loop is measurable, and full-body attributes are expensive.
  • "We should trace everything at 100%." Affordable at low volume; at scale it changes the cost structure of the service and can add meaningful latency to the request path.
  • "The metric is missing, so the code path did not execute." It may not have been exported, may have been dropped, or may have been filtered at the collector.

Measure, fix, validate

An optimization is not finished until the metric that motivated it has moved.

How to measure it
  • • Chart pipeline health next to service health: exporter queue depth, dropped spans/metrics, collector rejections, ingestion lag.
  • • Measure instrumentation overhead directly: compare request latency and CPU with instrumentation enabled and disabled under the same load.
  • • Track series count and its growth rate per metric name — a step change means someone added a label.
  • • Track telemetry cost per signal per month against a budget, so growth is noticed before the invoice.
What actually fixes it
  • • Add pipeline health signals to the same dashboard as service health, and alert on dropped telemetry.
  • • Enable auto-instrumentation for framework and client boundaries first; add manual spans only where a domain operation has no library boundary.
  • • Keep metric labels to bounded sets; push tenant, user and request identifiers onto span attributes and log fields instead.
  • • Choose sampling deliberately per signal, and prefer tail-based sampling if slow and errored traces are what you need most.
  • • Redact at the collector as a safety net, so a mistake in one service does not become a data incident (see [[logs-and-secrets]]).
How you know it worked
  • • Under a load test at peak traffic, confirm dropped-span and dropped-metric counters stay at zero.
  • • Compare request latency with instrumentation on and off at the same load to quantify overhead rather than assuming it.
  • • Verify a deliberately emitted test span with a known attribute arrives end to end within the expected ingestion lag.
What it costs
  • • Richer instrumentation costs CPU, memory, egress and storage continuously, and the cost scales with traffic rather than with value.
  • • Sampling reduces cost and removes certainty: the request you want may not have been kept.
  • • Collector-side processing (redaction, tail sampling) adds a stateful component with its own capacity and failure modes.
Stop it coming back
  • Alert on telemetry drop rate and collector queue depth, treating the pipeline as a production dependency.
  • Add a series-count budget per service with an alert on sudden growth, catching cardinality mistakes before the backend does.
  • Review new metric labels in code review specifically for cardinality — it is the cheapest place to catch it.

Accuracy

Performance numbers are conditional. These are the conditions.

What these numbers depend on
  • ILLUSTRATIVEThe pipeline stages are a general shape. Specific SDKs differ in where aggregation happens and what they drop first under pressure.
  • ENVIRONMENT-SPECIFICOverhead and cost depend on SDK implementation, sampling rate, batch size, network egress pricing and backend storage model. Measure in your own environment rather than trusting a general figure.

Misconceptions

Claim
“Auto-instrumentation is enough.”
Reality
It covers boundaries, which is most of the value, and it cannot know your domain. The attributes that let you answer "was it only the new pricing path, in the EU?" have to be added deliberately.
Claim
“Adding a label to a metric is a small change.”
Reality
It is multiplicative. A label with 50,000 values multiplies every existing series in that metric by 50,000 — this is a standard way to take down a metrics backend for everyone sharing it.
Claim
“If the telemetry is missing, the code did not run.”
Reality
Missing telemetry means missing telemetry. Sampling, buffer drops, collector rejections and ingestion lag all produce absence that looks like non-execution.