Metadata-Version: 2.5
Name: elven-opentelemetry-instrumentation-py
Version: 0.1.5
Summary: Universal OpenTelemetry bootstrap for Python services with elite-quality defaults, privacy, auto-instrumentation, traces, and metrics.
Project-URL: Homepage, https://github.com/elven-observability/opentelemetry-instrumentation-py
Project-URL: Repository, https://github.com/elven-observability/opentelemetry-instrumentation-py
Author: Leonardo Zwirtes
License: Apache-2.0
Keywords: instrumentation,metrics,observability,opentelemetry,python,tracing
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: System :: Monitoring
Requires-Python: >=3.10
Requires-Dist: opentelemetry-api<1.41.0,>=1.40.0
Requires-Dist: opentelemetry-distro<0.62.0,>=0.61b0
Requires-Dist: opentelemetry-exporter-otlp<1.41.0,>=1.40.0
Requires-Dist: opentelemetry-instrumentation-aiohttp-client<0.62.0,>=0.61b0
Requires-Dist: opentelemetry-instrumentation-asgi<0.62.0,>=0.61b0
Requires-Dist: opentelemetry-instrumentation-asyncio<0.62.0,>=0.61b0
Requires-Dist: opentelemetry-instrumentation-boto3sqs<0.62.0,>=0.61b0
Requires-Dist: opentelemetry-instrumentation-celery<0.62.0,>=0.61b0
Requires-Dist: opentelemetry-instrumentation-django<0.62.0,>=0.61b0
Requires-Dist: opentelemetry-instrumentation-fastapi<0.62.0,>=0.61b0
Requires-Dist: opentelemetry-instrumentation-flask<0.62.0,>=0.61b0
Requires-Dist: opentelemetry-instrumentation-grpc<0.62.0,>=0.61b0
Requires-Dist: opentelemetry-instrumentation-httpx<0.62.0,>=0.61b0
Requires-Dist: opentelemetry-instrumentation-kafka-python<0.62.0,>=0.61b0
Requires-Dist: opentelemetry-instrumentation-logging<0.62.0,>=0.61b0
Requires-Dist: opentelemetry-instrumentation-pika<0.62.0,>=0.61b0
Requires-Dist: opentelemetry-instrumentation-psycopg2<0.62.0,>=0.61b0
Requires-Dist: opentelemetry-instrumentation-psycopg<0.62.0,>=0.61b0
Requires-Dist: opentelemetry-instrumentation-pymongo<0.62.0,>=0.61b0
Requires-Dist: opentelemetry-instrumentation-pymysql<0.62.0,>=0.61b0
Requires-Dist: opentelemetry-instrumentation-redis<0.62.0,>=0.61b0
Requires-Dist: opentelemetry-instrumentation-requests<0.62.0,>=0.61b0
Requires-Dist: opentelemetry-instrumentation-sqlalchemy<0.62.0,>=0.61b0
Requires-Dist: opentelemetry-instrumentation-threading<0.62.0,>=0.61b0
Requires-Dist: opentelemetry-instrumentation-urllib3<0.62.0,>=0.61b0
Requires-Dist: opentelemetry-instrumentation-wsgi<0.62.0,>=0.61b0
Requires-Dist: opentelemetry-instrumentation<0.62.0,>=0.61b0
Requires-Dist: opentelemetry-propagator-b3<1.41.0,>=1.40.0
Requires-Dist: opentelemetry-sdk<1.41.0,>=1.40.0
Provides-Extra: dev
Requires-Dist: aiohttp>=3.12.0; extra == 'dev'
Requires-Dist: boto3>=1.42.0; extra == 'dev'
Requires-Dist: build>=1.4.0; extra == 'dev'
Requires-Dist: celery>=5.6.0; extra == 'dev'
Requires-Dist: django>=5.2.0; extra == 'dev'
Requires-Dist: fastapi>=0.115.0; extra == 'dev'
Requires-Dist: flask>=3.1.0; extra == 'dev'
Requires-Dist: graphql-core>=3.2.8; extra == 'dev'
Requires-Dist: grpcio>=1.76.0; extra == 'dev'
Requires-Dist: hatchling>=1.29.0; extra == 'dev'
Requires-Dist: httpx>=0.28.0; extra == 'dev'
Requires-Dist: kafka-python>=2.2.0; extra == 'dev'
Requires-Dist: loguru>=0.7.0; extra == 'dev'
Requires-Dist: pika>=1.3.0; extra == 'dev'
Requires-Dist: psycopg2-binary>=2.9.0; extra == 'dev'
Requires-Dist: psycopg[binary]>=3.2.0; extra == 'dev'
Requires-Dist: pymongo>=4.14.0; extra == 'dev'
Requires-Dist: pymysql>=1.1.0; extra == 'dev'
Requires-Dist: pyright>=1.1.408; extra == 'dev'
Requires-Dist: pytest-asyncio>=1.3.0; extra == 'dev'
Requires-Dist: pytest-cov>=7.1.0; extra == 'dev'
Requires-Dist: pytest>=9.0.0; extra == 'dev'
Requires-Dist: redis>=7.4.0; extra == 'dev'
Requires-Dist: requests>=2.32.0; extra == 'dev'
Requires-Dist: ruff>=0.15.0; extra == 'dev'
Requires-Dist: sqlalchemy>=2.0.0; extra == 'dev'
Requires-Dist: strawberry-graphql>=0.312.0; extra == 'dev'
Requires-Dist: structlog>=25.5.0; extra == 'dev'
Requires-Dist: urllib3>=2.5.0; extra == 'dev'
Requires-Dist: uvicorn>=0.30.0; extra == 'dev'
Provides-Extra: full
Requires-Dist: aiohttp>=3.12.0; extra == 'full'
Requires-Dist: boto3>=1.42.0; extra == 'full'
Requires-Dist: celery>=5.6.0; extra == 'full'
Requires-Dist: django>=5.2.0; extra == 'full'
Requires-Dist: fastapi>=0.115.0; extra == 'full'
Requires-Dist: flask>=3.1.0; extra == 'full'
Requires-Dist: graphql-core>=3.2.8; extra == 'full'
Requires-Dist: grpcio>=1.76.0; extra == 'full'
Requires-Dist: httpx>=0.28.0; extra == 'full'
Requires-Dist: kafka-python>=2.2.0; extra == 'full'
Requires-Dist: pika>=1.3.0; extra == 'full'
Requires-Dist: psycopg2-binary>=2.9.0; extra == 'full'
Requires-Dist: psycopg[binary]>=3.2.0; extra == 'full'
Requires-Dist: pymongo>=4.14.0; extra == 'full'
Requires-Dist: pymysql>=1.1.0; extra == 'full'
Requires-Dist: redis>=7.4.0; extra == 'full'
Requires-Dist: requests>=2.32.0; extra == 'full'
Requires-Dist: structlog>=25.5.0; extra == 'full'
Requires-Dist: urllib3>=2.5.0; extra == 'full'
Description-Content-Type: text/markdown

