Operating Microservices: When Logs Aren’t Enough
Imagine receiving a bug ticket: “User cannot pay, system spins for 10 seconds and returns a 500 error.” In a Docker-based Microservices system, this request might pass through 5-7 different services. If you only use docker logs to investigate, you’ll spend hours piecing together timestamps. Finding which service is the actual “bottleneck” is a real nightmare.
OpenTelemetry (OTel) was born to solve this pain point. It helps us collect Traces (request paths) and Metrics (system stats) centrally. OTel’s greatest strength is its Auto-instrumentation capability. You can monitor Java, Python, or Node.js applications without changing a single line of business logic. Everything is configured entirely from outside the container.
Three Approaches to Observability: Where Are You?
Each stage of project development suits a different deployment method. Here are the 3 methods I have applied:
- Manual Instrumentation: You have to install libraries and write code to create Spans. This provides the highest level of detail but is extremely labor-intensive when the system scales to dozens of services.
- Sidecar Pattern: Each application container is paired with a secondary Collector container. This is common on Kubernetes but wastes RAM and CPU resources if deployed on pure Docker Compose.
- Auto-Instrumentation via Docker Env (Recommended): You simply load an Agent (.jar file or package) and configure it via environment variables. This is the most balanced solution: fast deployment, no code changes, and easy maintenance.
In my experience, if you want results in 15 minutes, choose the third option.
Deployment Architecture: Don’t Send Data Directly
Many developers often configure applications to push data directly to Jaeger or Prometheus. However, this approach lacks flexibility. Instead, use the OTel Collector as a transit station following the Hub-and-Spoke model.
Standard data flow: App Container (Agent) -> OTel Collector -> Storage/Visualization (Jaeger/Grafana).
Using a Collector allows you to switch backends easily. For example, when moving from Jaeger to Datadog, you only need to modify 5 lines of config in the Collector. All upstream application containers remain completely unaffected.
Hands-on Deployment Guide
Here is how I integrate OTel for a Flask (Python) application running in Docker without modifying the app.py file.
Step 1: Setting up the OTel Collector Brain
Create an otel-config.yaml file to define how to receive and export data. This is where you decide where the data goes:
receivers:
otlp:
protocols:
grpc: # Default port 4317
http: # Default port 4318
exporters:
jaeger:
endpoint: "jaeger:14250"
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp]
exporters: [jaeger]
Step 2: Optimizing Dockerfile with OTel Agent
Instead of manually installing each library, I use opentelemetry-bootstrap. This command automatically detects frameworks like Flask, Django, or Redis to install the appropriate instrumentation version.
FROM python:3.9-slim
WORKDIR /app
COPY . .
RUN pip install flask opentelemetry-distro opentelemetry-exporter-otlp
# Automatically install necessary plugins
RUN opentelemetry-bootstrap -a install
# Run the application through the OTel wrapper
CMD ["opentelemetry-instrument", "python", "app.py"]
Step 3: Connecting with Docker Compose
Pro tip: I’m using Docker Compose V2 (the docker compose command without the hyphen). Environment variables will handle connecting the application to the Collector.
services:
app:
build: .
environment:
- OTEL_SERVICE_NAME=order-service
- OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
- OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production
depends_on:
- otel-collector
otel-collector:
image: otel/opentelemetry-collector-contrib
volumes:
- ./otel-config.yaml:/etc/otel-collector-config.yaml
command: ["--config=/etc/otel-collector-config.yaml"]
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "16686:16686"
Real-world Tips and Time-Consuming Pitfalls
After several “getting burned” experiences during real-world deployments, I’ve gathered 3 important notes:
- DNS Issues in Docker: Never use
localhost:4317in the App’s environment variables. The container will look for itself instead of the Collector. Use the service name defined in the compose file. - Control Sampling Rate: By default, OTel sends 100% of data. For systems processing over 1,000 requests/second, this will consume all your bandwidth. Use
OTEL_TRACES_SAMPLER=traceidratioand set the value to0.1to capture only 10% of the data. - Debug Collector: If Jaeger is blank, check the Collector’s logs using
docker logs otel-collector. Usually, the error lies in a mismatch between gRPC and HTTP protocols.
Conclusion
Integrating OpenTelemetry into Docker is the first step toward mastering Microservices systems. This method gives you a transparent view of every request without cluttering your source code. Once data is centralized, performance optimization becomes an interesting challenge rather than a nightmare whenever an incident occurs.

