Many Python services still rely on print statements and ad-hoc logs. This step-by-step guide instruments a FastAPI service with OpenTelemetry auto-instrumentation, shipping traces, metrics, and traceId-carrying logs to OBSERVE, with three common pitfalls to avoid.
Observability for Python services often stops at print statements and hand-rolled logs: when something breaks you grep a log file. With OpenTelemetry auto-instrumentation you get traces and metrics without touching business code, and once you attach traceIds to your logs the debugging path gets far smoother. Here is the full walkthrough using FastAPI.
pip install opentelemetry-distro opentelemetry-exporter-otlp-proto-http \
opentelemetry-instrumentation-fastapi \
opentelemetry-instrumentation-requests \
opentelemetry-instrumentation-psycopg2
opentelemetry-distro pulls in the SDK and sane global defaults, while each instrumentation package generates spans for its framework automatically. The mechanism is worth understanding: the packages monkey-patch the framework at import time, wrapping route handlers and HTTP clients so that spans are created with the correct parent-child relationships. That is why you never import them in business code.
export OTEL_SERVICE_NAME=order-service
export OTEL_EXPORTER_OTLP_ENDPOINT=https://ob.jjhub.cn/otel
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
export OTEL_TRACES_SAMPLER=parentbased_traceidratio
export OTEL_TRACES_SAMPLER_ARG=0.1
export OTEL_LOGS_EXPORTER=otlp
export OTEL_METRICS_EXPORTER=otlp
The service name must be unique, otherwise multiple instances merge into one line in the trace graph. OTEL_EXPORTER_OTLP_PROTOCOL selects the wire format and must match the backend; http/protobuf is the safest default because it traverses load balancers without opening long-lived gRPC connections. Start sampling at 10% and tune it once the pipeline is proven and you know the data volume. Setting OTEL_LOGS_EXPORTER and OTEL_METRICS_EXPORTER to otlp ensures all three signal types flow through the same exporter instead of defaulting to a local console.
Two more variables are worth setting from day one. OTEL_RESOURCE_ATTRIBUTES=deployment.environment=prod,service.version=1.4.2 tags every span and metric with environment and version, which is what lets OBSERVE split dashboards by environment and correlate a regression back to the release that introduced it. OTEL_PROPAGATORS=tracecontext,baggage keeps the trace context alive across your own HTTP calls to other services so the trace does not break at a service boundary.
Without changing any code, launch uvicorn through opentelemetry-instrument:
opentelemetry-instrument uvicorn app.main:app --host 0.0.0.0 --port 8000
Every FastAPI request now produces a span, and downstream HTTP calls made through requests and database queries through psycopg2 are automatically attached as child spans. On the metrics side you get the standard HTTP server metrics out of the box, including http.server.duration histograms and request counts, which feed directly into OBSERVE dashboards without any extra wiring.
Auto-instrumentation covers traces and metrics, but linking logs to traces requires a small manual step. When your business code logs through the standard logging module, carry the current span context along:
import logging
from opentelemetry import trace
logger = logging.getLogger("order-service")
def get_order(order_id):
ctx = trace.get_current_span().get_span_context()
logger.info("fetching order %s", order_id,
extra={"trace_id": ctx.trace_id, "span_id": ctx.span_id})
Once OBSERVE recognizes the trace_id and span_id fields, the trace detail page shows every log line that request produced, and you can reverse from a log line back to the full call chain. If you want this done everywhere without editing each log call, install opentelemetry-instrumentation-logging and configure its format string to include the trace context automatically, then route the logging handler to the OTLP log exporter.
Beyond the automatic spans, add a couple of business attributes that your on-call engineers actually care about: order id, customer tier, or tenant. These show up as searchable fields in the trace view and turn a wall of identical requests into something you can filter. Keep them high-signal and low-cardinality; adding a customer email to every span bloats storage without helping anyone.
Send a real request and confirm three things in the console: the trace graph shows the FastAPI, downstream HTTP, and database spans; the metrics page reports request count, latency, and error rate; and the log page resolves the corresponding lines from a traceId.
Three common pitfalls: the OTLP endpoint must include the /otel path or you will get a 404; the exporter protocol must match the backend, so do not mix http/protobuf and grpc; and changes to OTEL_SERVICE_NAME only take effect after a process restart, so never hot-edit it in a running service. If spans are missing entirely, check that the endpoint is reachable from the container and that the exporter is not silently retrying into a dead queue; the SDK's internal logging can be turned on with OTEL_LOG_LEVEL=debug to see exactly what is being exported.
Once everything is flowing, the highest-leverage follow-up is a service map: because every downstream call is now a child span, OBSERVE can render a live map of which services call which. It is the fastest way to spot a newly introduced dependency or a chatty service that nobody documented.