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
Tracerto track requests across Flask routes and background tasks. - Metrics: A custom request counter is implemented via
Meterandcreate_counterfor 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 + Loggingtrace.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)




How to Test All Endpoints
# Rootcurl http://localhost:8085/# Upload endpointcurl http://localhost:8085/upload# Process endpointcurl http://localhost:8085/process# Report endpointcurl http://localhost:8085/report# Batch jobcurl http://localhost:8085/batch-job# Simulate error (for APM alerting)curl http://localhost:8085/simulate-error
Each route creates:
- a trace with custom labels (
job,status,env,region, etc.) - a metric counter
- a log line with
trace_idandspan_idfor correlation
Troubleshooting Tips
StatusCode.UNAVAILABLE on exporter
- Cause: OTEL Collector not ready
- Fix: Ensure
otel-collectoris started before app - Used “ in docker-compose
logging exporter deprecated
- Error: Collector fails with
logging exporter is deprecated - Fix: Switched to
debugexporter
gRPC localhost binding in the collector
- Issue: The Collector used
localhost:4317, not accessible from other containers - Fix: Change the endpoint to just
4317to 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






Leave a Reply