This Quick Start shows you how to use OpenTelemetry in your Python app to:

  • Configure a tracer
  • Generate trace data
  • Propagate context in the application’s context
  • Propagate context over HTTP
  • Export the trace data to the console and to LightStep
  • Use LightStep to view the trace data

This Quick Start uses a simple Python app and then a client and server component to show how to propagate context over HTTP.

The full code for the examples in this guide can be found here.

Requirements

Python 3.4 or newer

Setup

To use OpenTelemetry, you need to install the API and SDK packages for Python. The version of the sdk and api used in this guide is 0.3a0, the most current version as of writing.

Install using pip:
pip install opentelemetry-api opentelemetry-sdk

Collect Trace Data

You need to configure a Tracer to collect tracing information. A tracer is an object that tracks the currently active span and allows you to create (or activate) new spans. As spans are created and completed, the tracer dispatches them to an exporter that can send the spans to a backend system for analysis.

In this first example, you configure the tracer to use the ConsoleSpanExporter, which prints tracing information to the console.

  1. Import OpenTelemetry and create a tracer configured to send data to the console.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    #!/usr/bin/env python3
    # first_trace.py
    from opentelemetry import trace
    from opentelemetry.sdk.trace import Tracer
    from opentelemetry.sdk.trace.export import ConsoleSpanExporter
    from opentelemetry.sdk.trace.export import BatchExportSpanProcessor
    
    trace.set_preferred_tracer_implementation(lambda T: Tracer())
    tracer = trace.tracer()
    span_processor = BatchExportSpanProcessor(ConsoleSpanExporter())
    tracer.add_span_processor(span_processor)
    
  2. Now create a Span object. A span is the building block of a trace and is a named, timed operation that represents a piece of the workflow in the distributed system. Multiple spans are pieced together to create a trace.

    The only required parameter is the span’s name. But often, more information about the span is needed so that you can effectively debug and monitor in your backend system. Attributes allow you to add name/value pairs to describe the span. Events represent an event that occurred at a specific time within a span’s workload.

    Along with the name, add attributes for the platform and version and an event.

    1
    2
    3
    4
    
    span = tracer.start_span('foo')
    span.set_attribute("platform", "osx")
    span.set_attribute("version", "1.2.3")
    span.add_event("event in foo", {"name": "foo1"})
    
  3. Add a child span to the existing span. To create the relationship between the new span and the previous one, set the parent argument at creation time. Note that in this example, you also set attributes by passing a Dict as an attributes argument to start_span.
    1
    2
    3
    4
    5
    
      attributes = {
       "platform": "osx",
       "version": "1.2.3",
      }
      child_span = tracer.start_span('baz', parent=span, attributes=attributes)
    
  4. Operations that are represented by spans are not considered complete, until the span is ended. You need to call end on the parent and child span objects in order to signal to the exporter that the tracing information is ready to be sent to its destination. In this example, the exporter will print the information to the console.
    1
    2
    
      child_span.end()
      span.end()
    
  5. Run the component (first_trace.py) and view the data in your console. $ python3 ./first_trace.py

The output on the console should show us something like this:

1
2
  Span(name="baz", context=SpanContext(trace_id=0x77ff34ed5402dec1e2787d2603c6d4fc, span_id=0x19d4a9af9b4c7d54, trace_state={}), kind=SpanKind.INTERNAL, parent=Span(name="foo", context=SpanContext(trace_id=0x77ff34ed5402dec1e2787d2603c6d4fc, span_id=0xd20ad8c01d7777d0, trace_state={})), start_time=2020-01-09T19:55:31.443103Z, end_time=2020-01-09T19:55:31.443117Z)
  Span(name="foo", context=SpanContext(trace_id=0x77ff34ed5402dec1e2787d2603c6d4fc, span_id=0xd20ad8c01d7777d0, trace_state={}), kind=SpanKind.INTERNAL, parent=None, start_time=2020-01-09T19:55:31.443036Z, end_time=2020-01-09T19:55:31.443131Z)

Propagating the Context

Creating relationships between spans is what allows the propagation of the context of a trace across different parts of a process or across processes. Another way to create a hierarchical relationship between spans is using use_span, this method sets the span as the current span in the application’s context using either thread-local storage in Python 3.4 to 3.6 or contextvars in Python 3.7+. The following example sets up the tracer and creates two spans, with foo being the parent of baz. They are all part of a single trace.

1
2
3
4
5
span = tracer.start_span('foo', attributes=attributes)
with tracer.use_span(span):
    child_span = tracer.start_span('baz', attributes=attributes)
child_span.end()
span.end()

A more convenient method to use is start_as_current_span, which starts a new span and sets it as the current span. The Context Manager simplifies the code by calling end on the span when the scope is exited, which means you write even less code!

1
2
3
with tracer.start_as_current_span('foo', attributes=attributes):
    with tracer.start_as_current_span('bar', attributes=attributes):
        print("hello")

HTTP propagation

