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.1.0.tar.gz (141.5 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.1.0-py3-none-any.whl (36.8 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for neva_otel-0.1.0.tar.gz
Algorithm Hash digest
SHA256 210d0ed97eaab7a931bc7e44c87b40128f15d85bf7ba5e3877954907a98f3880
MD5 54ad520b373c9c54654beea6264a66ea
BLAKE2b-256 1fca7b4a99c5d48318c6a1c0f0191c49fcd5e148c4f649f9dd3f0a638d2c1e81

See more details on using hashes here.

File details

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

File metadata

  • Download URL: neva_otel-0.1.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.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 131b2427479804ab178b5f9cda885a4017614161043fe977c39d172c2f363dd7
MD5 db4f3c978256d4b41a30c318cb5a67ea
BLAKE2b-256 50fa0d71b69159d370eeb7ed2e55c56a0c9a4dbf7fa645f8823fb17e23adc142

See more details on using hashes here.

Release history Release notifications | RSS feed

0.2.0

2 files

This release

0.1.0 This release

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