How to instrument Python application automatically using OpenTelemetry Framework

How to instrument Python application automatically using OpenTelemetry Framework

opentelemetry_python_instrumentation

Here in this article we will see how we can instrument a Python flask application using OpenTelemetry framework without any modification in the application code. This is an example automatic instrumentation of application using the OpenTelemetry agent.

Test Environment

Fedora 39 workstation
Python 3.x

What is Observability

It is the ability to understand about the internals of the software or system by collecting different type of data and analysing it. The data can be related to the load , request traffic, resource utilization, component tracing. The data that we basically collect to analyze the system state is called telemetry data which includes traces, metrics, logs.

What is OpenTelemetry

Its a vendor neutral open source Observability framework for instrumenting, generating, collecting and exporting telemetry data such as traces, metrics, logs.

What is Automatic Instrumentation

Its a method to collect the telemetry data from application without any modification in the code. Language specific implementation of OpenTelemetry will provide a way to instrument the application in this automated manner. These implementation at the minimum add the OpenTelemetry API and SDK capabilities to the application. They also may add instrumentation libraries and exporter dependencies.

If you are interested in watching the video. Here is the YouTube video on the same step by step procedure outlined below.

Procedure

Step1: Create a Python Virtual Environment

As a first step let us create a virtual environment and activate it to install the relevant packages for python Flask application and OpenTelemetry Framework.

admin@fedser:vscodeprojects$ mkdir otel-getting-started
admin@fedser:vscodeprojects$ cd otel-getting-started
admin@fedser:otel-getting-started$ python -m venv venv
admin@fedser:otel-getting-started$ source ./venv/bin/activate
(venv) admin@fedser:otel-getting-started$ 

Step2: Install Flask

Here we are installed the required packages for python Flask application. They are ‘flask’ and ‘werkzeug’.

(venv) admin@fedser:otel-getting-started$ pip install 'flask<3' 'werkzeug<3'

Step3: Create and Run Flask Application

Now let us create this very basic web application which is a simulation of dice game. Whenever “/rolldice” context is called it provides us with a random number between 1 and 6 as the output. If we pass the “player” argument with his name. It will capture the player name who is rolling the dice, otherwise it will log as anonymous user.

Create Application

(venv) admin@fedser:otel-getting-started$ cat app.py 
from random import randint
from flask import Flask, request
import logging

app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

@app.route("/rolldice")
def roll_dice():
    player = request.args.get('player', default = None, type = str)
    result = str(roll())
    if player:
        logger.warn("%s is rolling the dice: %s", player, result)
    else:
        logger.warn("Anonymous player is rolling the dice: %s", result)
    return result

def roll():
    return randint(1, 6)

Run Application

(venv) admin@fedser:otel-getting-started$ flask run -p 8080

Validate Application

URL - http://localhost:8080/rolldice?player=alice

Console Log

(venv) admin@fedser:otel-getting-started$ flask run -p 8080
 * Debug mode: off
INFO:werkzeug:WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:8080
INFO:werkzeug:Press CTRL+C to quit
WARNING:app:alice is rolling the dice: 3
INFO:werkzeug:127.0.0.1 - - [09/Dec/2023 12:15:50] "GET /rolldice?player=alice HTTP/1.1" 200 -

Stop the application before proceeding to the next step.

Step4: Instrument Application

Here we’ll use the opentelemetry-instrument agent. Install the opentelemetry-distro package, which contains the OpenTelemetry API, SDK and also the tools opentelemetry-bootstrap and opentelemetry-instrument as shown below. Also we will install opentelemetry-exporter-otlp package which installs supported exporters.

(venv) admin@fedser:otel-getting-started$ pip install opentelemetry-distro opentelemetry-exporter-otlp

The opentelemetry-bootstrap -a install command reads through the list of packages installed in your active site-packages folder, and installs the corresponding instrumentation libraries for these packages.

(venv) admin@fedser:otel-getting-started$ opentelemetry-bootstrap -a install

Step5: Run Instrumented Application

Now its time to run the instrumented application as shown below. Here we are exporting the traces, metrics and logs data to the console from where the flast application is started and also we are giving our application a service name called “dice-server”.

Once the instrumented application is up and running we can try to access the application context using “http://localhost:8080/rolldice?player=alice” from browser. You can try to send multiple requests to the the application. After a while you should see the spans printed in the console, such as the following.

(venv) admin@fedser:otel-getting-started$ opentelemetry-instrument --traces_exporter console --metrics_exporter console --logs_exporter console --service_name dice-server flask run -p 8080
 * Debug mode: off
