Skip to main content
In this lesson you’ll generate and inspect the first OpenTelemetry span in a simple Python application. We’ll cover:
  • Explicit span lifecycle with start_span() / end()
  • The recommended Python context manager start_as_current_span
  • Nesting spans to form parent/child relationships
  • Moving span creation inside functions
  • Using the context manager as a decorator for concise function-level instrumentation
Prerequisite: a configure_tracer() helper that returns a configured tracer (for example via trace.get_tracer(...) after installing a TracerProvider and a span exporter). See the OpenTelemetry Python docs for setup details: https://opentelemetry.io/docs/instrumentation/python/

1) Creating a span with start_span / span.end()

You can explicitly start and end a span. This is straightforward but becomes noisy as code grows. Example (payment.py):
Console output:
Example of a simple span export:
Key concepts:
  • trace_id ties all spans in a trace (for example, one user request).
  • span_id uniquely identifies the current span.
  • parent_id is null for root spans.
  • kind for in-process spans is SpanKind.INTERNAL. Other kinds include CLIENT and SERVER.

Starting and ending spans manually gets error-prone. The recommended pattern in Python is tracer.start_as_current_span("name") used as a context manager. It automatically sets the active span and ends it when the block exits (including on exceptions). Basic example:
All code inside the with block is part of the "Starting Payment" span. Use context managers to ensure deterministic start/stop semantics and less boilerplate.

3) Nesting spans to create parent/child relationships

Nest context-managed spans to model the call hierarchy. The outer span is the parent of the inner span and they share the same trace_id. Example:
Result:
  • Both spans have the same trace_id.
  • The "Starting Payment" span’s parent_id is the "Payment Service" span’s span_id.
  • Trace backends can visualize these parent/child relationships to show execution flow.

4) Moving span creation inside functions

Place spans inside the functions they represent so instrumentation is local to the code being traced. This ensures any call to the function automatically creates the span. Example:
And call from a top-level parent span:
Keeping spans close to the implementation reduces coupling and makes instrumentation more maintainable.

5) Adding spans for smaller operations

Instrument smaller operations as child spans so traces reveal finer-grained behavior:
Complete module structure (context managers inside functions):
When executed, you should see multiple spans exported with correct parent/child parent_id relationships, providing clear observability into the operation.
Use context managers for deterministic start/stop semantics and for clearer scope handling. They automatically end spans even if exceptions occur.

6) Using decorators to start a span for a function

You can reuse the context manager as a decorator to start a span every time a function is invoked. Important: the tracer instance used by the decorator must be available at function-definition time.
If using @tracer.start_as_current_span("...") as a decorator, initialize the tracer (for example with tracer = configure_tracer()) before defining the decorated functions. Otherwise the decorator won’t have access to the tracer and will raise an error.
Example (initialize tracer first):
Notes:
  • The decorator starts and ends the span on each function invocation.
  • Decorators are concise and readable for simple functions.
  • For dynamic span names or attributes, prefer explicit context managers inside the function body.

Quick comparison

MethodWhen to useExample
tracer.start_span() + span.end()Simple explicit control, rarely recommended for complex codespan = tracer.start_span("name"); ...; span.end()
with tracer.start_as_current_span("name"):Recommended Python pattern for deterministic end and clear scopewith tracer.start_as_current_span("name"): do_work()
@tracer.start_as_current_span("name") (decorator)Concise instrumentation for small, self-contained functions (tracer must be initialized before decoration)@tracer.start_as_current_span("name")\ndef f(): ...

Summary

  • Manual spans (start_span / end) work but are verbose.
  • Use tracer.start_as_current_span("name") as a context manager for safer, idiomatic Python instrumentation.
  • Nest context managers to create parent/child spans and show the call hierarchy.
  • Move span creation into functions to ensure instrumentation is applied consistently.
  • Use the context manager as a decorator for short, self-contained functions — but initialize the tracer before defining decorated functions.
Further reading and references:

Watch Video