skip to content

You are adding hand-written OpenTelemetry spans to a service. What must be true at span creation, at activation and at end for the span to be sampled, parented and exported correctly — and what makes a hand-written span useless even when it is technically valid?

level: middleimportance: must knowfreq 52%

answer

  1. sampler sees creation-time inputs only
  2. started ≠ current → orphaned children
  3. no end → no export + retained memory
  4. updates after end are ignored
  5. attribute on the current span beats a new span

basics

~20 s

At creation the sampler sees only parent context, trace id, name, kind, links and the attributes passed there. Activation is separate from starting — an unactivated span orphans nested work. End on every path or nothing exports. Useless spans duplicate auto-instrumentation or carry no decision-changing detail.

solid answer

~50 s

Three moments. **Creation** freezes the head sampler's inputs: parent context, trace id, name, kind, links, and only the attributes supplied to the builder. It also fixes the parent — most SDKs default to whatever span is currently active, so with nothing active you silently start a new root trace instead of a child. **Activation** is a separate act from starting. A started-but-never-activated span still exports, but nested instrumentation parents to whatever was current, so the manual span shows up childless while the real work hangs off the enclosing request span. **End** is what hands the span to the processor and exporter: no end means no export plus a retained object; updates after end are ignored; scoped constructs beat hand-rolled try/finally. A span is *useless* when nothing about it could change a decision — it wraps a call the agent already instruments, has no attributes, or is one span per loop iteration where a `batch.size` attribute would do.

code

text · 14 lines
text
request span  [==================================]  (currently active)
  span = start("checkout.pricing")   # started, NOT activated
  callPricingService()               # auto-instrumented CLIENT span
  span.end()

resulting tree:
  request
  |- checkout.pricing        <- empty, no children
  |- HTTP GET /pricing       <- attached to request, not to checkout.pricing

with activation:
  request
  |- checkout.pricing
     |- HTTP GET /pricing

go deeper

for a junior

Know that a manual span must be ended on every path, that it has to be made current for nested work to nest under it, and that spans that never end never show up.

for a middle

Explain the creation-time sampler input set versus later attributes, ambient-context parenting and how a root span appears by accident, and the end-triggers-export chain.

for a senior

Diagnose orphaned, empty and unended spans from the trace shape; handle context across async hand-offs; judge granularity against export volume and what auto-instrumentation already gives you.

for a principal

Set the standard: where hand-written spans are worth their volume across services, a scoped-helper convention so ending cannot be skipped, and guidance to prefer attributes on existing spans over new ones.

