OpenTelemetry Instrumentation Python (v1)
Python-first OpenTelemetry bootstrap for backend services, focused on:
- deterministic startup and shutdown
- explicit feature toggles for tracing, metrics and instrumentations
- strict privacy defaults for database statements and user identifiers
- zero-code startup through the official distro/configurator mechanism
- log correlation only, with no log export pipeline inside this package
Installation
pip install "elven-opentelemetry-instrumentation-py[full]"
Runtime support
v1 supports runtime="python" only.
Passing runtime="serverless" or runtime="edge" raises an explicit error.
Zero-code
export OTEL_SERVICE_NAME="my-service"
export OTEL_SERVICE_VERSION="1.0.0"
elven-otel-instrument python app.py
Disable wrapper initialization explicitly:
export OTEL_ZERO_CODE=false
Kubernetes Operator
Build the Python auto-instrumentation image expected by the OpenTelemetry Operator:
docker build -f Dockerfile.operator -t your-registry/elven-opentelemetry-instrumentation-py:0.1.3 .
Use that image in the Instrumentation resource:
apiVersion: opentelemetry.io/v1alpha1
kind: Instrumentation
metadata:
name: instrumentation
spec:
exporter:
endpoint: "http://opentelemetrycollector.monitoring.svc.cluster.local:4318"
propagators:
- tracecontext
- baggage
- b3
- b3multi
sampler:
type: parentbased_traceidratio
argument: "1"
python:
image: docker.io/elvenobservability/python-k8s-operator:latest
env:
- name: OTEL_PYTHON_DISTRO
value: "elven"
- name: OTEL_PYTHON_CONFIGURATOR
value: "elven"
- name: OTEL_PYTHON_LOG_AUTO_INSTRUMENTATION
value: "false"
- name: OTEL_LOGS_EXPORTER
value: "none"
- name: OTEL_EXPORTER_OTLP_PROTOCOL
value: "http/protobuf"
- name: OTEL_METRICS_EXPORTER
value: "otlp"
- name: OTEL_TRACES_EXPORTER
value: "otlp"
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "http://opentelemetrycollector.monitoring.svc.cluster.local:4318"
This image ships both /autoinstrumentation and /autoinstrumentation-musl, sets
OTEL_PYTHON_DISTRO=elven, sets OTEL_PYTHON_CONFIGURATOR=elven, and keeps logs export disabled by default to match the package contract.
If you prefer split endpoints, keep the same http/protobuf protocol and point both variables to the collector HTTP port:
- name: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
value: "http://opentelemetrycollector.monitoring.svc.cluster.local:4318/v1/traces"
- name: OTEL_EXPORTER_OTLP_METRICS_ENDPOINT
value: "http://opentelemetrycollector.monitoring.svc.cluster.local:4318/v1/metrics"
For Alpine-based workloads, add:
metadata:
annotations:
instrumentation.opentelemetry.io/otel-python-platform: "musl"
Manual initialization
from opentelemetry.sdk.trace.sampling import Decision
from elven_opentelemetry_instrumentation import (
MetricsConfig,
ObservabilityConfig,
PrivacyConfig,
SamplingRule,
ServiceConfig,
TracingConfig,
init_observability,
)
handle = init_observability(
ObservabilityConfig(
service=ServiceConfig(
service_name="payment-service",
service_version="1.0.0",
deployment_environment="production",
),
tracing=TracingConfig(
enabled=True,
rules=[
SamplingRule(
attribute_key="http.route",
attribute_value="/healthz",
decision=Decision.DROP,
),
],
),
metrics=MetricsConfig(enabled=True, export_interval_millis=60_000),
privacy=PrivacyConfig(
redact_db_statement=True,
hash_user_id=True,
),
)
)
handle.tracer.with_span("startup", lambda span: span.set_attribute("boot.ready", True))
handle.metrics.increment("jobs.started", 1)
handle.force_flush()
handle.shutdown()
Sampling
Standard samplers from the environment
OTEL_TRACES_SAMPLER and OTEL_TRACES_SAMPLER_ARG are honoured. Supported values:
OTEL_TRACES_SAMPLER |
OTEL_TRACES_SAMPLER_ARG |
|---|---|
always_on |
ignored |
always_off |
ignored |
traceidratio |
ratio in [0.0, 1.0] |
parentbased_always_on |
ignored |
parentbased_always_off |
ignored |
parentbased_traceidratio |
ratio in [0.0, 1.0] |
An unknown sampler name is ignored with a warning and the default sampler is used. A
missing, unparseable or out-of-range ratio falls back to 1.0 with a warning.
Precedence
TracingConfig(sampler=...)— an explicitSamplerobjectTracingConfig(ratio=...)orTracingConfig(rules=[...])— the built-inSmartSamplerOTEL_TRACES_SAMPLER/OTEL_TRACES_SAMPLER_ARG- Default:
ParentBased(root=SmartSampler(ratio=1.0))
TracingConfig.ratio defaults to None, not 1.0, so "not configured" stays
distinguishable from "configured to 1.0". That is what allows the environment variables to
take effect. Passing ratio=1.0 explicitly still pins the ratio to 1.0.
Injecting your own sampler
Pass the sampler through the public config before initialization:
from opentelemetry.sdk.trace.sampling import ParentBasedTraceIdRatio
init_observability(
ObservabilityConfig(
service=ServiceConfig(service_name="reports", service_version="1.0.0"),
tracing=TracingConfig(enabled=True, sampler=ParentBasedTraceIdRatio(0.1)),
)
)
Do not assign to provider.sampler, provider._sampler or provider._delegate after
startup. TracerProvider.get_tracer() hands its sampler to each Tracer at construction
time, so tracers that already exist keep the old sampler and the swap silently does
nothing for them. Initialization here is idempotent: calling init_observability() again
returns the existing handle without reconfiguring anything. To change the sampler at
runtime, call shutdown() and then init_observability() again, or configure it before
the zero-code bootstrap starts.
Bounding the number of spans in one trace
ELVEN_OTEL_MAX_SPANS_PER_TRACE (or TracingConfig(max_spans_per_trace=...)) caps how
many recording spans a single trace may start in this process. It is disabled by
default — traces stay unbounded unless you opt in.
ELVEN_OTEL_MAX_SPANS_PER_TRACE=500
- The guardrail wraps the effective sampler at the top, so it runs for every span,
including children of an already-sampled parent. A limiter installed as
ParentBased(root=...)would not work:ParentBased.should_sample()routes children of a sampled parent to its parent-sampled delegate and never calls the root sampler. - The delegated decision is evaluated first and never upgraded. Only spans the delegate would record consume budget, and a trace that was not sampled stays unsampled.
- The root span counts against the limit: with
500, a root plus 499 children are recorded and every later span in that trace is dropped. - Dropped spans keep the parent's
tracestate, so W3C trace state still propagates. - A non-positive or unparseable value disables the guardrail.
- The process-local registry is memory-bounded. If it is saturated entirely by active traces, new reservations fail closed instead of evicting an active trace and allowing that trace to grow without protection.
The limit is per process, not distributed. Each Gunicorn/Uvicorn worker, and each service in the request path, keeps its own counter. It is a safety net against a single oversized trace, not a global budget.
It is not an architectural fix. A job that processes many chunks under one HTTP
request should start an independent root trace per chunk — a fresh Context() without the
current parent plus a Link back to the original HTTP context — or move the work to an
async job and return 202. Adding one child span per chunk keeps everything in the same
trace. Tail sampling in the collector reduces how many traces are stored, but it does not
make one sampled, oversized trace safe.
Public API
init_observability(config) returns an ObservabilityHandle with:
tracermetricsshutdown()force_flush()is_started()
The initializer is idempotent and protects against duplicate startup.
Feature precedence
For booleans, precedence is:
- explicit config
- environment variable
- package default
Examples:
OTEL_TRACES_EXPORTER=nonedisables traces by defaultTracingConfig(enabled=True)overrides that defaultOTEL_INSTR_PSYCOPG=falsedisables psycopg instrumentation unless explicitly enabled in config
The same order applies to sampling (TracingConfig.sampler / ratio / rules before
OTEL_TRACES_SAMPLER) and to the span guardrail (TracingConfig.max_spans_per_trace
before ELVEN_OTEL_MAX_SPANS_PER_TRACE, disabled when neither is set).
Supported coverage
- Web: ASGI, WSGI, FastAPI, Flask, Django
- GraphQL: request-level enrichment plus operation and field spans through
graphql-core - HTTP clients: requests, httpx, urllib3, aiohttp-client
- Data: SQLAlchemy, psycopg, psycopg2, pymysql, mysqlclient, Redis, MongoDB
- Workers and queues: Celery, threading, asyncio, gRPC, Kafka, RabbitMQ/Pika, SQS via boto3
- Logging: stdlib logging correlation
Instrumentation is activated only when the target library is installed and enabled.
Development
python -m venv .venv
. .venv/bin/activate
pip install -e ".[dev]"
ruff check .
pyright
pytest
Metadata
Release files for elven-opentelemetry-instrumentation-py 0.1.5
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| elven_opentelemetry_instrumentation_py-0.1.5.tar.gz | 35.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| elven_opentelemetry_instrumentation_py-0.1.5-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 63.6 kB
Release files / elven_opentelemetry_instrumentation_py-0.1.5.tar.gz
| Download URL | elven_opentelemetry_instrumentation_py-0.1.5.tar.gz |
|---|---|
| Size | 35.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
fbad4163c7a088d358daf6f4c6cf6d0489575e441c9586182976ae10cb16e2c7
|
|
BLAKE2b-256 checksum How to use checksums |
23fdf5237c185074e9b3b2ef210caa625cdcdc68a2590d166dface8d0d2bab09
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.16
|
Release files / elven_opentelemetry_instrumentation_py-0.1.5-py3-none-any.whl
| Download URL | elven_opentelemetry_instrumentation_py-0.1.5-py3-none-any.whl |
|---|---|
| Size | 28.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1917b2f1c08e6c4a1ef98337f3058b31f52110b617635f524651caa516a6217f
|
|
BLAKE2b-256 checksum How to use checksums |
d1a2fab711d7e78d5db08cce1ff873c80ac1c70d44709d26157d02780650a861
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.16
|