Auto-Instrumentação de uma aplicação Python usando Open Telemetry

Introdução:
Vamos começar fazendo a instrumentação de uma aplicação em Python que salva os dados em um banco de dados Postgresql. O que esse tutorial vai abordar:
Vou compartilhar a aplicação em Python utilizada
Criação do Dockerfile
Criação do Docker Compose
Criação do Arquivo de configuração do Otlp Collector
Além de explicar o que cada componente faz pretendo mostrar algumas outras formas de como fazer essa instrumentação.
Uma série de videos que eu recomnedo são destes dois canais no youtube: https://www.youtube.com/watch?v=NEYVJSp4rKo e https://www.youtube.com/watch?v=9mifCIFhtIQ
Entendendo o Opentelemetry Python Auto-Instrumentation:
É um projeto de código aberto que oferece uma abordagem unificada à observabilidade, oferecendo ferramentas e bibliotecas para coletar dados de telemetria, como rastros, métricas e logs. Um de seus principais recursos é a autoinstrumentação, que automatiza o processo de instrumentação de aplicativos para rastreamento distribuído sem a necessidade de alterações manuais no código.
Na documentação oficial do OTLP você consegue acessar outras linguagens que suportam este tipo de instrumentação: https://opentelemetry.io/docs/zero-code/
Passo 1:
Nessa etapa, antes de transformar nossa aplicação em um container e salvar os dados em um banco de dados Postgresql, resolvi dar um passo atrás pra mostrar como podemos fazer a instrumentação e visualização dos dados usando apenas comandos manuais e vamos progredindo a partir disso.
Então nós temos uma aplicação super simples em Python usando Flask:
import logging
from flask import Flask, request, jsonify
from datetime import datetime
import sys
app = Flask(__name__)
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('app.log'),
logging.StreamHandler(sys.stdout)
]
)
# Create logger
logger = logging.getLogger(__name__)
# Request logging middleware
@app.before_request
def log_request_info():
logger.info(f"Request: {request.method} {request.url} from {request.remote_addr}")
if request.method == 'POST':
logger.info(f"Request data: {request.get_data()}")
@app.after_request
def log_response_info(response):
logger.info(f"Response: {response.status_code} for {request.method} {request.path}")
return response
# Error handler
@app.errorhandler(404)
def not_found(error):
logger.warning(f"404 Error: {request.method} {request.url} - {error}")
return jsonify({"error": "Endpoint not found"}), 404
@app.errorhandler(500)
def internal_error(error):
logger.error(f"500 Error: {request.method} {request.url} - {error}")
return jsonify({"error": "Internal server error"}), 500
# Define a simple route
@app.route('/')
def hello():
logger.info("Hello endpoint accessed")
return "Hello, World!"
# GET request - retrieve user info
@app.route('/user/<name>')
def get_user(name):
logger.info(f"User endpoint accessed for: {name}")
try:
if len(name) < 2:
raise ValueError("Name too short")
return f"Hello, {name}! This is a GET request."
except ValueError as e:
logger.error(f"Validation error in get_user: {e}")
return jsonify({"error": "Name must be at least 2 characters"}), 400
# POST request - create/submit data
@app.route('/submit', methods=['POST'])
def submit_data():
logger.info("Submit endpoint accessed")
try:
data = request.get_json()
if not data:
logger.warning("No JSON data received in submit")
return jsonify({"status": "error", "message": "No JSON data received"}), 400
if 'message' not in data:
logger.warning("Missing 'message' field in submit data")
return jsonify({"status": "error", "message": "Missing 'message' field"}), 400
logger.info(f"Successfully processed message: {data['message']}")
return jsonify({
"status": "success",
"received_message": data['message'],
"response": "Data received successfully!",
"timestamp": datetime.now().isoformat()
})
except Exception as e:
logger.error(f"Unexpected error in submit_data: {e}")
return jsonify({"status": "error", "message": "Internal server error"}), 500
# Health check endpoint
@app.route('/health')
def health_check():
logger.info("Health check accessed")
return jsonify({
"status": "healthy",
"timestamp": datetime.now().isoformat(),
"version": "1.0.0"
})
# Run the application
if __name__ == '__main__':
logger.info("Starting Flask application...")
app.run(debug=True, host='0.0.0.0', port=5000)
A primeira coisa que devemos fazer é criar nosso ambiente virtual:
python -m venv venv
source venv/bin/activate
Agora vamos usar uma ferramente que se chama otel-tui para instalar essa ferramenta é necessário que você tenha go instalado na sua vm.
O otel-tui é uma ferramenta que vai receber os seus dados instrumtnados da sua aplicação,ou seja, ele vai se comportar como otlp collector e backend ao mesmo tempo. Essa ferramenta é muito boa para casos de troubleshooting ou visualização incial dos dados, se eles estão se comportando como você gostaria, etc.
Se quiser saber mais sobre aqui está o link do repositório: https://github.com/ymtdzzz/otel-tui
Para instalar a ferramenta basta:
go install github.com/ymtdzzz/otel-tui@latest
E para ver a ferramenta em execução faça:
otel-tui
Vai mostrar algo assim, veja que se você tiver uma aplicação instrumentada, já é possivel ver os Traces, Metricas, Logs, Topologia, etc.

Voltando ao que nos interessa, vamos definir as variáveis:
OTEL_SERVICE_NAME: nome do serviço
OTEL_EXPORTER_OTLP_ENDPOINT: pra onde seus dados devem ir, no nosso caso os dados vão ser enviados para o otel-tui.
OTEL_TRACES_EXPORTER, OTEL_METRICS_EXPORTER e OTEL_LOGS_EXPORTER: vamos definir como console e otlp, porque ativando o console conseguimos ver o que nossa aplicação instrumentada está fazendo, se realmente os dados estão sendo enviados para o destino esperado e se metricas, traces, logs estão sendo coletados.
export OTEL_SERVICE_NAME='flask-python'
export OTEL_EXPORTER_OTLP_ENDPOINT='localhost:4317'
export OTEL_EXPORTER_OTLP_INSECURE='true'
export OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED='true'
export OTEL_TRACES_EXPORTER='console,otlp'
export OTEL_METRICS_EXPORTER='console,otlp'
export OTEL_LOGS_EXPORTER='console,otlp'
vamos instalar os binários necessários para nossa aplicação executar:
pip install flask fastapi opentelemetry-distro opentelemetry-exporter-otlp
Configure o OTLP:
opentelemetry-bootstrap -a install
Agora vamos executar nossa aplicação instrumentada, primeiro em um outro cmd deixe o otel-tui executando porque ele quem vai receber os dados:
otel-tui
Execute a aplicação:
OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED=true opentelemetry-instrument --traces_exporter otlp,console --metrics_exporter otlp,console --logs_exporter otlp,console --service_name flask-python flask run -p 5000
Você vai ver algo como:

Alguns testes que você pode fazer é:
curl http://localhost:5000/
curl http://localhost:5000/user/John
curl -X POST http://localhost:5000/submit \
-H "Content-Type: application/json" \
-d '{"message": "Hello from curl!"}'
curl http://localhost:5000/health
curl -X POST http://localhost:5000/submit \
-H "Content-Type: application/json" \
-d '{
"message": "Complex request",
"user": "admin",
"priority": "high"
}'
curl http://localhost:5000/09
curl http://localhost:5000/user/João
E no seu otel-tui você já vai começar a receber dados de traces e métricas:


Agora vamos dar um próximo passo no nosso ambiente, ao invés de usarmos o otel-tui para receber os dados, vamos fazer uma configuração usando docker-compose, nós ainda vamos fazer a instrumentação do código na mão. Porém, o destino agora será um container que vai usar uma imagem grafana/otel-lgtm:latest essa imagem já tem o otlp collector embutido, além disso, a imagem possui alguns serviços web como o prometheus, grafana, tempo, etc, que nos possibilita visualizar os dados que estão chegando. Docker-compose:
services:
otel-lgtm:
image: grafana/otel-lgtm
container_name: otel-lgtm
ports:
- "4317:4317" # OTLP gRPC receiver
# - "4318:4318" # OTLP HTTP receiver
- "3000:3000" # Grafana UI
# networks:
# - app-network
restart: unless-stopped
Pode parar a execução do otel-tui e basta executar:
docker compose up -d
Para executar a aplicação vamos fazer o mesmo comando:
OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED=true opentelemetry-instrument --traces_exporter otlp,console --metrics_exporter otlp,console --logs_exporter otlp,console --service_name flask-python flask run -p 5000
Agora você pode ir Explorer no Grafana escolher o que você quer visualizar, usando Tempo, Prometheus ou até mesmo Loki:

Passo 2:
Já definimos nossa aplicação, fizemos a istrumentação e já estamos usando uma ferramenta que está recebendo nossos dados. Agora vamos dar mais uma incrementada na nossa stack, vamos melhorar nossa aplicação, agora vamos ajustar a aplicação para ela começar a salvar dados em um DB e depois veremos se a instrumentação está coletando este dado.
Você pode baixar a aplicação e os arquivos do projeto: https://github.com/joaochiroli/otel-instrumentacao-python. Criei uma nova branch pra essa etapa do projeto application-with-db.
OBS: quando estiver instrumentando sua aplicação não deixe o Debug enable, isso pode atrapalhar o otlp na hora de coletar os dados.
Vamos alterar e colocar as dependências no arquivo requirements.txt para:
flask
fastapi
opentelemetry-distro
opentelemetry-exporter-otlp
Flask-SQLAlchemy
psycopg2-binary
python-dotenv
opentelemetry-instrumentation-flask
opentelemetry-instrumentation-sqlalchemy
opentelemetry-instrumentation-requests
opentelemetry-instrumentation-wsgi
flask_sqlalchemy
OBS: talvez eu tenha colocado dependencias demais, acredito que voces possam testar com menos.
Vamos fazer um ajuste no Dockerfile:
FROM python:3.9-slim
# Defina o diretório de trabalho
WORKDIR /app
# Copie os arquivos necessários para o diretório de trabalho
COPY . /app
# Instale as dependências
RUN pip install -r requirements.txt
# Configura o OTLP
RUN opentelemetry-bootstrap -a install
# Exponha a porta que a aplicação vai rodar
EXPOSE 5000
# Comando para rodar a aplicação
CMD ["opentelemetry-instrument", "python", "app.py"]
Vamos fazer um update no nosso docker-compose, iremos colocar configurações do Postgresql e colocar as variáveis necessárias para nossa aplicação enviar os traces, métricas e logs pro no backend(grafana-lgtm):
services:
db:
image: postgres:14.15
container_name: postgres-db
ports:
- "5432:5432"
environment:
POSTGRES_DB: simple_app
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 10
networks:
- app-network
otel-lgtm:
image: grafana/otel-lgtm
container_name: otel-lgtm
ports:
- "4317:4317" # OTLP gRPC receiver
# - "4318:4318" # OTLP HTTP receiver
- "3000:3000" # Grafana UI
networks:
- app-network
restart: unless-stopped
app:
build: .
container_name: flask-app
volumes:
- .:/app
ports:
- "5000:5000"
environment:
POSTGRES_HOST: db
POSTGRES_PORT: 5432
POSTGRES_DB: simple_app
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
FLASK_ENV: development
FLASK_DEBUG: "true"
OTEL_SERVICE_NAME: flask-api
OTEL_EXPORTER_OTLP_ENDPOINT: otel-lgtm:4317
OTEL_EXPORTER_OTLP_PROTOCOL: grpc
OTEL_EXPORTER_OTLP_INSECURE: "true"
OTEL_TRACES_EXPORTER: console,otlp
OTEL_METRICS_EXPORTER: console,otlp
OTEL_LOGS_EXPORTER: console,otlp
OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED: "true"
# OTEL_LOG_LEVEL: DEBUG
OTEL_PYTHON_LOG_CORRELATION: "true"
OTEL_PYTHON_FLASK_ENABLED: "true"
OTEL_PYTHON_SQLALCHEMY_ENABLED: "true"
OTEL_PYTHON_REQUESTS_ENABLED: "true"
OTEL_TRACES_SAMPLER: always_on
OTEL_METRICS_SAMPLER: always_on
# OTEL_TRACES_SAMPLER: "always_on"
# OTEL_TRACES_SAMPLER_ARG: "1.0"
# OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: ""
# OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: ".*"
# OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: ".*"
networks:
- app-network
depends_on:
db:
condition: service_healthy
volumes:
postgres_data:
networks:
app-network:
driver: bridge
Depois basta executar: docker compose up -d —build
Comandos que você pode usar pra testar a aplicação:
curl http://localhost:5000/
curl http://localhost:5000/user/joao
curl -X POST http://localhost:5000/submit -H "Content-Type: application/json" -d '{"message":"test"}'
Depois disso, abrindo a página web do Grafana → Explore → Tempo → Query type: Search
E você vai ver os Traces e spans dos eventos que foram gerados através do curl, vai conseguir verificar os dados que foram coletados, o que foi feito, quanto tempo demorou pra cada evento acontecer, entre outras coisas.