## What a manual span is competing with Auto-instrumentation — a zero-code agent, or the maintained instrumentation libraries — already produces spans at process boundaries: inbound requests, outbound HTTP/RPC calls, database client calls, message publish and consume. Those spans arrive with conventional attributes populated and with context injected and extracted for you. A hand-written span earns its keep only when it names something those boundary spans cannot show: an in-process phase between two boundaries, work over a transport nobody instrumented, or a business-meaningful unit of work. Its kind must of course be set to the right value for its role at the boundary, but span shape is a separate subject; what follows is the three moments in a manual span's own life where instrumentation goes wrong. ## Creation: the sampler's input set is frozen here The specification's sampling hook receives a fixed argument list: the parent context, the trace id, the span name, the span kind, the initial attributes, and the links. Nothing else. It cannot see attributes you add later, cannot see the duration, cannot see the status — those do not exist yet. The practical rule is blunt: any value that should drive a keep/drop decision must be an argument to the span builder, not a later attribute set. (Which sampler runs and how it is configured is a separate concern; with the common parent-based default the local inputs only decide anything at root spans or under a custom sampler.) Creation also fixes the parent. In SDKs with ambient context storage the default parent is "whatever span is currently active", so if nothing is active you get a root span and therefore a brand-new trace — a silent failure that looks like a missing trace rather than an error. In SDKs where context is passed explicitly the same mistake is visible in the call signature. Creation is also where an explicit start timestamp goes, if you are recording work that already elapsed. One consequence worth knowing: when the sampler drops a span, the API still returns a valid, non-recording span whose span context carries the trace and span ids with the sampled flag cleared. Propagation therefore keeps working downstream; only recording and export stop. A record-only outcome sits in between — collected in process, never exported. ## Activation: starting is not entering Every SDK separates "create and start a span" from "make this span the current one", though the spelling differs: a start-and-activate convenience wrapper versus a plain start in most languages, and in explicit-context languages a start call that hands you back a new context you must thread through. If you only start, the span is real and will export, but nothing nests under it — the library calls inside your block still see the previously active span as their parent. The trace then shows an empty manual span beside the work it was supposed to contain. The mirror-image bug is a leaked activation: the scope is never closed, or is not restored on the exception path, so later unrelated work on the same thread parents to a span that has already ended. And ambient context does not cross a thread-pool submit, a callback or a reactive operator by itself; the context must be captured at hand-off and restored in the worker, usually via the SDK's wrapping helpers. ## End: the only thing that exports Ending a span is what invokes the span processor's end hook, which queues it for batching and export. An unended span therefore never reaches a backend and stays resident in memory with all its attributes and events — worse than a missing span, because it also looks like a truncated trace. Per the spec, updates after end are ignored and a second end call is ignored, so idempotence is on your side but forgetfulness is not. End on every path: success, thrown exception, early return, cancellation, timeout. Use the language's scoped construct so the compiler or runtime enforces it. At process level, a batching processor holds a queue; a short-lived job that exits without shutting the provider down loses whatever was still queued. ## Valid but useless The test is: would a difference in this span's duration or attributes change what someone does next? Common failures — a manual span wrapping a call the agent already instruments, producing two nested spans with identical duration; a span with no attributes, so every instance is indistinguishable; a span whose whole duration is its single child's duration; one span per loop iteration or per retry attempt, multiplying volume to carry information a single `batch.size` or retry-count attribute would have carried. Very often the right move is not a span at all but an attribute or event on the span that is already current: zero extra export volume, and it lands where queries already look. ## Granularity economics Cost is per exported span — ingest, storage and query all scale with it. Instrument the loop, not the iteration; the retry sequence, not the attempt, unless attempts genuinely differ. How deeply to instrument a service by hand should track how much of its latency and failure surface is invisible from the boundaries the agent already covers.

  • You add an attribute just before ending a span and it never influences which traces are kept. Why?
    The head sampling decision is taken when the span starts, and the hook is handed only the parent context, trace id, name, kind, links and the attributes supplied at creation. Anything set afterwards can be displayed and queried but arrives long after the decision. If a value must drive retention, either pass it at creation or move the decision to a collector-side pipeline that sees whole finished traces.
  • The sampler drops a span. Does downstream work still get a usable trace context?
    Yes. A dropped span is returned as a valid but non-recording span: it still carries a real trace id and span id with the sampled flag cleared, so injection into outbound headers still works and the trace stays coherent. What stops is recording and export. With the common parent-based default, children inherit the cleared flag and are dropped too, so the drop is consistent along the branch rather than leaving half-traces.
  • A short-lived batch job runs, its code clearly creates and ends spans, yet nothing arrives in the backend. What would you check first?
    Whether the process exits before the batching processor flushes. Ended spans are queued, not sent synchronously, so an exit without shutting the tracer provider down discards the queue. Check that shutdown or an explicit flush runs on every exit path, including error exits, and only then look at exporter endpoint and connectivity.

Starting a span is opening a folder; activating it is putting it on top of the desk so everything you file next lands inside; ending it is handing it to the courier. Skip step two and the papers pile up outside; skip step three and nothing ever ships.

saying these in an interview costs you the question

  • Assuming attributes added later can still influence the sampling decision
  • Believing that starting a span is enough for nested calls to become its children
  • Ending spans only on the success path, or trusting garbage collection to do it
  • Wrapping a call that auto-instrumentation already covers in a manual span "to be safe"
  • Creating one span per loop iteration or per retry attempt by default instead of an attribute

context