Auto-Instrumentação de uma aplicação Python usando OTLP Operator

Introdução:
Anteriormente, realizamos a instrumentação manual da nossa aplicação. Neste artigo, vamos explorar a auto-instrumentação utilizando o OpenTelemetry Operator, uma funcionalidade que simplifica significativamente o processo de instrumentação em ambientes Kubernetes.
Todo o código desta etapa está disponível em:
Repositório: https://github.com/joaochiroli/otel-instrumentacao-python
Branch:
kubernetes-operatorDiretório:
/kubernetes
Etapas de Implementação:
Neste artigo, você aprenderá a configurar a auto-instrumentação seguindo estas etapas:
Preparação da aplicação
Alteração do Dockerfile
Alteração do requirements.txt
Instalação de dependências
Instalação do Cert Manager
Instalação do OpenTelemetry Operator
Configuração do ambiente Kubernetes
Criação do namespace
Criação e instalação do banco de dados (PostgreSQL)
Configuração da observabilidade
Criação e instalação do Jaeger
Criação e instalação do OpenTelemetry Collector
Criação e instalação do Instrumentation
Deploy da aplicação
- Criação e instalação dos manifestos da aplicação Flask
Preparação da aplicação
Alteramos o arquivo requirements.txt para instalarmos apenas as dependências usadas pela aplicação, retiramos as dependências do OTLP.
flask
Flask-SQLAlchemy
psycopg2-binary
flask_sqlalchemy
Fizemos o mesmo para o 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
# Exponha a porta que a aplicação vai rodar
EXPOSE 5000
# Comando para rodar a aplicação
CMD ["python", "app.py"]
Instalação de dependências
Para o o OTLP Operator funcionar precisa ter instalado o Cert Manager:
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.18.2/cert-manager.yaml
Use o comando kubectl get pods -A para verificar se seus pods estão executando corretamente

Vamos instalar o operator com o seguinte comando:
kubectl apply -f https://github.com/open-telemetry/opentelemetry-operator/releases/latest/download/opentelemetry-operator.yaml
Use o comando kubectl get pods -A para verificar se seus pods estão executando corretamente

Configuração do ambiente Kubernetes
Criamos um namespace pro nosso projeto.
Para fazer o deploy do namespace execute kubectl apply -f namespace.yaml:
apiVersion: v1
kind: Namespace
metadata:
name: simple-app
labels:
name: simple-app
Para fazer o deploy do configmap execute kubectl apply -f configmap-app.yaml:
# configmap-app.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: flask-app-config
namespace: simple-app
data:
DATABASE_URL: "postgresql://postgres:postgres@postgres-db:5432/simple_app"
FLASK_ENV: "development"
FLASK_DEBUG: "false"
Para fazer o deploy do secret execute kubectl apply -f secret-postgres.yaml:
apiVersion: v1
kind: Secret
metadata:
name: postgres-secret
namespace: simple-app
type: Opaque
data:
postgres-password: cG9zdGdyZXM= # postgres em base64
postgres-user: cG9zdGdyZXM= # postgres em base64
postgres-db: c2ltcGxlX2FwcA== # simple_app em base64
Para fazer o deploy do Banco de Dados PostgreSQL execute kubectl apply -f postgres.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
namespace: simple-app
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:latest
ports:
- containerPort: 5432
env:
- name: POSTGRES_DB
valueFrom:
secretKeyRef:
name: postgres-secret
key: postgres-db
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: postgres-secret
key: postgres-user
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: postgres-password
- name: POSTGRES_HOST_AUTH_METHOD
value: "md5"
# - name: PGDATA
# value: /var/lib/postgresql/data/pgdata
# volumeMounts:
# - name: postgres-storage
# mountPath: /var/lib/postgresql/data
# subPath: pgdata
livenessProbe:
exec:
command:
- pg_isready
- -U
- postgres
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
exec:
command:
- pg_isready
- -U
- postgres
initialDelaySeconds: 5
periodSeconds: 5
volumes:
- name: postgres-storage
persistentVolumeClaim:
claimName: postgres-pvc
---
apiVersion: v1
kind: Service
metadata:
name: postgres
namespace: simple-app
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
namespace: simple-app
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
Você pode verificar se os pods estão executando usando o comando kubectl get pods -n simple-app

E vendo os logs do pod pra saber se está sendo executado como deveria. kubectl logs <nome do pod> -n simple-app

Configuração da observabilidade
Podemos seguir com os próximos passos. O primeiro deles é subir nosso backend que no caso é o Jaeger. Para fazer o deploy execute kubectl apply -f jaeger.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: jaeger
namespace: simple-app
spec:
replicas: 1
selector:
matchLabels:
app: jaeger
template:
metadata:
labels:
app: jaeger
spec:
containers:
- name: jaeger
image: jaegertracing/all-in-one:latest
ports:
- containerPort: 5775
protocol: UDP
- containerPort: 6831
protocol: UDP
- containerPort: 6832
protocol: UDP
- containerPort: 5778
protocol: TCP
- containerPort: 16686
protocol: TCP
- containerPort: 4317
protocol: TCP
- containerPort: 4318
protocol: TCP
- containerPort: 14250
protocol: TCP
- containerPort: 14268
protocol: TCP
- containerPort: 14269
protocol: TCP
- containerPort: 9411
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: jaeger-query
namespace: simple-app
spec:
ports:
- port: 16686
targetPort: 16686
name: query
selector:
app: jaeger
type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
name: jaeger-collector
namespace: simple-app
spec:
ports:
- port: 4317
targetPort: 4317
name: grpc
- port: 4318
targetPort: 4318
name: http
- port: 14268
targetPort: 14268
name: binary
selector:
app: jaeger
type: ClusterIP
Verifique se os pods estão executando. kubectl get pods -n simple-app.

