Skip to main content
There are three ways to emit spans from your application: For practical setup, see Auto-instrumentation, Manual instrumentation, and Combining auto and manual. This page covers what each approach is doing under the hood.

Auto-instrumentation

Auto-instrumentation automatically collects telemetry from supported libraries — no per-call code changes needed. Recommended whenever an OpenInference auto-instrumentor exists for your stack.

Initializing an Auto-instrumentor

After this runs, every OpenAI call your application makes is traced — model name, input messages, output messages, token counts, finish reason, all set as attributes on an LLM span. For the full list of supported frameworks and providers, see Integrations.

How Auto-instrumentors Work

Auto-instrumentors use a technique called monkey patching to wrap library functions with tracing logic. The mechanics: When you call OpenAIInstrumentor().instrument():
  1. Identify — the instrumentor targets specific functions in the library (e.g., openai.chat.completions.create).
  2. Wrap — the original function is replaced with a wrapper that:
    1. Creates a span before the original function runs.
    2. Calls the original function.
    3. Sets span attributes from the function’s arguments and response (e.g., llm.input_messages, llm.output_messages, llm.token_count.*).
    4. Ends the span.
The wrapping happens at import time (or at instrument() call time, depending on the library). After that, every call to the wrapped function flows through the tracing logic transparently. For the source of every OpenInference instrumentor, see the OpenInference repository.

Manual Instrumentation

Manual instrumentation means creating each span explicitly using the OTel tracer and setting attributes following the OpenInference semantic conventions. The most common pattern uses start_as_current_span so the new span automatically becomes a child of the most recent active span in the OTel context:
start_as_current_span does two important things:
  1. It automatically sets the span as the active span in the OTel context, so any spans created inside the block become children.
  2. It automatically calls span.end() when the context-manager block exits, so you don’t have to manage span lifetime by hand.
When spans are created without explicitly setting their context, they inherit the most recent active span as their parent_id — which is exactly what you want for nested operations.

Pros and Cons

Common Pitfalls

A few failure modes worth knowing about:
  • Forgetting to end a span — spans are only handed off to the span processor when end() is called. A never-ended span never exports. Use start_as_current_span (which auto-ends) to avoid this entirely.
  • Not setting span status — set Ok or Error explicitly so the Arize AX UI can flag failures.
  • Missing important semantic conventions:
    • openinference.span.kind — without this, the Arize AX UI can’t pick the right icon.
    • input.value and output.value — without these, the trace list view shows blanks.
    • llm.input_messages and llm.output_messages on LLM spans — the chat history doesn’t render in the Arize AX UI without them.
    • tool.name and tool.parameters on tool spans — tool detail panels stay empty.
For the practical how-to, see Manual Instrumentation.

Hybrid Instrumentation

Auto-instrumentors are an easy way to begin tracing, but they only know about what the underlying library exposes. The most interesting parts of an AI application — tool execution in user code, custom business logic, agent loops, evaluation steps — often need extra spans or extra attributes the instrumentor can’t see. Hybrid instrumentation combines auto-instrumentation with targeted manual work. Three primary ways to extend an auto-instrumented trace:

Custom Span Processor

Custom span processors are the most powerful hybrid pattern. You write a class that implements on_start and on_end (and optionally the upcoming on_ending) and attach it to the Tracer Provider. What you can do at each hook: The classic use case is PII redaction: walk every outgoing span and scrub sensitive values from the attributes. For practical examples, see Mask and redact data.
The ReadableSpan passed to on_end is intended to be immutable. In Python you can edit it anyway, but doing so affects every downstream processor and exporter attached to the same Tracer Provider. If you need to modify a span, prefer copying the ReadableSpan with edited attributes rather than mutating it in place.
For the full lifecycle details and how on_end immutability varies between Python and TypeScript, see the Span Processor page.

Choosing an Approach

A practical decision tree:
  1. Is there an OpenInference auto-instrumentor for your stack? Use it. Start there.
  2. Are there gaps it doesn’t cover (tool execution, business logic, custom retrieval)? Add manual spans for those operations.
  3. Do you need to attach the same context to many spans (session ID, user ID)? Use the context managers.
  4. Do you need centralized policy across every span (PII redaction, attribute enrichment, conditional filtering)? Write a custom span processor.

Next step

The easiest way to add context to a whole block of spans is with the OpenInference context managers:

Next: OpenInference Context Managers