Skip to main content

neva-otel

OpenTelemetry tracing and metrics for the Neva framework.

python-neva ships logging only and has no OpenTelemetry dependency at all — not even opentelemetry-api. This package is the backend: the SDK, the tracer and meter providers, the exporters, the samplers, the resource, and the instrumentors. Everything that commits an application to a destination lives here.

The split follows the line OpenTelemetry itself draws, and it mirrors Laravel, where the framework ships Log and every APM is a separate package. The consequence worth preserving: a team that prefers something else installs nothing from here and pays nothing.

Install

uv add neva-otel            # console exporters
uv add 'neva-otel[otlp]'    # + OTLP over gRPC      (:4317)
uv add 'neva-otel[otlp-http]'  # + OTLP over HTTP   (:4318)

Enable it

Add the provider to config/providers.py:

from neva.otel import OtelServiceProvider

config = {"providers": [OtelServiceProvider]}

…and write config/otel.py:

from neva.otel import OtelConfig

config: OtelConfig = {
    "enabled": True,
    "service": {"name": "billing", "version": "2.1.0"},
    "traces": {
        "exporter": "otlp",
        "sampler": "parent_ratio",
        "ratio": 0.1,
        "otlp": {"endpoint": "http://collector:4317", "insecure": True},
    },
    "metrics": {"exporter": "otlp", "interval": 60_000},
}

enabled defaults to false. Installing this package and adding no config changes nothing — no provider, no exporter, and no instrumentation that would cost engine setup and export nowhere.

service.name falls back to the core's app.name, and environment to app.environment, so {"enabled": True} on its own already yields a correct resource.

Configuration

Key Values Default
enabled master switch false
service name, namespace, version, instance_id from app.name
environment deployment.environment.name from app.environment
resource extra attributes, merged last —
traces.enabled overrides enabled for traces enabled
traces.exporter console, otlp, otlp_http, memory, none console
traces.sampler always_on, always_off, parent_ratio parent_ratio
traces.ratio head-sampling ratio, clamped to [0, 1] 1.0
traces.otlp endpoint, headers, timeout, insecure from OTEL_EXPORTER_OTLP_*
metrics.enabled overrides enabled for metrics enabled
metrics.exporter as for traces; selects the reader carrying it console
metrics.interval / timeout milliseconds 60000 / 30000
propagators tracecontext, baggage both
instrumentation.sqlalchemy patch engine creation tracing is on
logging.enabled span ids on log records tracing is on

Unset OTLP keys are omitted rather than passed as None, so the exporters' own OTEL_EXPORTER_OTLP_* environment handling still applies.

An unknown sampler, exporter or propagator raises at boot with a message naming it. Application.register_providers discards the Result a provider's register() returns, so an error there would boot an application with no telemetry and nothing saying why.

Tracing

from neva.otel import Trace

with Trace.span("checkout", order_id=42) as span:
    span.set_attribute("currency", "EUR")

With tracing off the same call records nothing, so call sites need no guard.

Metrics

from neva.otel import Metric

Metric.counter("orders.placed").add(1)
Metric.histogram("checkout.duration", unit="ms").record(elapsed)
Metric.up_down_counter("queue.depth").add(-1)
Metric.gauge("pool.size").set(7)

Instruments are cached by name and kind, so asking for one at the call site that records it is correct.

Log correlation

trace_id, span_id and trace_sampled land on every log record emitted inside a recording span, on every channel, via the core's LogManager.processor seam. Records emitted outside a span carry none, rather than zeroed ids that would match a search for a trace that does not exist.

Propagation

from neva.otel import continued, inject

headers = inject()          # producer
with continued(headers):    # consumer — attached, so spans parent onto it
    ...

The carrier is any str → str mapping. W3C Trace Context is not web-specific: the spec defines the format of two fields and HTTP headers are one binding, so a message envelope's header bag carries a trace across a queue just as well.

extract alone returns a Context that still has to be attached — use continued, or activated(extract(carrier)).

SQLAlchemy

Instrumentation is installed from the provider's register(), globally, with no engine= argument. That patches engine creation, so every connection in database.connections is traced, along with any engine registered later.

Three constraints make that the only correct shape, and each was measured:

  • register(), not lifespan(). The database provider's lifespan builds every configured engine, and base providers' lifespans enter first — a plugin lifespan is already too late to patch creation.
  • No engine=. Instrumenting one engine leaves every other connection silent.
  • Guarded on is_instrumented_by_opentelemetry. The instrumentor is a process-wide singleton whose instrument() is not idempotent, so a second application in one interpreter is a no-op rather than a warning.

There is one hazard this package cannot fix from here: it works because neva/database/manager.py reaches create_async_engine through the asyncio module at call time. Rewritten as from sqlalchemy.ext.asyncio import create_async_engine, the local name would hold the unpatched original and every engine would be created untraced, silently. tests/test_sqlalchemy.py::TestTheCoreImportHazard fails if that ever changes.

Testing

# conftest.py
pytest_plugins = ["neva.testing.fixtures", "neva.otel.testing"]
from neva.otel.testing import clear_spans, counter_value, span_named

clear_spans(self.app)
with Trace.span("checkout", order_id=42):
    pass

assert span_named(self.app, "checkout").attributes["order_id"] == 42
assert counter_value(self.app, "orders.placed") == 1

Configure traces.exporter: "memory" with sampler: "always_on" — neva.otel.testing.IN_MEMORY is exactly that configuration, and write_config(config_dir) writes it out. The memory exporter is driven by a SimpleSpanProcessor, so a span is readable the moment it ends, with no flush and no race against a batch worker.

captured_spans and captured_metrics are function-scoped fixtures over the core's application fixture.

Develop

uv sync --all-extras
poe lint && poe fmt && poe tc && poe test

Commits follow Conventional Commits with gitmoji via cz commit; releases are cut with cz bump.

Metadata

Release files for neva-otel 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for neva-otel 0.3.0
File Size Uploaded
neva_otel-0.3.0.tar.gz 153.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for neva-otel 0.3.0
File Interpreter ABI Platform
neva_otel-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 197.3 kB

Release files / neva_otel-0.3.0.tar.gz

Download URL neva_otel-0.3.0.tar.gz
Size 153.8 kB
Tags Source
SHA-256 checksum
How to use checksums
49c57e6482d04a35b000ab2c64b8ec77fa7251f565a3b33eb91aa30e0e8e8cc2
BLAKE2b-256 checksum
How to use checksums
fe3e9e1d7e482be79fb527bba002d38cc3e5da2968eac0f6a6618152d5e9c165
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.8.22

Release files / neva_otel-0.3.0-py3-none-any.whl

Download URL neva_otel-0.3.0-py3-none-any.whl
Size 43.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e0a8eac5b0fae0f4e3570d1872da85a7613dd38efab661a6040c4c190f29c9da
BLAKE2b-256 checksum
How to use checksums
d424f3685f1f68d4716d580c0e91433ea4e288f2f511d0e48d516179774607e1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.8.22

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page