This release is a pre-release and may not be stable for production use.
agnara-telemetry
Explicit OpenTelemetry metrics and span bridges for Agnara's execution hooks. This distribution is built and versioned with the synchronized workspace set. Which versions exist on an index is answered by its PyPI project page, not by this file: a README ships inside the artifact and cannot describe the state of a publication that happens after it is built.
It imports agnara and opentelemetry-api, never a sibling adapter or the SDK.
Composition
The application installs/configures its chosen SDK and supplies a meter:
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import InMemoryMetricReader
from agnara.execution import ExecutionPlan
from agnara_telemetry import OpenTelemetryMetricsHook
# In-memory example: no network exporter or global provider installation.
reader = InMemoryMetricReader()
provider = MeterProvider(metric_readers=[reader], shutdown_on_exit=False)
try:
hook = OpenTelemetryMetricsHook(provider.get_meter("agnara_telemetry"))
plan = ExecutionPlan.compile(definition, registry, hooks=[hook])
# Invoke plans during your application's owned lifecycle.
# provider.force_flush() may be called by the application when needed.
finally:
provider.shutdown()
definition and registry are the application's declared capability and DI
registry. Reuse one hook across plans; duplicate hook registration intentionally
duplicates measurements. A supplied NoOpMeterProvider().get_meter(...) works
without SDK setup. The package neither selects a global provider nor owns
flush, shutdown, background workers, network endpoints or exporter credentials.
Measurements
| Instrument | Kind | Unit |
|---|---|---|
agnara.invocation.count |
Counter | 1 |
agnara.invocation.duration |
Histogram | s |
Each terminal callback records one count and the core elapsed duration,
converted from monotonic nanoseconds to seconds. The start callback stores
nothing. Attributes are restricted to agnara.capability.id and
agnara.invocation.outcome. Outcomes are success, failure, timeout and
cancellation; unrecognized direct events map to unknown. Negative elapsed
durations are rejected before recording. Use static capability IDs to avoid
unbounded metric cardinality.
No tracking IDs, arguments, return values, exception details or principal data are added. Applications still control SDK resource attributes, exemplars, readers and exporters, so review their full export independently. Metric names are custom Agnara names, not a protocol or GenAI semantic-convention claim.
Counts describe terminal deliveries, not every attempted invocation. Calls
rejected before core emits a start event are excluded. An explicitly returned
canonical Failure counts as a successful Python return in the existing core
event semantics. Ordinary instrument errors are isolated by the core runtime;
a failed instrument may leave partial measurements. There are no retries or
exactly-once export promises.
Ownership and validation
The hook retains only instrument handles; nested/overlapping invocations and repeated tracking IDs need no correlation map. The supplied meter must support concurrent, synchronous, non-blocking recording. Do not mutate instruments while plans are active. A custom blocking instrument blocks invocation.
Development tests use the pinned OpenTelemetry API/SDK 1.44.0 with an in-memory
reader and an in-memory span exporter:
uv run pytest tests/telemetry tests/architecture. Tests cover exact
values/units, attributes, span names, status mapping, parent/child nesting,
released correlation state, redaction, runtime outcomes, nested and concurrent
execution, thread sharing, API no-op behavior and application-owned lifecycle.
They do not establish free-threading compatibility, network exporter
conformance, screening of application-added SDK data or performance
superiority.
Spans
OpenTelemetryTracingHook(tracer) opens one span per invocation and ends it on
the matching terminal event:
from opentelemetry.sdk.trace import TracerProvider
from agnara_telemetry import OpenTelemetryTracingHook
provider = TracerProvider(shutdown_on_exit=False)
# The application adds its own span processor and exporter here.
try:
hook = OpenTelemetryTracingHook(provider.get_tracer("agnara_telemetry"))
plan = ExecutionPlan.compile(definition, registry, hooks=[hook])
finally:
provider.shutdown()
The span is named after the capability, is INTERNAL, and carries only
agnara.capability.id and agnara.invocation.outcome. success sets OK;
failure and timeout set ERROR with the outcome word as the description;
cancellation is recorded but left UNSET, because the caller withdrew rather
than the capability failing. No exception text, argument, result, principal,
transport field or tracking ID is attached, and no span events are recorded.
Pairing uses the runtime's invocation_id, never a caller tracking_id. That
identity is not exported: it would be unbounded cardinality on every span, and
the span ID already identifies the span.
A nested invocation becomes a child span, because the started span is attached to the OpenTelemetry context; invocations in sibling tasks are unrelated, because each task holds its own copy of that context. Register the hook at most once per plan — a second registration opens a second span for one invocation. Delivering events to this hook by hand, out of order, is outside the runtime's nesting guarantee: the spans still end, but the previous context is not restored.
Metrics and span hooks compose on one plan and can be registered together.
Joining a caller's trace
Agnara reads no propagation header. A capability span is parented by whatever OpenTelemetry context is current when the invocation starts, so it joins a caller's distributed trace exactly when the application has propagated that caller's context — typically with the ASGI or client instrumentation the application already runs.
The consequence worth stating plainly: a request carrying traceparent still
produces an unlinked root span if nothing extracted it. Linking is opt-in.
This is a deliberate boundary, not an omission. Honouring a caller-supplied
traceparent lets that caller choose the trace identity their operation is
recorded under, which is reasonable inside a trusted perimeter and rarely so at
an untrusted edge. That judgment belongs to a deployment, not to a framework
default. See proposed ADR 0056.
Verified over the real HTTP dispatcher and the real MCP invoker: with
propagation the capability span carries the caller's trace ID and parent span;
without it, a root span. Malformed headers are inert rather than fatal, and an
unknown traceparent version links, because W3C requires forward compatibility
and forbids only ff.
Semantic conventions
Agnara emits agnara.-namespaced names plus exactly one OpenTelemetry
convention attribute: error.type, on a span whose outcome is failure or
timeout, carrying that same outcome word. It is the only stable convention
attribute that applies to a capability invocation. It is not an exception type:
exception text is never exported, and the convention asks for a low-cardinality
identifier. A cancellation carries none, because the caller withdrew rather
than the capability failing. Metric attributes are unchanged.
The GenAI and MCP vocabularies are not emitted. gen_ai.operation.name with
the value execute_tool would assert that every capability is a tool, which is
false for an HTTP request made by a human, and the tool call argument and result
attributes are payloads this package never exports. Both vocabularies are also
still incubating.
If your application genuinely serves capabilities as MCP tools and you want a
backend's GenAI views, add mcp.* and gen_ai.* at your MCP layer, where a
tool identity and a method name actually exist. A transport-neutral capability
span is the wrong place to claim them.
Two tests check every attribute both hooks emit against the installed
opentelemetry-semantic-conventions package: each must be agnara.-namespaced
or stable, and no incubating name may appear. See
proposed ADR 0057.
No-op benchmarks remain E9.6. See proposed ADR 0054 and proposed ADR 0055.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file agnara_telemetry-0.1.0a8.tar.gz.
File metadata
- Download URL: agnara_telemetry-0.1.0a8.tar.gz
- Upload date:
- Size: 10.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d9e304dfc536ed23dd777e31dea8a1246339b94a4eb5d1fd5a51b6b41e5e1522
|
|
| MD5 |
84d32ff3c123708315ba1980c8b9d6b0
|
|
| BLAKE2b-256 |
c02793d080ff984ee14fe2f25d59118b5b2671c0c89b1637206db995c5790c04
|
Provenance
The following attestation bundles were made for agnara_telemetry-0.1.0a8.tar.gz:
Publisher:
release.yml on Blandskron/agnara
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
agnara_telemetry-0.1.0a8.tar.gz -
Subject digest:
d9e304dfc536ed23dd777e31dea8a1246339b94a4eb5d1fd5a51b6b41e5e1522 - Sigstore transparency entry: 2770599322
- Sigstore integration time:
-
Permalink:
Blandskron/agnara@02c53bdafbc9a8da06c8e52bc6357d8ed5c0c695 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Blandskron
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@02c53bdafbc9a8da06c8e52bc6357d8ed5c0c695 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file agnara_telemetry-0.1.0a8-py3-none-any.whl.
File metadata
- Download URL: agnara_telemetry-0.1.0a8-py3-none-any.whl
- Upload date:
- Size: 12.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
512f4fc8f0eac596f1d659a71d5a6231896dd49b0ca6d98630d21fffa383dceb
|
|
| MD5 |
8ef0cd290a9882da9d2ba01368d6ab2b
|
|
| BLAKE2b-256 |
c76dbce6a325055adfdda0a8bdd7f201171ad096f4e661a4ce7ec58348620926
|
Provenance
The following attestation bundles were made for agnara_telemetry-0.1.0a8-py3-none-any.whl:
Publisher:
release.yml on Blandskron/agnara
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
agnara_telemetry-0.1.0a8-py3-none-any.whl -
Subject digest:
512f4fc8f0eac596f1d659a71d5a6231896dd49b0ca6d98630d21fffa383dceb - Sigstore transparency entry: 2770599745
- Sigstore integration time:
-
Permalink:
Blandskron/agnara@02c53bdafbc9a8da06c8e52bc6357d8ed5c0c695 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Blandskron
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@02c53bdafbc9a8da06c8e52bc6357d8ed5c0c695 -
Trigger Event:
workflow_dispatch
-
Statement type: