Back to Observability guides

Implementing OpenTelemetry Metrics in Python Apps

Ayooluwa Isaiah
Updated on March 3, 2025

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

OpenTelemetry 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.

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!

Prerequisites

Before proceeding with this tutorial, you'll need to know the basics of metrics in OpenTelemetry.

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:

 
mkdir otel-metrics-python
cd otel-metrics-python

Now, create a virtual environment and activate it:

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

Next, install Flask and the necessary OpenTelemetry packages:

 
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:

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:

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
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:

requirements.txt
flask
opentelemetry-api
opentelemetry-sdk
opentelemetry-exporter-otlp-proto-http
opentelemetry-instrumentation-flask

Create an .env file with basic configuration:

.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:

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:

 
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:

 
curl http://localhost:8000

The response should be:

 
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:

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:

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:

 
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:

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:

 
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:

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:

 
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:

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:

 
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:

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:

 
# 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!

Author's avatar
Article by
Ayooluwa Isaiah
Ayo is a technical content manager at Better Stack. His passion is simplifying and communicating complex technical ideas effectively. His work was featured on several esteemed publications including LWN.net, Digital Ocean, and CSS-Tricks. When he's not writing or coding, he loves to travel, bike, and play tennis.
Got an article suggestion? Let us know
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Make your mark

Join the writer's program

Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.

Write for us
Writer of the month
Marin Bezhanov
Marin is a software engineer and architect with a broad range of experience working...
Build on top of Better Stack

Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.

community@betterstack.com

or submit a pull request and help us build better products for everyone.

See the full list of amazing projects on github