Implementation:Apache Airflow BaseTracer Protocol
| Knowledge Sources | |
|---|---|
| Domains | Observability, Tracing |
| Last Updated | 2026-02-08 21:00 GMT |
Overview
Tracer protocol and no-op fallback implementation that defines the tracing interface for type checking and provides EmptyTrace/EmptySpan as safe fallbacks when tracing is not configured.
Description
The base_tracer module establishes the tracing contract for Airflow's distributed tracing subsystem. It contains three key components:
- Tracer (Protocol): Defines the interface that any tracing backend must implement. Methods include start_span, start_root_span, start_child_span, use_span, get_current_span, inject, and extract. This protocol is used for static type checking only and is not instantiated directly.
- EmptyTrace: A concrete no-op implementation of the Tracer interface. All span-creation methods return EMPTY_SPAN, inject returns an empty dict, and extract returns EMPTY_CTX. This serves as the fallback when no tracing backend (e.g., OpenTelemetry) is configured.
- EmptySpan: A no-op span that supports context manager usage (
withblocks), decorator usage (__call__), and attribute/event operations that silently do nothing. Its is_recording() method always returns False. - EmptyContext: A minimal span context with default trace_id=1 and span_id=1.
The module also provides gen_context() and gen_links_from_kv_list() as lazy-import wrappers that delegate to the OTel tracer implementation when available.
Usage
The Tracer protocol is used for type annotations throughout Airflow's tracing code. EmptyTrace is the default tracer when OpenTelemetry is not installed or not configured. Components can safely call tracing methods without checking whether tracing is active.
Code Reference
Source Location
- Repository: Apache Airflow
- File: shared/observability/src/airflow_shared/observability/traces/base_tracer.py (232 lines)
Signature
class EmptyContext:
"""If no Tracer is configured, EmptyContext is used as a fallback."""
trace_id = 1
span_id = 1
class EmptySpan:
"""If no Tracer is configured, EmptySpan is used as a fallback."""
def __enter__(self) -> Self: ...
def __exit__(self, *args, **kwargs): ...
def __call__(self, obj): ...
def get_span_context(self): ...
def set_attribute(self, key, value) -> None: ...
def set_attributes(self, attributes) -> None: ...
def is_recording(self) -> bool: ... # Always returns False
def add_event(self, name: str, attributes=None, timestamp=None) -> None: ...
def add_link(self, context, attributes=None) -> None: ...
def end(self, end_time=None, *args, **kwargs) -> None: ...
class Tracer(Protocol):
"""Protocol defining the tracing interface for type checking."""
instance: Tracer | EmptyTrace | None = None
@classmethod
def get_tracer(cls, component): ...
@classmethod
def start_span(cls, span_name: str, component=None, parent_sc=None,
span_id=None, links=None, start_time=None): ...
@classmethod
def use_span(cls, span): ...
@classmethod
def get_current_span(self): ...
@classmethod
def start_root_span(cls, span_name=None, component=None,
start_time=None, start_as_current=True): ...
@classmethod
def start_child_span(cls, span_name=None, parent_context=None,
component=None, links=None, start_time=None,
start_as_current=True): ...
@classmethod
def inject(cls) -> dict: ...
@classmethod
def extract(cls, carrier) -> EmptyContext: ...
class EmptyTrace:
"""No-op tracer fallback. All methods return EMPTY_SPAN or EMPTY_CTX."""
...
Import
from airflow_shared.observability.traces.base_tracer import Tracer, EmptyTrace, EmptySpan
from airflow_shared.observability.traces.base_tracer import EMPTY_SPAN, EMPTY_CTX
I/O Contract
Tracer Protocol Methods
| Method | Parameters | Return Type | Description |
|---|---|---|---|
| get_tracer | component: str | Tracer | Obtain a tracer for a named component |
| start_span | span_name, component, parent_sc, span_id, links, start_time | Span | Start a new span with optional parent context |
| start_root_span | span_name, component, start_time, start_as_current | Span | Start a root span (no parent) |
| start_child_span | span_name, parent_context, component, links, start_time, start_as_current | Span | Start a child span under an existing parent |
| use_span | span | ContextManager | Set a span as the current active span |
| get_current_span | (none) | Span | Retrieve the currently active span |
| inject | (none) | dict | Inject trace context into a carrier dict for propagation |
| extract | carrier: dict | Context | Extract trace context from a carrier dict |
EmptySpan Interface
| Method | Behavior |
|---|---|
| __enter__ / __exit__ | No-op context manager |
| __call__(obj) | Returns obj unchanged (no-op decorator) |
| set_attribute / set_attributes | Silently ignored |
| add_event / add_link | Silently ignored |
| is_recording() | Always returns False |
| end() | No-op |
| get_span_context() | Returns EMPTY_CTX |
Usage Examples
Safe Tracing with Fallback
from airflow_shared.observability.traces.base_tracer import EmptyTrace
# When tracing is not configured, EmptyTrace is used
tracer = EmptyTrace
with tracer.start_span("process_dag", component="scheduler") as span:
span.set_attribute("dag_id", "example_dag")
# Work proceeds normally; span operations are no-ops
Context Propagation
# Inject context for cross-process propagation
carrier = tracer.inject() # Returns {} when using EmptyTrace
# Extract context from incoming request
context = tracer.extract(carrier) # Returns EmptyContext