This project demonstrates full observability (logs, traces, and metrics) in a Python (Flask) application using OpenTelemetry, Elastic APM, and the OpenTelemetry Collector. We show how to instrument a simple app manually, export telemetry using OTLP gRPC, and ship all data to Elastic Cloud.

A small Flask app with:

  • Manual tracing using OpenTelemetry SDK
  • Manual metrics instrumentation
  • Manual logging with OpenTelemetry context enrichment
  • Logs exported via OTLP to OpenTelemetry Collector
  • Collector exports all telemetry to Elastic APM

How Data Flows




[Flask App]
├── Traces --> Manual instrumentation using OpenTelemetry SDK
│ → OTLP gRPC Exporter → otel-collector → Elastic APM (Traces)
├── Metrics --> Manual instrumentation using OTEL SDK (create_counter)
│ → PeriodicExportingMetricReader → otel-collector → Elastic APM (Metrics)
└── Logs --> Standard Python logging + OTEL enrichment
→ OTLP gRPC via LoggingOTLPHandler → otel-collector → Elastic APM (Logs)

Everything is shipped from the app to otel-collector over gRPC (port 4317). The collector forwards it securely to Elastic Cloud using an APM Endpoint and secret token.

Instrumentation Strategy

  • Traces: Manually instrumented using OpenTelemetry’s Tracer to track requests across Flask routes and background tasks.
  • Metrics: A custom request counter is implemented via Meter and create_counter for tracking throughput.
  • Logs: Structured logs are enriched with OTEL context (e.g., trace IDs) and exported via the LoggingOTLPHandler.

This hybrid approach ensures observability while maintaining control over instrumentation costs.

Project Structure




project-root/
├── app/
│ ├── main.py # Flask app with OTEL SDK
│ └── requirements.txt # Python deps
├── collector-config.yaml # OpenTelemetry Collector config
└── docker-compose.yml # Container setup

Code Highlights




# Manual Tracing + Metrics + Logging
trace.set_tracer_provider(TracerProvider(resource=resource))
tracer = trace.get_tracer(__name__)
meter = metrics.get_meter(__name__)
logger = logging.getLogger("flask-app")
logger.addHandler(LoggingOTLPHandler(endpoint="http://otel-collector:4317", insecure=True))
logger = logging.LoggerAdapter(logger, extra={"custom": "value"})

Background thread for traces/metrics

def generate_traces_and_metrics():
while True:
with tracer.start_as_current_span("background_transaction") as span:
request_counter.add(1, {"endpoint": "/background"})
logger.info("Background tick", extra={"otel_span_id": span.get_span_context().span_id})
time.sleep(2)

OpenTelemetry Collector Config (collector-config.yaml)




receivers:
otlp:
protocols:
grpc:
http:
exporters:
debug:
otlp/elastic:
endpoint: "https://<your-apm>.elastic.co:443"
headers:
Authorization: "Bearer <your-token>"
service:
pipelines:
traces:
receivers: [otlp]
exporters: [debug, otlp/elastic]
metrics:
receivers: [otlp]
exporters: [debug, otlp/elastic]
logs:
receivers: [otlp]
exporters: [debug, otlp/elastic]

Docker Compose




services:
flask-app:
build: ./app
ports:
- "8085:8085"
environment:
- OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
depends_on:
- otel-collector
otel-collector:
image: otel/opentelemetry-collector-contrib:latest
volumes:
- ./collector-config.yaml:/etc/otelcol/config.yaml
ports:
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP

Visualizing in Kibana (Elastic APM)

Once ingested:

  • Traces show span durations, service map, dependency flow
  • Metrics show counters like request rates per endpoint
  • Logs include enriched fields (trace_id, span_id, severity, endpoint)
1vxffptkavew 4g6w7fdiiq
1fbpjunzzegs1uhlcgsymqg
1fg8vuwnwu9upn7yfnrw9zg

How to Test All Endpoints

# Root
curl http://localhost:8085/
# Upload endpoint
curl http://localhost:8085/upload
# Process endpoint
curl http://localhost:8085/process
# Report endpoint
curl http://localhost:8085/report
# Batch job
curl http://localhost:8085/batch-job
# Simulate error (for APM alerting)
curl http://localhost:8085/simulate-error

Each route creates:

  • trace with custom labels (jobstatusenvregion, etc.)
  • metric counter
  • log line with trace_id and span_id for correlation

Troubleshooting Tips

StatusCode.UNAVAILABLE on exporter

  • Cause: OTEL Collector not ready
  • Fix: Ensure otel-collector is started before app
  • Used “ in docker-compose

logging exporter deprecated

  • Error: Collector fails with logging exporter is deprecated
  • Fix: Switched to debug exporter

gRPC localhost binding in the collector

  • Issue: The Collector used localhost:4317, not accessible from other containers
  • Fix: Change the endpoint to just 4317 to bind to all interfaces



receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317 # <- changed from localhost
http:
endpoint: 0.0.0.0:4318 # <- changed from localhost

Summary

This setup gives you a ready-to-deploy observability stack:

  • Manual instrumentation with OpenTelemetry in Python
  • The collector routes telemetry to Elastic.
  • Logs, metrics, and traces correlated via trace/span IDs
  • All data is accessible and visualizable in Elastic APM

The code will be uploaded to my GitHub.

Reach out on LinkedIn for any questions or


Discover more from Tech Insights & Blogs by Rahul Ranjan

Subscribe to get the latest posts sent to your email.

Leave a Reply

Trending

Discover more from Tech Insights & Blogs by Rahul Ranjan

Subscribe now to keep reading and get access to the full archive.

Continue reading