Of course! Here's a comprehensive guide to using OpenTracing in Python, covering the core concepts, setup, and practical examples.

What is OpenTracing?
First, it's crucial to understand the context. OpenTracing is an API specification, not a concrete implementation. Think of it as a set of standard rules and interfaces for how to create and manage distributed traces.
- API (Specification): Defines how to create Spans, add Tags, set Log events, and manage context propagation (how trace information travels across service boundaries).
- Implementation (Library): A specific library that implements this API. The most famous implementation that grew from OpenTracing is Jaeger.
Important Note: The OpenTracing project has been merged into the OpenTelemetry project. OpenTelemetry is now the industry-standard, CNCF-hosted project for observability (metrics, logs, and traces). While you can still find and use OpenTracing libraries, new projects should strongly consider using OpenTelemetry. However, understanding OpenTracing is still very valuable as the concepts are the same.
This guide will focus on the opentracing-api and the jaeger-client, which is a classic way to get started with tracing in Python.
Core Concepts in OpenTracing
Before we code, let's understand the key terms:

- Tracer: The main entry point to the tracing system. You use it to start new traces or extract existing ones from incoming requests. You get a single
Tracerinstance for your application. - Span: A fundamental unit of work. A span represents a single operation in a distributed system. It has:
- A unique name (e.g.,
process_payment,get_user_from_db). - A start time and an end time.
- A set of key-value pairs called Tags (e.g.,
error: true,http.status_code: 404). Tags are usually set once and are useful for filtering and searching. - A list of Log events with timestamps (e.g.,
{"event": "query", "payload": "SELECT * FROM users"}). Logs are for discrete events. - A SpanContext, which contains all the information needed to identify a span across process boundaries (like a unique trace ID and span ID).
- A unique name (e.g.,
- Baggage: Key-value data that you attach to the
SpanContext. Unlike tags, baggage is propagated across every service in the trace. It's useful for passing user-specific data or debugging information. - Context Propagation: This is the magic of distributed tracing. It's the mechanism of passing the
SpanContextfrom one service to another. This is typically done via HTTP headers (e.g.,traceparentin OpenTelemetry, oruber-trace-idin Jaeger).
Setup and Installation
First, you need to install the necessary Python packages. We'll install the OpenTracing API and the Jaeger client implementation.
pip install opentracing pip install jaeger-client
You will also need a Jaeger backend to send your traces to. The easiest way to run one is using Docker:
docker run -d --name jaeger \ -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \ -p 5775:5775/udp \ -p 6831:6831/udp \ -p 6832:6832/udp \ -p 5778:5778 \ -p 16686:16686 \ -p 14268:14268 \ jaegertracing/all-in-one:latest
Now you can access the Jaeger UI at http://localhost:16686.
Example 1: A Simple, Single-Service Trace
Let's create a basic script that starts a trace, creates a few spans, and logs them to the console.
import opentracing
from jaeger_client import Config
# --- 1. Initialize the Tracer ---
# This configures the Jaeger client to send traces to the local collector.
# In a real app, you would do this once at startup.
config = Config(
config={ # usually read from a file or environment variables
'sampler': {
'type': 'const', # Sample all traces
'param': 1,
},
'logging': True,
'reporter_batch_size': 1, # Send spans one by one for demo purposes
},
service_name='my-python-service',
validate=True,
)
# Initialize the tracer
tracer = config.initialize_tracer()
# --- 2. Create and use Spans ---
# Start a root span. The `with` statement ensures the span is finished.
with tracer.start_span('process_request') as root_span:
print("Root span started")
# Add a tag to the root span
root_span.set_tag('http.method', 'GET')
root_span.set_tag('http.url', '/api/users/123')
# Start a child span. It will be automatically linked to the parent.
with tracer.start_span('fetch_user_from_db', child_of=root_span) as db_span:
print("Child span for DB operation started")
db_span.set_tag('db.type', 'sql')
db_span.set_tag('db.statement', 'SELECT * FROM users WHERE id = 123')
# Simulate some work and log an event
db_span.log_event({'event': 'query_executed', 'size': 1})
# Start another child span
with tracer.start_span('render_template', child_of=root_span) as template_span:
print("Child span for template rendering started")
template_span.set_tag('template.name', 'user_profile.html')
print("Spans finished and reported to Jaeger")
How to Run and View:
- Save the code as
simple_trace.py. - Run it:
python simple_trace.py. - Open your browser to http://localhost:16686.
- Select
my-python-servicefrom the service dropdown and click "Find Traces". - You should see your trace. Click on it to see the visualization of the parent and child spans.
Example 2: Distributed Tracing with Context Propagation
This is where tracing becomes powerful. Let's simulate two services: an api_service and a db_service. The api_service will make an HTTP request to the db_service, and the trace context must be propagated.
First, let's create the db_service. This service will listen for requests, extract the trace context from the headers, and continue the trace.
db_service.py
import opentracing
from jaeger_client import Config
import flask
import time
# --- 1. Initialize the Tracer for the DB Service ---
config = Config(
config={
'sampler': {'type': 'const', 'param': 1},
'logging': True,
'reporter_batch_size': 1,
},
service_name='db_service',
validate=True,
)
tracer = config.initialize_tracer()
# --- 2. Create a simple Flask app ---
app = flask.Flask(__name__)
@app.route('/user/<user_id>')
def get_user(user_id):
# --- 3. Extract the SpanContext from the incoming HTTP request ---
# This looks for tracing headers (e.g.,uber-trace-id) in the request.
# If found, it continues the existing trace. If not, it starts a new one.
span_ctx = tracer.extract(opentracing.Format.HTTP_HEADERS, flask.request.headers)
# Start a new span, linked to the parent span if one was extracted.
# The `child_of` parameter can be a SpanContext object.
with tracer.start_span(
'get_user_from_db',
child_of=span_ctx if span_ctx else None
) as span:
span.set_tag('user.id', user_id)
span.log_event({'event': 'processing_user_request', 'user_id': user_id})
# Simulate DB work
time.sleep(0.1)
return f"User data for {user_id}", 200
if __name__ == '__main__':
app.run(port=5001)
Now, let's create the api_service that calls this db_service.
api_service.py
import opentracing
from jaeger_client import Config
import requests
import time
# --- 1. Initialize the Tracer for the API Service ---
config = Config(
config={
'sampler': {'type': 'const', 'param': 1},
'logging': True,
'reporter_batch_size': 1,
},
service_name='api_service',
validate=True,
)
tracer = config.initialize_tracer()
def call_backend_service(tracer, user_id):
# --- 2. Start a span for the outgoing HTTP request ---
with tracer.start_span('call_db_service') as parent_span:
# --- 3. Inject the SpanContext into the HTTP request headers ---
# This takes the context of our current span and injects it
# into a dictionary of HTTP headers.
headers = {}
tracer.inject(
parent_span.context,
opentracing.Format.HTTP_HEADERS,
headers
)
# Add our custom headers to the request
headers['Content-Type'] = 'application/json'
parent_span.set_tag('backend.url', f'http://localhost:5001/user/{user_id}')
# Make the HTTP call
response = requests.get(
f'http://localhost:5001/user/{user_id}',
headers=headers
)
return response.text
if __name__ == '__main__':
# Start the root span for the entire operation
with tracer.start_span('process_user_request') as root_span:
user_id = '123'
root_span.set_tag('request.user_id', user_id)
# Call the downstream service
result = call_backend_service(tracer, user_id)
root_span.log_event({'event': 'api_call_completed', 'result': result})
print(f"Final result from API service: {result}")
How to Run and View:
- Make sure your Jaeger container is running.
- Open two terminal windows.
- In the first terminal, run the DB service:
python db_service.py
- In the second terminal, run the API service:
python api_service.py
- Go to http://localhost:16686. You should now see traces from both
api_serviceanddb_service. - Click on a trace. You will see a beautiful visualization showing:
- The
process_user_requestspan inapi_service. - The
call_db_servicespan as a child of the root. - The
get_user_from_dbspan indb_serviceas a child ofcall_db_service, proving the context was successfully propagated.
- The
Transitioning to OpenTelemetry
As mentioned, OpenTelemetry is the future. The concepts are identical, but the API is slightly different.
Here's how you would rewrite the simple trace using OpenTelemetry:
# pip install opentelemetry-api opentelemetry-sdk opentelemetry-instrumentation-requests
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import (
ConsoleSpanExporter,
SimpleSpanProcessor,
)
# --- 1. Configure the TracerProvider ---
# This replaces the Jaeger Config object.
trace.set_tracer_provider(TracerProvider())
# Add a processor to export spans to the console
# In a real app, you would use a JaegerExporter instead.
span_processor = SimpleSpanProcessor(ConsoleSpanExporter())
trace.get_tracer_provider().add_span_processor(span_processor)
# --- 2. Get a Tracer ---
tracer = trace.get_tracer(__name__)
# --- 3. Create and use Spans ---
with tracer.start_span("process_request") as root_span:
root_span.set_attribute("http.method", "GET")
root_span.set_attribute("http.url", "/api/users/123")
with tracer.start_span("fetch_user_from_db", attributes={"db.type": "sql"}) as db_span:
# Note: In OpenTelemetry, logs are now called 'Events'
db_span.add_event("query_executed", {"size": 1})
with tracer.start_span("render_template", attributes={"template.name": "user_profile.html"}) as template_span:
pass
The key differences are:
TracerProvideris the main configuration point.set_attribute()is used instead ofset_tag().add_event()is used instead oflog_event().- The instrumentation for libraries like
requestsorflaskis much more straightforward in OpenTelemetry, often requiring just a few lines of configuration.
