# Implementing OpenTelemetry Metrics in Python Apps

Modern applications demand robust observability solutions to monitor
performance, detect bottlenecks, and ensure seamless user experiences.

[OpenTelemetry](https://opentelemetry.io/) has emerged as a powerful,
standardized framework for capturing telemetry data — including traces, metrics,
and logs — from distributed systems. By offering a unified approach to
instrumentation, OpenTelemetry allows developers to track and visualize
application health across complex architectures.

<iframe width="100%" height="315" src="https://www.youtube.com/embed/sMqlObpNz64" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

In this tutorial, we'll focus on implementing OpenTelemetry metrics in a Python
Flask application. Starting with the setup of the OpenTelemetry SDK, we'll
explore key metric types such as `Counter`, `UpDownCounter`, `Gauge`, and
`Histogram`.

You'll learn how to instrument your application to automatically track HTTP
metrics, customize data aggregation, and configure efficient data collection and
filtering. Finally, we'll cover how to send your metrics data to an
OpenTelemetry Collector, allowing you to route it to a backend of your choice
for analysis and visualization.

By the end of this guide, you'll have a comprehensive understanding of how to
leverage OpenTelemetry metrics in Python, empowering you to gain actionable
insights into your application's performance and health.

Let's get started!

[ad-logs]

## Prerequisites

Before proceeding with this tutorial, you'll need to know the basics of
[metrics in OpenTelemetry](https://opentelemetry.io/docs/concepts/signals/metrics/).

## Step 1 — Setting up the demo project

Let's start by creating a basic "Hello World" Flask server. Create a new
directory for your project and navigate into it:

```bash
mkdir otel-metrics-python
cd otel-metrics-python
```

Now, create a virtual environment and activate it:

```bash
python -m venv venv
source venv/bin/activate  # On Windows, use: venv\Scripts\activate
```

Next, install Flask and the necessary OpenTelemetry packages:

```bash
pip install flask opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp-proto-http
```

Let's create a simple Flask application. Create a file named `app.py` with the
following content:

```python
[label app.py]
from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
   return "Hello world!"

if __name__ == '__main__':
   app.run(host='0.0.0.0', port=8000, debug=True)
```

Next, create a `compose.yaml` file to set up our services:

```yaml
[label compose.yaml]
services:
 app:
   build: .
   container_name: otel-metrics-demo
   environment:
     PORT: ${PORT}
     LOG_LEVEL: ${LOG_LEVEL}
   env_file:
     - ./.env
   ports:
     - 8000:8000
   networks:
     - otel-metrics-demo-network
   volumes:
     - .:/app

 collector:
   container_name: otel-metrics-demo-collector
   image: otel/opentelemetry-collector:latest
   volumes:
     - ./otelcol.yaml:/etc/otelcol/config.yaml
   networks:
     - otel-metrics-demo-network

networks:
 otel-metrics-demo-network:
```

Create a `Dockerfile` for the Python application:

```Dockerfile
[label Dockerfile]
FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install -r requirements.txt

COPY . .

CMD ["python", "app.py"]
```

Create a `requirements.txt` file:

```text
[label requirements.txt]
flask
opentelemetry-api
opentelemetry-sdk
opentelemetry-exporter-otlp-proto-http
opentelemetry-instrumentation-flask
```

Create an `.env` file with basic configuration:

```text
[label .env]
PORT=8000
LOG_LEVEL=info
OTEL_SERVICE_NAME=otel-metrics-demo
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-metrics-demo-collector:4318
```

Finally, create an `otelcol.yaml` file for the OpenTelemetry Collector
configuration:

```yaml
[label otelcol.yaml]
receivers:
 otlp:
   protocols:
     http:
       endpoint: otel-metrics-demo-collector:4318

processors:
 batch:

exporters:
 logging:
   verbosity: detailed

service:
 pipelines:
   metrics:
     receivers: [otlp]
     processors: [batch]
     exporters: [logging]
```

To launch both services, execute the command below:

```bash
docker compose up
```

You should see output indicating that both the Flask application and the
OpenTelemetry Collector are running. To test the application, send a request to
the root endpoint from a different terminal:

```bash
curl http://localhost:8000
```

The response should be:

```text
Hello world!
```

Now that the services are up and running, let's go ahead and set up the
OpenTelemetry SDK.

## Step 2 — Initializing the OpenTelemetry SDK

Before we can collect metrics, we need to initialize the OpenTelemetry SDK in
our Flask application. Update your `app.py` file with the following code:

```python
[label app.py]
import os
import time
from flask import Flask, request

from opentelemetry import metrics
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.sdk.resources import Resource
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
from opentelemetry.semconv.resource import ResourceAttributes

def setup_otel_sdk():
   # Configure the resource with service info
   resource = Resource.create({
       ResourceAttributes.SERVICE_NAME: os.getenv("OTEL_SERVICE_NAME", "otel-metrics-demo"),
       ResourceAttributes.SERVICE_VERSION: "0.1.0",
   })

   # Create an OTLP exporter
   exporter = OTLPMetricExporter(
       endpoint=os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318") + "/v1/metrics"
   )

   # Create a metric reader that will periodically export metrics
   reader = PeriodicExportingMetricReader(
       exporter=exporter,
       export_interval_millis=3000  # Export every 3 seconds
   )

   # Initialize the MeterProvider with resource and reader
   provider = MeterProvider(resource=resource, metric_readers=[reader])

   # Set the global MeterProvider
   metrics.set_meter_provider(provider)

   return provider

app = Flask(__name__)

# Initialize OpenTelemetry SDK
meter_provider = setup_otel_sdk()

@app.route('/')
def hello():
   return "Hello world!"

if __name__ == '__main__':
   app.run(host='0.0.0.0', port=8000, debug=True)
```

This code sets up the OpenTelemetry SDK by:

1. Configuring a `Resource` that contains metadata about your application
2. Creating an OTLP exporter to send metrics to the OpenTelemetry Collector
3. Setting up a `PeriodicExportingMetricReader` to export metrics every 3
   seconds
4. Initializing a `MeterProvider` with your configured resource and reader
5. Setting the global meter provider so it can be accessed from anywhere in your
   code

Once these changes are saved, the Flask application will restart with
OpenTelemetry configured. Now let's move on to automatic instrumentation for
Flask.

## Step 3 — Automatically instrument HTTP server metrics

OpenTelemetry provides automatic instrumentation for common libraries to help
you save time and focus on business-specific metrics. For Flask, we can use the
`opentelemetry-instrumentation-flask` package, which we've already installed
through our requirements.

Update your `app.py` file with the following code:

```python
[label app.py]
import os
import time
from flask import Flask, request

from opentelemetry import metrics
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.sdk.resources import Resource
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
from opentelemetry.semconv.resource import ResourceAttributes
from opentelemetry.instrumentation.flask import FlaskInstrumentor

def setup_otel_sdk():
   # Configure the resource with service info
   resource = Resource.create({
       ResourceAttributes.SERVICE_NAME: os.getenv("OTEL_SERVICE_NAME", "otel-metrics-demo"),
       ResourceAttributes.SERVICE_VERSION: "0.1.0",
   })

   # Create an OTLP exporter
   exporter = OTLPMetricExporter(
       endpoint=os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318") + "/v1/metrics"
   )

   # Create a metric reader that will periodically export metrics
   reader = PeriodicExportingMetricReader(
       exporter=exporter,
       export_interval_millis=3000  # Export every 3 seconds
   )

   # Initialize the MeterProvider with resource and reader
   provider = MeterProvider(resource=resource, metric_readers=[reader])

   # Set the global MeterProvider
   metrics.set_meter_provider(provider)

   return provider

app = Flask(__name__)

# Initialize OpenTelemetry SDK
meter_provider = setup_otel_sdk()

# Instrument Flask with OpenTelemetry
FlaskInstrumentor().instrument_app(app)

@app.route('/')
def hello():
   return "Hello world!"

if __name__ == '__main__':
   app.run(host='0.0.0.0', port=8000, debug=True)
```

The key addition here is the `FlaskInstrumentor().instrument_app(app)` line,
which automatically adds instrumentation to your Flask application.

Once the changes are saved and the application restarts, the Flask
instrumentation will automatically track HTTP metrics. The OpenTelemetry
Collector logs will now show metrics for your Flask application including:

- `http.server.duration`: Measures the duration of inbound HTTP requests
- `http.server.request.size`: Tracks the size of HTTP request messages
- `http.server.response.size`: Tracks the size of HTTP response messages

Send a request to the root endpoint to generate some metrics:

```bash
curl http://localhost:8000
```

You should now see these metrics being logged by the collector.

## Step 4 — Creating a Counter metric

Now, let's create custom metrics starting with a `Counter` metric. A Counter is
used to record a value that only increases. Let's track the total number of
requests to our Flask application.

Update your `app.py` file:

```python
[label app.py]
import os
import time
from flask import Flask, request

from opentelemetry import metrics
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.sdk.resources import Resource
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
from opentelemetry.semconv.resource import ResourceAttributes
from opentelemetry.instrumentation.flask import FlaskInstrumentor

def setup_otel_sdk():
   # Configuration code remains the same
   # ...

app = Flask(__name__)

# Initialize OpenTelemetry SDK
meter_provider = setup_otel_sdk()

# Instrument Flask with OpenTelemetry
FlaskInstrumentor().instrument_app(app)

# Create a meter
meter = metrics.get_meter(os.getenv("OTEL_SERVICE_NAME", "otel-metrics-demo"))

# Create a request counter
request_counter = meter.create_counter(
   name="http.server.requests",
   description="Total number of HTTP requests received.",
   unit="{requests}"
)

@app.route('/')
def hello():
   # Increment the request counter with route attribute
   request_counter.add(1, {"http.route": request.path})
   return "Hello world!"

if __name__ == '__main__':
   app.run(host='0.0.0.0', port=8000, debug=True)
```

In this code, we create a meter using `metrics.get_meter()` and use it to create
a counter named `http.server.requests`. Every time the root endpoint is
accessed, we increment the counter by 1 and add the route as an attribute.

Send a request to the server:

```bash
curl http://localhost:8000
```

You should now see the custom `http.server.requests` metric being logged by the
collector.

## Step 5 — Creating an UpDownCounter metric

An `UpDownCounter` is similar to a `Counter`, but it allows the value to both
increase and decrease. This is perfect for tracking the number of active
requests.

Update your `app.py` file:

```python
[label app.py]
# Previous imports and setup code remain the same

# Create a meter
meter = metrics.get_meter(os.getenv("OTEL_SERVICE_NAME", "otel-metrics-demo"))

# Create a request counter
request_counter = meter.create_counter(
   name="http.server.requests",
   description="Total number of HTTP requests received.",
   unit="{requests}"
)

# Create an active requests up-down counter
active_requests = meter.create_up_down_counter(
   name="http.server.active_requests",
   description="Number of in-flight requests.",
   unit="{requests}"
)

@app.route('/')
def hello():
   # Increment the request counter with route attribute
   request_counter.add(1, {"http.route": request.path})

   # Increment active requests counter
   active_requests.add(1)

   # Simulate processing time
   time.sleep(1)

   result = "Hello world!"

   # Decrement active requests counter
   active_requests.add(-1)

   return result

if __name__ == '__main__':
   app.run(host='0.0.0.0', port=8000, debug=True)
```

In this code, we've added an `UpDownCounter` called `active_requests`. When a
request is received, we increment the counter, and after processing the request,
we decrement it. This gives us a real-time count of in-progress requests.

Let's test this with some concurrent requests. You can use a tool like Apache
Bench:

```bash
ab -n 100 -c 10 http://localhost:8000/
```

This command sends 100 requests with 10 concurrent connections. The
`active_requests` metric should show the number of active requests fluctuating.

## Step 6 — Creating a Gauge metric

A `Gauge` in OpenTelemetry is used to record values that can go up and down, but
unlike an `UpDownCounter`, a Gauge is designed to track values that change
independently of previous measurements, like memory usage.

In Python, we can use an `ObservableGauge` to track memory usage:

```python
[label app.py]
# Previous imports and setup code remain the same
import psutil

# Install psutil if not already installed
# pip install psutil

# Create a meter
meter = metrics.get_meter(os.getenv("OTEL_SERVICE_NAME", "otel-metrics-demo"))

# Create a request counter and active requests counter as before
# ...

# Function to get memory usage
def get_memory_usage():
   return psutil.Process().memory_info().rss

# Create an observable gauge for memory usage
memory_gauge = meter.create_observable_gauge(
   name="system.memory.usage",
   description="Memory usage of the application.",
   unit="By",
   callbacks=[lambda observer: observer.observe(get_memory_usage())]
)

@app.route('/')
def hello():
   # Previous metric instrumentation remains the same
   # ...
   return "Hello world!"
```

Here, we use the `psutil` library to get the resident set size (RSS) of the
current process, which represents the memory usage. The
`create_observable_gauge` method sets up a gauge with a callback function that
will be called periodically to observe the current memory usage.

Make sure to install the `psutil` library:

```bash
pip install psutil
```

Update your `requirements.txt` file to include `psutil` as well.

## Step 7 — Creating and customizing Histograms

Histograms are useful for measuring the distribution of values, such as request
durations. Let's create a histogram to measure the processing time of our
request handler:

```python
[label app.py]
# Previous imports and setup code remain the same

# Create a meter
meter = metrics.get_meter(os.getenv("OTEL_SERVICE_NAME", "otel-metrics-demo"))

# Create other metrics as before
# ...

# Create a histogram for request processing time
request_duration = meter.create_histogram(
   name="http.server.request.processing.duration",
   description="Time spent processing the request.",
   unit="s"
)

@app.route('/')
def hello():
   # Increment the request counter with route attribute
   request_counter.add(1, {"http.route": request.path})

   # Increment active requests counter
   active_requests.add(1)

   # Start timing request processing
   start_time = time.time()

   # Simulate processing time
   time.sleep(1)

   # Calculate duration
   duration = time.time() - start_time

   # Record the processing time in the histogram
   request_duration.record(duration, {"http.route": request.path})

   result = "Hello world!"

   # Decrement active requests counter
   active_requests.add(-1)

   return result
```

This creates a histogram named `http.server.request.processing.duration` that
records the time spent processing each request. By default, OpenTelemetry's
histograms use a predefined set of bucket boundaries, but you can customize them
if needed:

```python
# Create a histogram with custom bucket boundaries
request_duration = meter.create_histogram(
   name="http.server.request.processing.duration",
   description="Time spent processing the request.",
   unit="s",
   # Define custom buckets for durations in seconds
   # Note: The exact API for custom boundaries may vary by OTel SDK version
   # In some versions, you might need to use a view instead
)
```

In OpenTelemetry Python, configuring custom bucket boundaries typically requires
setting up a view through the SDK configuration, which is a bit more complex
than directly specifying them when creating the histogram.

## Final thoughts

In this tutorial, we explored the essentials of implementing OpenTelemetry
metrics in a Python Flask application. We covered setting up the OpenTelemetry
SDK, automatic instrumentation of HTTP server metrics, and creating custom
metrics including `Counter`, `UpDownCounter`, `Gauge`, and `Histogram`.

OpenTelemetry provides a unified framework for collecting and exporting
telemetry data, making it easier to monitor and troubleshoot your applications.
With the knowledge gained from this tutorial, you can implement comprehensive
metrics collection in your Python applications, gaining valuable insights into
their performance and health.

For next steps, you might want to explore distributed tracing with OpenTelemetry
or dive deeper into creating custom dashboards and alerts in your chosen metrics
backend. You could also look into more advanced configurations of the
OpenTelemetry Collector to filter, transform, or enrich your telemetry data.

Thanks for reading, and happy monitoring!