Instrument Your Python Applications With Datadog and OpenTelemetry | Datadog

Instrument your Python applications with Datadog and OpenTelemetry

Author Mallory Mooney

Published: July 30, 2020

If you are familiar with OpenTracing and OpenCensus, then you have probably already heard of the OpenTelemetry project. OpenTelemetry merges the OpenTracing and OpenCensus projects to provide a standard collection of APIs, libraries, and other tools to capture distributed request traces and metrics from applications and easily export them to third-party monitoring platforms. At Datadog, we’re proud to be supporters of the project, and we’re building on that to provide out-of-the-box instrumentation for your applications using OpenTelemetry’s suite of tools and our existing tracing libraries.

As part of this ongoing work, we’re excited to announce a new Python exporter for sending traces from your instrumented Python applications to Datadog, with support for exporting metrics coming soon. OpenTelemetry exporters are libraries that transform and send data to one or more destinations. The Datadog exporter enables you to integrate the OpenTelemetry tracing library into your application and seamlessly connect to other applications already instrumented with either OpenTelemetry and Datadog libraries.

In this guide, we’ll show how to instrument an application with OpenTelemetry as well as how to plug in Datadog’s new Python exporter and start collecting data.

Instrumenting a Python application with OpenTelemetry

We’ll first take a look at a basic Python application that is already instrumented with OpenTelemetry.

app.py

 
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import (
    ConsoleSpanExporter,
    SimpleExportSpanProcessor,
)

trace.set_tracer_provider(TracerProvider())
trace.get_tracer_provider().add_span_processor(
    SimpleExportSpanProcessor(ConsoleSpanExporter())
)
tracer = trace.get_tracer(__name__)

with tracer.start_as_current_span("span_1"):
    with tracer.start_as_current_span("span_2"):
        with tracer.start_as_current_span("span_3"):
            print("Hello world from OpenTelemetry Python!")

The application imports modules from the OpenTelemetry API and SDK packages. The API package provides the necessary interfaces for instrumentation such as the TracerProvider, Tracer, and Span classes. With the OpenTelemetry API, developers can ship instrumented code or libraries and allow their users to easily plug in their preferred vendor backend using the OpenTelemetry SDK. The SDK package is an implementation of the API and provides the functionality for creating and exporting traces and spans. For example, the above application uses the SimpleExportSpanProcessor, which receives trace spans and sends them directly to the ConsoleSpanExporter exporter, which shows span information in your console’s output.

As part of it’s specification, OpenTelemetry requires both a tracer and span processor for instrumentation. The application sets the current global tracer provider with the opentelemetry.trace.set_tracer_provider and then adds the span processor to that tracer provider with trace.get_tracer_provider().add_span_processor. Finally, it uses tracer.start_as_current_span to create three spans.

Running the above application would show a JSON output in your console of a single trace with the three configured spans:

Hello world from OpenTelemetry Python!
{
    "name": "span_3",
    "context": {
        "trace_id": "0x03daf682c3ea43db5d3efda7be2647e1",
        "span_id": "0xad92ecf97088654f",
        "trace_state": "{}"
    },
    "kind": "SpanKind.INTERNAL",
    "parent_id": "0xc6bc3607c80840d7",
    "start_time": "2020-07-27T13:58:54.622999Z",
    "end_time": "2020-07-27T13:58:54.623071Z",
    "status": {
        "canonical_code": "OK"
    },
    "attributes": {},
    "events": [],
    "links": []
}
{
    "name": "span_2",
    "context": {
        "trace_id": "0x03daf682c3ea43db5d3efda7be2647e1",
        "span_id": "0xc6bc3607c80840d7",
        "trace_state": "{}"
    },
    "kind": "SpanKind.INTERNAL",
    "parent_id": "0xca376238f0dafb19",
    "start_time": "2020-07-27T13:58:54.622960Z",
    "end_time": "2020-07-27T13:58:54.623591Z",
    "status": {
        "canonical_code": "OK"
    },
    "attributes": {},
    "events": [],
    "links": []
}
{
    "name": "span_1",
    "context": {
        "trace_id": "0x03daf682c3ea43db5d3efda7be2647e1",
        "span_id": "0xca376238f0dafb19",
        "trace_state": "{}"
    },
    "kind": "SpanKind.INTERNAL",
    "parent_id": null,
    "start_time": "2020-07-27T13:58:54.622878Z",
    "end_time": "2020-07-27T13:58:54.623763Z",
    "status": {
        "canonical_code": "OK"
    },
    "attributes": {},
    "events": [],
    "links": []
}

In order to send those traces to a backend for storage and analysis, you can replace the console exporter with another compatible exporter. A key benefit to instrumenting applications with OpenTelemetry is the ability to easily plug in any exporter without needing to change your instrumentation.

