Tracing for Python
Python apps export traces to Railway with the OpenTelemetry Python distribution. Its opentelemetry-instrument command wraps your start command and activates instrumentation for the frameworks and libraries you have installed, so most apps need no code changes.
Before you start, enable tracing for the service. Railway adds the OTEL_* variables on the next deploy, and the SDK reads them.
Install the SDK
Install the distribution and the OTLP exporter, then let the bootstrap command detect your libraries:
pip install opentelemetry-distro opentelemetry-exporter-otlp
opentelemetry-bootstrap -a installopentelemetry-bootstrap looks at the packages in your environment and installs the matching instrumentation packages, for example opentelemetry-instrumentation-fastapi or opentelemetry-instrumentation-psycopg2. Run it locally and add the packages it installs to requirements.txt (or your pyproject.toml), so they're installed during the Railway build. Run opentelemetry-bootstrap without -a install to print the list instead of installing it.
Set the start command
Prefix the start command of the service with opentelemetry-instrument. For a FastAPI app served by Uvicorn:
opentelemetry-instrument uvicorn main:app --host 0.0.0.0 --port $PORTFor a Flask app served by Gunicorn:
opentelemetry-instrument gunicorn app:app --bind 0.0.0.0:$PORTFor a Django project served by Gunicorn:
opentelemetry-instrument gunicorn mysite.wsgi --bind 0.0.0.0:$PORTThe Django instrumentation needs the DJANGO_SETTINGS_MODULE variable set on the service, for example mysite.settings.
opentelemetry-instrument configures the SDK from environment variables. Besides the variables Railway provides, set these two on the service. The distribution exports metrics by default, and Railway's receiver accepts traces only:
OTEL_METRICS_EXPORTER=none
OTEL_LOGS_EXPORTER=noneFrameworks
FastAPI, Starlette, Flask, and Django are instrumented by their packages. Each incoming request produces a server span named after the method and route, with http.route and http.response.status_code attributes. The instrumentation reads the traceparent header the edge sends, so the span joins the edge's trace.
Outgoing calls made with requests, httpx, urllib3, or aiohttp produce client spans and carry the trace context to the callee. Database clients such as psycopg2, asyncpg, SQLAlchemy, redis, and pymongo produce client spans with the statement or command.
Add a custom span
Wrap the work you care about in your own span with the OpenTelemetry API:
from opentelemetry import trace
tracer = trace.get_tracer("checkout")
def calculate_total(cart):
with tracer.start_as_current_span("calculate-total") as span:
span.set_attribute("cart.items", len(cart.items))
return price_items(cart)The span becomes a child of the request span that's active when the function runs. An exception raised inside the with block marks the span as an error and records the exception as an event.
Verify
- Deploy the service and send it a few requests.
- Open the Traces page. In the Tracing setup panel, the App indicator for the service turns green when its first span arrives.
- Open a trace. The request's server span and any child spans appear under the edge and proxy spans.
If spans from the app don't arrive, see Troubleshooting. Set OTEL_LOG_LEVEL=debug on the service to have the SDK log its configuration and export attempts.