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 strstr 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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

neva_otel-0.2.0.tar.gz (141.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

neva_otel-0.2.0-py3-none-any.whl (36.8 kB view details)

Uploaded Python 3

File details

Details for the file neva_otel-0.2.0.tar.gz.

File metadata

  • Download URL: neva_otel-0.2.0.tar.gz
  • Upload date:
  • Size: 141.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.8.22

File hashes

Hashes for neva_otel-0.2.0.tar.gz
Algorithm Hash digest
SHA256 d3a17647bbd63d2c3c9a2ec3f48968af232828a675ab701d2ca9213bc2735a49
MD5 0b9d84c08e1b562b2672e59c68dc4b06
BLAKE2b-256 a97a1ebad5859035ec2d05ebbb1bc7584015230970227bd363a112972f19d2d9

See more details on using hashes here.

File details

Details for the file neva_otel-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: neva_otel-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 36.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.8.22

File hashes

Hashes for neva_otel-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5dddc87d8d57130be7c0ca0358439ec56f2f08a1983261f18855b3c78f6e747a
MD5 28e2ff0b909153eee6150788ed3db2fa
BLAKE2b-256 1827c5b52fb2ca1f5a159cc6307174c11b3d07f1fb3928f6b057e0e25a7982f7

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 files

0.1.0

2 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