You can instrument a request from a client to a server which allows you to propagate the context across process boundaries. This ensures that you can correlate events that happen in separate processes into a single trace. You use opentelemetry-ext-wsgi and opentelemetry-ext-http-requests to do this.

  1. Add the dependencies: pip install flask opentelemetry-ext-wsgi opentelemetry-ext-http-requests

  2. Instrument the server (server.py - a Flask app), using OpenTelemetryMiddleware. The / handler sends a request using the requests module which is instrumented as well, using the OpenTelemetry requests integration.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    
     #!/usr/bin/env python3
     # server.py
     import flask
     import requests
    
     from opentelemetry import trace
     from opentelemetry.ext import http_requests
     from opentelemetry.ext.wsgi import OpenTelemetryMiddleware
     from opentelemetry.sdk.trace import Tracer
     from opentelemetry.sdk.trace.export import ConsoleSpanExporter
     from opentelemetry.sdk.trace.export import BatchExportSpanProcessor
    
    
     exporter = ConsoleSpanExporter()
     trace.set_preferred_tracer_implementation(lambda T: Tracer())
     tracer = trace.tracer()
     span_processor = BatchExportSpanProcessor(exporter)
     tracer.add_span_processor(span_processor)
    
     http_requests.enable(tracer)
     app = flask.Flask(__name__)
     app.wsgi_app = OpenTelemetryMiddleware(app.wsgi_app)
    
     @app.route("/")
     def hello():
       with tracer.start_as_current_span("parent"):
         requests.get("https://www.wikipedia.org/wiki/Rabbit")
       return "hello"
    
     if __name__ == "__main__":
       app.run(debug=True)
       span_processor.shutdown()  
    
  3. Have the client code (client.py) make a request to the server using the Requests module, which is instrumented using the http_request OpenTelemetry integration.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  #!/usr/bin/env python3
  # client.py
  import requests

  from opentelemetry import trace
  from opentelemetry.ext import http_requests
  from opentelemetry.sdk.trace import Tracer
  from opentelemetry.sdk.trace.export import ConsoleSpanExporter
  from opentelemetry.sdk.trace.export import BatchExportSpanProcessor

  exporter = ConsoleSpanExporter()
  trace.set_preferred_tracer_implementation(lambda T: Tracer())
  tracer = trace.tracer()
  span_processor = BatchExportSpanProcessor(exporter)
  tracer.add_span_processor(span_processor)

  http_requests.enable(tracer)
  response = requests.get(url="http://127.0.0.1:5000/")
  span_processor.shutdown()
  1. Launch server.py in a terminal.
    python3 ./server.py

  2. Open a second terminal to run the client.
    python3 client.py

The output from running those scripts looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ python3 ./server.py
 * Serving Flask app "server" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 441-767-875
127.0.0.1 - - [14/Jan/2020 20:39:04] "GET / HTTP/1.1" 200 -
Span(name="/wiki/Rabbit", context=SpanContext(trace_id=0x463b6f1491d5b4f4a0a2c0536f17c3b6, span_id=0xc801b6747366b583, trace_state={}), kind=SpanKind.CLIENT, parent=Span(name="parent", context=SpanContext(trace_id=0x463b6f1491d5b4f4a0a2c0536f17c3b6, span_id=0x177d30710d0785d9, trace_state={})), start_time=2020-01-14T20:39:04.096754Z, end_time=2020-01-14T20:39:04.587821Z)
Span(name="parent", context=SpanContext(trace_id=0x463b6f1491d5b4f4a0a2c0536f17c3b6, span_id=0x177d30710d0785d9, trace_state={}), kind=SpanKind.INTERNAL, parent=Span(name="/", context=SpanContext(trace_id=0x463b6f1491d5b4f4a0a2c0536f17c3b6, span_id=0x49516da6a5653bc4, trace_state={})), start_time=2020-01-14T20:39:04.096580Z, end_time=2020-01-14T20:39:04.590423Z)
Span(name="/", context=SpanContext(trace_id=0x463b6f1491d5b4f4a0a2c0536f17c3b6, span_id=0x49516da6a5653bc4, trace_state={}), kind=SpanKind.SERVER, parent=SpanContext(trace_id=0x463b6f1491d5b4f4a0a2c0536f17c3b6, span_id=0x4853012f413d7dc1, trace_state={}), start_time=2020-01-14T20:39:04.096395Z, end_time=2020-01-14T20:39:04.591348Z)

$ python3 ./client.py
Span(name="/", context=SpanContext(trace_id=0x463b6f1491d5b4f4a0a2c0536f17c3b6, span_id=0x4853012f413d7dc1, trace_state={}), kind=SpanKind.CLIENT, parent=None, start_time=2020-01-14T20:39:04.092865Z, end_time=2020-01-14T20:39:04.592588Z)

Looking at the output, you can see that the same trace_id=0x463b6f1491d5b4f4a0a2c0536f17c3b6 is propagated across the spans from the client and the server.

Export and Explore Trace Data in LightStep

To make use of the trace data being generated, exporters send the data to various supported collectors. The ConsoleSpanExporter is really useful when debugging while instrumenting. But now, you’ll use the experiemental LightStepSpanExportor to send tracing data to LightStep’s public Satellites.

If you don’t already have a LightStep account, you can create a free account here.

  1. Install the exporter library. pip install git+https://github.com/lightstep/opentelemetry-exporter-python

  2. Update the exporter configuration in client.py and server.py to use the LightStepSpanExporter. You’ll need to provide a name for your service and a LightStep access token.

1
2
3
4
5
6
  from opentelemetry.ext.lightstep import LightStepSpanExporter

  exporter = LightStepSpanExporter(
    name="exporter-demo-server",
    token="<YOUR ACCESS TOKEN HERE>",
  )
  1. Restart server.py and run client.py again. Tracing information is now available in your LightStep project!

  2. Click Explorer in the left navigation bar to see the trace data.

  3. Click on any span in the Trace Analysis table to view the full trace.