Implementing OpenTelemetry Metrics in Python Apps
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:
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:
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:
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:
flask
opentelemetry-api
opentelemetry-sdk
opentelemetry-exporter-otlp-proto-http
opentelemetry-instrumentation-flask
Create an .env
file with basic configuration:
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:
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:
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:
- Configuring a
Resource
that contains metadata about your application - Creating an OTLP exporter to send metrics to the OpenTelemetry Collector
- Setting up a
PeriodicExportingMetricReader
to export metrics every 3 seconds - Initializing a
MeterProvider
with your configured resource and reader - 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:
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 requestshttp.server.request.size
: Tracks the size of HTTP request messageshttp.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:
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:
# 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:
# 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:
# 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!
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
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.comor submit a pull request and help us build better products for everyone.
See the full list of amazing projects on github