INFO:werkzeug:WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:8080
INFO:werkzeug:Press CTRL+C to quit
WARNING:app:henry is rolling the dice: 4
INFO:werkzeug:127.0.0.1 - - [09/Dec/2023 12:40:34] "GET /rolldice?player=henry HTTP/1.1" 200 -
{
    "name": "/rolldice",
    "context": {
        "trace_id": "0xcc029b2cbb1b449c7719f935b8fdf63a",
        "span_id": "0x50cfb5201728d919",
        "trace_state": "[]"
    },
    "kind": "SpanKind.SERVER",
    "parent_id": null,
    "start_time": "2023-12-09T07:10:34.862450Z",
    "end_time": "2023-12-09T07:10:34.863636Z",
    "status": {
        "status_code": "UNSET"
    },
    "attributes": {
        "http.method": "GET",
        "http.server_name": "127.0.0.1",
        "http.scheme": "http",
        "net.host.port": 8080,
        "http.host": "127.0.0.1:8080",
        "http.target": "/rolldice?player=henry",
        "net.peer.ip": "127.0.0.1",
        "http.user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
        "net.peer.port": 58876,
        "http.flavor": "1.1",
        "http.route": "/rolldice",
        "http.status_code": 200
    },
    "events": [],
    "links": [],
    "resource": {
        "attributes": {
            "telemetry.sdk.language": "python",
            "telemetry.sdk.name": "opentelemetry",
            "telemetry.sdk.version": "1.21.0",
            "service.name": "dice-server",
            "telemetry.auto.version": "0.42b0"
        },
        "schema_url": ""
    }
}
^C

The generated span tracks the lifetime of a request to the /rolldice route. The log line emitted during the request contains the same trace ID and span ID and is exported to the console via the log exporter.

  • trace_id – This is an id that’s assigned to a single request, job, or action. Something like each unique user initiated web request will have its own traceid.
  • span_id – This tracks a unit of work. Think of a request that consists of multiple steps

Try sending a few requests from multiple browser tabs and reload the pages to send more requests and wait for a little bit or terminate the app and you’ll see metrics in the console output, such as the following.

Metrics Data

{
    "resource_metrics": [
        {
            "resource": {
                "attributes": {
                    "telemetry.sdk.language": "python",
                    "telemetry.sdk.name": "opentelemetry",
                    "telemetry.sdk.version": "1.21.0",
                    "service.name": "dice-server",
                    "telemetry.auto.version": "0.42b0"
                },
                "schema_url": ""
            },
            "scope_metrics": [
                {
                    "scope": {
                        "name": "opentelemetry.instrumentation.flask",
                        "version": "0.42b0",
                        "schema_url": "https://opentelemetry.io/schemas/1.11.0"
                    },
                    "metrics": [
                        {
                            "name": "http.server.active_requests",
                            "description": "measures the number of concurrent HTTP requests that are currently in-flight",
                            "unit": "requests",
                            "data": {
                                "data_points": [
                                    {
                                        "attributes": {
                                            "http.method": "GET",
                                            "http.host": "127.0.0.1:8080",
                                            "http.scheme": "http",
                                            "http.flavor": "1.1",
                                            "http.server_name": "127.0.0.1"
                                        },
                                        "start_time_unix_nano": 1702105834862508699,
                                        "time_unix_nano": 1702105841052247437,
                                        "value": 0
                                    }
                                ],
                                "aggregation_temporality": 2,
                                "is_monotonic": false
                            }
                        },
                        {
                            "name": "http.server.duration",
                            "description": "measures the duration of the inbound HTTP request",
                            "unit": "ms",
                            "data": {
                                "data_points": [
                                    {
                                        "attributes": {
                                            "http.method": "GET",
                                            "http.host": "127.0.0.1:8080",
                                            "http.scheme": "http",
                                            "http.flavor": "1.1",
                                            "http.server_name": "127.0.0.1",
                                            "net.host.port": 8080,
                                            "http.status_code": 200
                                        },
                                        "start_time_unix_nano": 1702105834863724392,
                                        "time_unix_nano": 1702105841052247437,
                                        "count": 1,
                                        "sum": 1,
                                        "bucket_counts": [
                                            0,
                                            1,
                                            0,
                                            0,
                                            0,
                                            0,
                                            0,
                                            0,
                                            0,
                                            0,
                                            0,
                                            0,
                                            0,
                                            0,
                                            0,
                                            0
                                        ],
                                        "explicit_bounds": [
                                            0.0,
                                            5.0,
                                            10.0,
                                            25.0,
                                            50.0,
                                            75.0,
                                            100.0,
                                            250.0,
                                            500.0,
                                            750.0,
                                            1000.0,
                                            2500.0,
                                            5000.0,
                                            7500.0,
                                            10000.0
                                        ],
                                        "min": 1,
                                        "max": 1
                                    }
                                ],
                                "aggregation_temporality": 2
                            }
                        }
                    ],
                    "schema_url": "https://opentelemetry.io/schemas/1.11.0"
                }
            ],
            "schema_url": ""
        }
    ]
}

Hope you enjoyed reading this article. Thank you..