杰瑞科技汇

OpenTracing Python 如何快速上手?

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

OpenTracing Python 如何快速上手?-图1
(图片来源网络,侵删)

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:

OpenTracing Python 如何快速上手?-图2
(图片来源网络,侵删)
  1. 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 Tracer instance for your application.
  2. 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).
  3. 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.
  4. Context Propagation: This is the magic of distributed tracing. It's the mechanism of passing the SpanContext from one service to another. This is typically done via HTTP headers (e.g., traceparent in OpenTelemetry, or uber-trace-id in 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:

  1. Save the code as simple_trace.py.
  2. Run it: python simple_trace.py.
  3. Open your browser to http://localhost:16686.
  4. Select my-python-service from the service dropdown and click "Find Traces".
  5. 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:

  1. Make sure your Jaeger container is running.
  2. Open two terminal windows.
  3. In the first terminal, run the DB service:
    python db_service.py
  4. In the second terminal, run the API service:
    python api_service.py
  5. Go to http://localhost:16686. You should now see traces from both api_service and db_service.
  6. Click on a trace. You will see a beautiful visualization showing:
    • The process_user_request span in api_service.
    • The call_db_service span as a child of the root.
    • The get_user_from_db span in db_service as a child of call_db_service, proving the context was successfully propagated.

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:

  • TracerProvider is the main configuration point.
  • set_attribute() is used instead of set_tag().
  • add_event() is used instead of log_event().
  • The instrumentation for libraries like requests or flask is much more straightforward in OpenTelemetry, often requiring just a few lines of configuration.
分享:
扫描分享到社交APP
上一篇
下一篇