# 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

```bash
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

```bash
export OTEL_SERVICE_NAME="my-service"
export OTEL_SERVICE_VERSION="1.0.0"
elven-otel-instrument python app.py
```

Disable wrapper initialization explicitly:

```bash
export OTEL_ZERO_CODE=false
```

## Kubernetes Operator

Build the Python auto-instrumentation image expected by the OpenTelemetry Operator:

```bash
docker build -f Dockerfile.operator -t your-registry/elven-opentelemetry-instrumentation-py:0.1.3 .
```

Use that image in the `Instrumentation` resource:

```yaml
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:

```yaml
- 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:

```yaml
metadata:
  annotations:
    instrumentation.opentelemetry.io/otel-python-platform: "musl"
```

## Manual initialization

```python
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

1. `TracingConfig(sampler=...)` — an explicit `Sampler` object
2. `TracingConfig(ratio=...)` or `TracingConfig(rules=[...])` — the built-in `SmartSampler`
3. `OTEL_TRACES_SAMPLER` / `OTEL_TRACES_SAMPLER_ARG`
4. 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:

```python
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.

```bash
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:

- `tracer`
- `metrics`
- `shutdown()`
- `force_flush()`
- `is_started()`

The initializer is idempotent and protects against duplicate startup.

## Feature precedence

For booleans, precedence is:

1. explicit config
2. environment variable
3. package default

Examples:

- `OTEL_TRACES_EXPORTER=none` disables traces by default
- `TracingConfig(enabled=True)` overrides that default
- `OTEL_INSTR_PSYCOPG=false` disables 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

```bash
python -m venv .venv
. .venv/bin/activate
pip install -e ".[dev]"
ruff check .
pyright
pytest
```