Capture traces with Datadog’s Python exporter

Using the same application example, we’ll show how you can easily import Datadog’s Python exporter and route traces to Datadog for analysis. Note that you will need the Datadog Agent to send traces to Datadog. You can run a containerized version of the Agent to get started.

To instrument your application with Datadog, you will need to install the exporter package with the following Python command:

pip install opentelemetry-ext-datadog

The Datadog exporter includes its own span processor and exporter, which replaces the ConsoleSpanExporter and SimpleExportSpanProcessor options in the previous example:

datadog_example.py

from opentelemetry import trace
from opentelemetry.ext.datadog import (
    DatadogExportSpanProcessor,
    DatadogSpanExporter,
)
from opentelemetry.sdk.trace import TracerProvider

[...]

The Datadog span processor is responsible for preparing OpenTelemetry traces for the Datadog exporter to send to the Agent. It’s important to note that while the OpenTelemetry specification supports exporting individual spans as they are finished or as batches of arbitrary spans as they are queued up, the Datadog Agent expects to receive spans as parts of complete traces.

You can forward traces to the Datadog Agent by first configuring the DatadogSpanExporter with parameters for a service name and a URL endpoint the Agent listens to for traces. Then you can pass the configured exporter to the span processor, as seen below:

datadog_example.py

#import packages
[...]

trace.set_tracer_provider(TracerProvider())
trace.get_tracer_provider().add_span_processor(
    DatadogExportSpanProcessor(
        DatadogSpanExporter(
            agent_url="http://localhost:8126", service="dd_tracing_example"
        )
    )
)
tracer = trace.get_tracer(__name__)

[...]

Datadog will submit each trace when all of its spans are finished and append the service name as a tag that you can use to search on in Datadog APM.

This is a simple example of how easy it is to incorporate Datadog’s Python exporter into a Python application, but a key benefit of instrumentation is following request traces across service boundaries in more complex applications. Next, we’ll show how to do this in a Flask application and visualize its distributed traces in Datadog.

Instrument a Flask application with Datadog and OpenTelemetry

Flask is a web framework for Python applications, and both OpenTelemetry and Datadog instrumentation libraries work together seamlessly to track requests across Flask and other Python service boundaries. We’ll show how to instrument a simple Flask client-server application using Datadog’s Python exporter using this guide.

The Flask server example below creates a single /server_request endpoint that parses a param query parameter and raises an exception if the word “error” is used as the parameter:

server.py

 
#package imports
#initialize exporter
[...]

@app.route("/server_request")
def server_request():
    param = request.args.get("param")
    with tracer.start_as_current_span("server-inner"):
        if param == "error":
            raise ValueError("forced server error")
        return "served: {}".format(param)


if __name__ == "__main__":
    app.run(port=8082)

The snippet below shows a Flask client that allows users to pass a parameter to the /server_request route:

client.py

#package imports
#initialize exporter
[...]

with tracer.start_as_current_span("client"):
    with tracer.start_as_current_span("client-server"):
        headers = {}
        propagators.inject(dict.__setitem__, headers)
        requested = get(
            "http://localhost:8082/server_request",
            params={"param": argv[1]},
            headers=headers,
        )

        assert requested.status_code == 200
        print(requested.text)

To generate traces, you can start the Flask server and make requests to the service via the instrumented client:

#start flask server & make requests
opentelemetry-instrument python server.py
opentelemetry-instrument python client.py testing

You can also generate error traces by passing in the “error” argument to the client:

 
opentelemetry-instrument python client.py error

Note that the application will generate an AssertionError if you do not include an argument. Datadog will receive three spans from two services: one server-side span and two client-side spans. These spans are automatically connected in one trace through context propagation, which passes information, such as request headers, across services. In the example client code above, that is done through the propagators.inject() call. In application architectures that leverage a collection of loosely coupled services (i.e., microservices), context propagation is critical for following a request end to end.

Visualize your application traces in Datadog

Once you have traces flowing from your instrumented application into Datadog, you can query and analyze them in Datadog APM. You can get an overview of all your traces with Live Search, as seen below.

From this list, you can select a single trace to view more details such as a breakdown of traces with flame graphs. Flame graphs show the duration of each individual span within each trace so you can easily troubleshoot application bottlenecks or latency. For example, you can use flame graphs to debug the slowest endpoints of your Flask application or troubleshoot server errors.

Start instrumenting with OpenTelemetry and Datadog today

With Datadog’s Python exporter, you can start monitoring your instrumented Python applications and get deeper insights into each of your application services. We also have exporters for tracing your Ruby and JavaScript applications, with support for Java and .Net coming soon. Read our documentation to learn more about Datadog’s other available tracing libraries, or sign up for a today to start collecting telemetry data from your instrumented applications.