Se você for até o Prometheus vai conseguer verificar os dados das requisições HTTP:
GET
POST
PUT
DELETE
SELECT - do banco de dados
E você pode criar filtros de status code: 200, 404, 500, etc. Depende de quais status sua aplicação retorna.

Passo 3:
Agora já temos um projeto com uma stack a mais que seria a parte de salvar os dados em um banco. Para incremetar um pouco mais vamos retirar o Grafana LGTM e iremos substituir por um OTLP Collector, além disso, vamos subir dois backends um Jaeger e um Prometheus que serão responsáveis por recebr estes dados.
Nesse caso vamos fazer alterações apenas no docker-compose. Você pode ter acesso ao código através do repositório do projeto https://github.com/joaochiroli/otel-instrumentacao-python e foi criado a branch otlp-collector para essa parte do artigo.
Primeira coisa que devemos fazer é alterar a variável de ambiente da nossa aplicação instrumentada, antes os dados estavam indo para o Grafana LGTM e agora vamos enviar para o Otlp Collector:
app:
build: .
container_name: flask-app
volumes:
- .:/app
ports:
- "5000:5000"
environment:
POSTGRES_HOST: db
POSTGRES_PORT: 5432
POSTGRES_DB: simple_app
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
FLASK_ENV: development
FLASK_DEBUG: "true"
OTEL_SERVICE_NAME: flask-api
OTEL_EXPORTER_OTLP_ENDPOINT: otel-collector:4317 ### ALTERADO
OTEL_EXPORTER_OTLP_PROTOCOL: grpc
OTEL_EXPORTER_OTLP_INSECURE: "true"
OTEL_TRACES_EXPORTER: console,otlp
OTEL_METRICS_EXPORTER: console,otlp
OTEL_LOGS_EXPORTER: console,otlp
OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED: "true"
OTEL_PYTHON_LOG_CORRELATION: "true"
OTEL_PYTHON_FLASK_ENABLED: "true"
OTEL_PYTHON_SQLALCHEMY_ENABLED: "true"
OTEL_PYTHON_REQUESTS_ENABLED: "true"
OTEL_TRACES_SAMPLER: always_on
OTEL_METRICS_SAMPLER: always_on
# OTEL_LOG_LEVEL: DEBUG
# OTEL_TRACES_SAMPLER: "always_on"
# OTEL_TRACES_SAMPLER_ARG: "1.0"
# OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: ""
# OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: ".*"
# OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: ".*"
networks:
- app-network
depends_on:
db:
condition: service_healthy
Agora vamos criar o nosso serviço do OTLP Collector:
otel-collector:
image: otel/opentelemetry-collector-contrib:latest
container_name: otel-collector
command: ["--config=/etc/otel-collector-config.yaml"]
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
ports:
- "4317:4317" # OTLP gRPC receiver
- "4318:4318" # OTLP HTTP receiver
- "8888:8888" # Prometheus metrics
- "8889:8889" # Prometheus exporter metrics
networks:
- app-network
restart: unless-stopped
Nós iremos usar a imagem otel/opentelemetry-collector-contrib:latest. Estamos usando o collector-contrib porque é uma imagem completa que já possui todas as dependências e extensões, como vamos usar diferentes destinos de backend essa imagem vai nos ajudar. Mas esta é uma imagem para ambientes de desenvolvimento, por conta do seu tamanho e quantidade muito grande de extensões que na maioria das vezes não serão usados. https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/extension
para ambientes de produção é recomendado que você faça a criação da sua própria imagem do collector usando uma imagem do collector oficial, sem dependências e bibliotecas. O otel-collector-contrib é baseado no otel-collector oficial.
Arquivo de configuração do otlp collector:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
timeout: 10s
send_batch_size: 1024
exporters:
debug:
verbosity: detailed
otlp:
endpoint: jaeger:4317
tls:
insecure: true
# prometheus:
# endpoint: 0.0.0.0:8889
# namespace: flask_app
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [debug, otlp]
# metrics:
# receivers: [otlp]
# processors: [batch]
# exporters: [debug, prometheus]
logs:
receivers: [otlp]
processors: [batch]
exporters: [debug]
Receivers (Receptores) - Recebem dados de telemetria das aplicações. São o "ponto de entrada" do coletor.
otlp: Protocolo padrão do OpenTelemetrygRPC (porta 4317): Protocolo binário, mais performático
HTTP (porta 4318): Protocolo baseado em REST, mais compatível
Processors (Processadores) - Transformam, filtram ou enriquecem os dados antes de exportá-los.
batch: Agrupa dados em lotes para melhorar performancetimeout: 10s: Envia a cada 10 segundossend_batch_size: 1024: Envia quando acumular 1024 itens
Exporters (Exportadores) - Enviam os dados processados para destinos finais (backends).
debug: Exibe logs detalhados no console (útil para troubleshooting)otlp: Envia para o Jaeger (sistema de tracing) via gRPC sem TLSprometheus(comentado): Exportaria métricas no formato Prometheus
Service (Serviço) - Define os pipelines - fluxo de dados do receiver → processor → exporter.
traces: Pipeline para rastreamento distribuído (spans)metrics(comentado): Pipeline para métricaslogs: Pipeline para logs estruturados
Configuração do Jaeger:
jaeger:
image: jaegertracing/all-in-one:latest
container_name: jaeger
environment:
- COLLECTOR_OTLP_ENABLED=true
- LOG_LEVEL=debug
ports:
- "16686:16686" # Jaeger UI
# - "4317:4317" # OTLP gRPC receiver
# - "4318:4318" # OTLP HTTP receiver
- "14250:14250" # Jaeger gRPC
- "14268:14268" # Jaeger HTTP
networks:
- app-network
Agora você pode visualizar os traces:


E pra finalizar vamos adicionar a configuração do Prometheus
Vamos começar descomentando as linhas do arquivo otel-collector-config.yaml:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
timeout: 10s
send_batch_size: 1024
exporters:
debug:
verbosity: detailed
otlp:
endpoint: jaeger:4317
tls:
insecure: true
prometheus:
endpoint: 0.0.0.0:8889
namespace: flask_app
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [debug, otlp]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [debug, prometheus]
logs:
receivers: [otlp]
processors: [batch]
exporters: [debug]
Vamos ajustar o arquivo de configuração do docker-compose:
prometheus:
image: prom/prometheus:latest
container_name: prometheus
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.path=/prometheus"
- "--web.console.libraries=/usr/share/prometheus/console_libraries"
- "--web.console.templates=/usr/share/prometheus/consoles"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
ports:
- "9090:9090"
networks:
- app-network
depends_on:
- otel-collector
restart: unless-stopped
volumes:
postgres_data:
prometheus-data:
Precisamos criar um arquivo prometheus.yml, ele quem vai até o collector na porta 8889 para coletar as métricas:
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
# Scrape metrics do OpenTelemetry Collector
- job_name: 'otel-collector'
static_configs:
- targets: ['otel-collector:8889']
labels:
service: 'flask-api'
# Scrape metrics do próprio Prometheus
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
Para testar se essa parte de métricas está funcionando existem algumas maneiras, uma delas é indo até a página web do Prometheus → Target health e verificando se o Prometheus está acessando o Collector na porta correta

Para ver as métricas você pode acessar:
E também é possivel fazer uma busca no Prometheus pelo nome do nosso serviço, assim vai me trazer todas as métricas deste serviço, usei o comando a seguir: {__name__=~"flask_app.*"}. Você pode usar diferentes filtros, o que vai depender é quais métricas você está querendo acessar e quais são os componentes.

O OTLP é tão completo que ele já me traz informações a respeito do meu outro componente o DB:

Conclusão:
Neste artigo foi feito:
A criação de uma aplicação Flask e sua auto-instrumentação inicial, integrada com o otel-tui e depois com o Grafana LGTM.
Depois progredimos e melhoramos a aplicação, criamos um DB para salvar os dados e fizemos a auto-instrumentação de todos os componentes que foram integrados ao Grafana LGTM.
Por último e não menos importante fizemos a criação da stack completa com a aplicação e seus componentes auto instrumentados, enviando dados para o OTLP Collector e os dados foram enviados para o Jaeger e para o Prometheus.



