Skip to main content

Command Palette

Search for a command to run...

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

Updated
12 min read
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 OpenTelemetry

    • gRPC (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 performance

    • timeout: 10s: Envia a cada 10 segundos

    • send_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 TLS

  • prometheus (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étricas

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

http://localhost:8889/metrics

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.