Skip to main content

Command Palette

Search for a command to run...

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

Updated
8 min read
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:

Etapas de Implementação:

Neste artigo, você aprenderá a configurar a auto-instrumentação seguindo estas etapas:

  1. Preparação da aplicação

    • Alteração do Dockerfile

    • Alteração do requirements.txt

  2. Instalação de dependências

    • Instalação do Cert Manager

    • Instalação do OpenTelemetry Operator

  3. Configuração do ambiente Kubernetes

    • Criação do namespace

    • Criação e instalação do banco de dados (PostgreSQL)

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

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