Agora pra acessar a página web da sua aplicação faça kubectl port-forward -n simple-app svc/jaeger-query 16686:16686

Vamos agora fazer o deploy do OTLP Collector ele quem vai coletar os dados e enviar para o Backend.
kubectl apply -f jaeger.yaml
# otel-collector.yaml
apiVersion: opentelemetry.io/v1alpha1
kind: OpenTelemetryCollector
metadata:
name: otel-collector
namespace: simple-app
spec:
mode: deployment
image: otel/opentelemetry-collector-contrib:latest
config: |
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
memory_limiter:
check_interval: 1s
limit_mib: 512
exporters:
debug:
verbosity: detailed
sampling_initial: 5
sampling_thereafter: 200
otlp:
endpoint: jaeger-collector:4317
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [otlp]
metrics:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [debug]
logs:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [debug]
Agora vamos criar a configuração do Intrumentation. O Instrumentation é o "template de configuração" que diz ao Operator como instrumentar automaticamente suas aplicações sem modificar o código-fonte, definindo todos os parâmetros de configuração do OpenTelemetry de forma declarativa via Kubernetes.
Na documentação você pode verificar que é possível usar o Operator para instrumentar vários tipos de linguagem não só Python. Link: https://github.com/open-telemetry/opentelemetry-operator.
Vamos fazer o deploy kubectl apply -f instrumentation.yaml
# instrumentation.yaml
apiVersion: opentelemetry.io/v1alpha1
kind: Instrumentation
metadata:
name: flask-instrumentation
namespace: simple-app
spec:
# propagators:
# - tracecontext
# - baggage
# sampler:
# type: parentbased_traceidratio
# argument: "1.0" # ou "0.25" para 25% sampling
python:
image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-python:latest
env:
- name: OTEL_TRACES_EXPORTER
value: otlp
# - name: OTEL_METRICS_EXPORTER
# value: otlp
# - name: OTEL_LOGS_EXPORTER
# value: otlp
# - name: OTEL_EXPORTER_OTLP_PROTOCOL
# value: http/protobuf
# - name: OTEL_EXPORTER_OTLP_PROTOCOL
# value: grpc
- name: OTEL_SERVICE_NAME
value: flask-simple-app
- name: OTEL_RESOURCE_ATTRIBUTES
value: deployment.environment=development
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: http://otel-collector-collector-headless.simple-app.svc.cluster.local:4318
exporter:
endpoint: http://otel-collector-collector-headless.simple-app.svc.cluster.local:4318
Agora execute o comando kubectl get instrumentation -n simple-app para saber se seu instrumentation está devidamente configurado.
Deploy da aplicação
Por último e mais importante, vamos fazer o deploy da aplicação. A parte mais importante e que faz toda essa mágica ser possível é essa configuração abaixo, através dela a aplicação e o instrumentation conseguem se comunicar e enviar dados para o collector.
annotations:
instrumentation.opentelemetry.io/inject-python: "simple-app/flask-instrumentation"
Para fazer o deploy iremos usar o comando kubectl apply -f flask-app-deployment.yaml.
# flask-app-deployment.yaml - VERSÃO CORRIGIDA
apiVersion: apps/v1
kind: Deployment
metadata:
name: flask-app
namespace: simple-app
spec:
replicas: 1
selector:
matchLabels:
app: flask-app
template:
metadata:
labels:
app: flask-app
annotations:
instrumentation.opentelemetry.io/inject-python: "simple-app/flask-instrumentation"
spec:
containers:
- name: flask-app
image: joaochiroli123/app-flask:latest
ports:
- containerPort: 5000
name: http
env:
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: postgres-secret
key: postgres-user
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: postgres-password
- name: POSTGRES_DB
valueFrom:
secretKeyRef:
name: postgres-secret
key: postgres-db
- name: POSTGRES_HOST
value: "postgres"
- name: POSTGRES_PORT
value: "5432"
- name: FLASK_ENV
value: "development"
- name: FLASK_DEBUG
value: "false"
livenessProbe:
httpGet:
path: /health
port: 5000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 5000
initialDelaySeconds: 10
periodSeconds: 5
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
name: flask-app
namespace: simple-app
spec:
selector:
app: flask-app
ports:
- name: http
port: 80
targetPort: 5000
type: ClusterIP
Você pode verificar se os pods estão executando através do comando kubectl get pods -n simple-app

Verificar os logs da aplicação se estão corretos. kubectl logs <nome do pod> -n simple-app

E o mais importante através do próximo comando é possivel verificar se a instrumentação foi feita na sua aplicação sem quebrar internamente alguma coisa kubectl exec -n simple-app <nome do pod> -- python -c "import sys; print('\n'.join(sys.path))"

Para executar os comandos abaixo para testar nossa aplicação:
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 pra ver ela na página web vamos executar. kubectl port-forward -n simple-app svc/flask-app 5000:80
Agora já é possível fazer algumas execuções na nossa página Web e verificar os traces chegando no Jaeger.


Conclusão:
Neste artigo, exploramos como a auto-instrumentação utilizando o OpenTelemetry Operator simplifica significativamente a implementação de observabilidade em aplicações Python rodando em Kubernetes. Diferentemente da instrumentação manual, essa abordagem elimina a necessidade de modificar o código-fonte da aplicação ou gerenciar manualmente as dependências do OpenTelemetry.
Através do Operator, conseguimos:
Automatizar a injeção de bibliotecas de instrumentação via init containers
Centralizar as configurações de telemetria usando o recurso Instrumentation
Padronizar a coleta de traces, métricas e logs em todo o cluster
Simplificar o gerenciamento de observabilidade em ambientes Kubernetes



