OpenTelemetry metrics + span events for keel-llm-reliability. A pure wrapper around ResilientClient — emits per-attempt metrics and span events from the visible-degradation trail. No coupling: keel-llm-reliability stays observability-free.
Project description
keel-llm-otel
OpenTelemetry metrics + span events for
keel-llm-reliability. A pure external wrapper aroundResilientClient— emits signals from the visible-degradation trail after each call.keel-llm-reliabilityitself stays observability-free.
Part of the Keel toolkit. Composes cleanly: drop in over any ResilientClient instance, configure your OTel SDK however you like, see your multi-model LLM calls light up in Jaeger / Datadog / Honeycomb / Grafana / wherever.
Is this for you?
Adopt when — you run keel-llm-reliability in production with OpenTelemetry already in your stack and want per-attempt metrics + span events flowing into your existing tracing/metrics backend without writing the bridge yourself.
Skip when — you're not on OTel; you're prototyping (the visible Attempt trail in return values is plenty for development); you want a different backend (write your own thin wrapper over ResilientClient — the pattern here is ~80 LOC).
Install
pip install keel-llm-otel
# you bring your own OTel SDK + exporter, e.g.:
# pip install opentelemetry-sdk opentelemetry-exporter-otlp
Three seamless paths — pick one
Path 1 — Full distributed tracing (recommended) — [full] extra
One install, complete distributed tracing of LLM calls — reliability-layer metrics + span events and per-provider HTTP-level child spans with traceparent propagated to providers. Auto-instrumentor + opentelemetry-instrumentation-httpx activate together.
pip install 'keel-llm-otel[full]' # everything wired
opentelemetry-instrument python yourapp.py # discovers + activates automatically
Why
[full]is recommended: distributed tracing requires HTTP-level instrumentation to injecttraceparentheaders into outgoing provider calls. Installing onlykeel-llm-otel[auto]gives correct reliability-layer metrics + events but silently omits per-call child spans and trace propagation —[full]eliminates that failure mode.
Path 1b — Auto-instrument only (reliability layer, no HTTP spans) — [auto] extra
For consumers who already use opentelemetry-instrumentation-httpx elsewhere, or who want the lighter dep set (no wrapt transitively from httpx instrumentation):
pip install 'keel-llm-otel[auto]' # adds opentelemetry-instrumentation + wrapt
opentelemetry-instrument python yourapp.py # discovers + activates automatically
Or programmatically — one line at startup:
from keel_llm_otel import OTelInstrumentor
OTelInstrumentor().instrument() # call once; ResilientClient is patched globally
Your existing code is unchanged:
from keel_llm_reliability import ResilientClient, Request
client = ResilientClient([...])
result = await client.failover(req) # metrics + span events emitted automatically
Path 2 — Drop-in subclass (one-word code change) — lean install
pip install keel-llm-otel (no extras needed) gets you a ResilientClient subclass with emission built in. When you want explicit, per-instance control (multi-tenant meters, test isolation, custom Tracer):
- from keel_llm_reliability import ResilientClient
+ from keel_llm_otel import OTelObservableClient as ResilientClient
Same constructor, same methods, same return types — OTelObservableClient is a ResilientClient:
import asyncio
from keel_llm_otel import OTelObservableClient as ResilientClient
from keel_llm_adapter_openai import OpenAIAdapter
from keel_llm_reliability import Request
from keel_llm_protocol import user
client = ResilientClient([ # same call as before
OpenAIAdapter(model="llama-3.3-70b-versatile", api_key="gsk_…",
base_url="https://api.groq.com/openai/v1", provider="groq"),
OpenAIAdapter(model="llama-3.1-8b", base_url="http://localhost:11434/v1",
provider="local"),
])
async def main() -> None:
result = await client.failover(Request(messages=[user("Hello.")]))
# ↑ identical behaviour; metrics + span events emitted as a side-effect.
asyncio.run(main())
Both paths produce identical signals; both use OTel's no-op meter/tracer when nothing is configured (zero overhead, zero failure).
Path 3 — Direct emission (decoupled from keel-llm-reliability's orchestrator)
If you have your own multi-model orchestrator (in-tree fan-out / failover code), you can still emit on the same OTel vocabulary — Signals.emit(...) accepts any iterable of AttemptLike (a structural Protocol: anything with model_key / outcome / latency_ms / error). No keel-llm-reliability adoption required.
from keel_llm_otel import Signals
# Build your own attempt records (any shape with the four fields above):
attempts = [
MyAttempt(model_key="gpt-4o", outcome="success", latency_ms=120, error=None),
MyAttempt(model_key="claude-sonnet", outcome="deferred_backpressure",
latency_ms=45, error=MyError(category="backpressure")),
]
Signals().emit(strategy="my_orchestrator", attempts=attempts,
succeeded=True, degraded=True)
Useful when adopting Keel's full orchestrator isn't the right move (you have working in-tree machinery), but you still want unified OTel emission across consumers in your portfolio.
What gets emitted
Configure your OTel SDK (exporters, resource attributes, sampling) however you normally would — this package only produces the signals.
Metrics
| Name | Type | Labels | What it measures |
|---|---|---|---|
keel.llm.reliability.attempts |
Counter | strategy, gen_ai.request.model, outcome, error_category |
One per Attempt — count every provider interaction by disposition. |
keel.llm.reliability.attempt_duration_ms |
Histogram (ms) | strategy, gen_ai.request.model, outcome |
Latency per provider interaction. |
keel.llm.reliability.calls |
Counter | strategy, succeeded, degraded |
One per reliability call — track end-to-end outcome. |
(Counter names follow modern OTel convention — no _total suffix; the Prometheus exporter adds it automatically. The model-name label uses OTel's incubating GenAI convention gen_ai.request.model so reliability-layer dispositions can be joined with adapter-level gen_ai.* metrics in cross-layer queries.)
strategy is "failover" or "fan_out". outcome is one of success / preempted_open / preempted_limited / deferred_backpressure / empty / failed. error_category is the failed attempt's error.category (backpressure / transient / terminal), or "none" when the attempt didn't carry an error.
Span events
If the caller has a current span (the standard pattern — wrap your reliability call in your own span), each Attempt becomes a keel.attempt event on that span with attributes keel.strategy, gen_ai.request.model, keel.outcome, keel.latency_ms, keel.error_category:
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("llm.call") as span:
result = await client.failover(req)
# span now carries one `keel.attempt` event per provider interaction —
# visible in your tracing UI alongside whatever else you put on it.
Design notes
- Pure wrapper, no coupling.
keel-llm-reliabilitydoesn't depend on OTel; this package wraps it externally. Want a different backend (Prometheus, Datadog APM, structured-log emitter)? Write your own ~80-LOC wrapper overResilientClient— theAttempttrail is the contract. - Post-call emission only. v0 emits signals after the wrapped call returns, using
latency_msfrom eachAttempt. No new spans are started. Proper async span-context propagation into provider HTTP calls belongs in adapter-level HTTP instrumentation (e.g.opentelemetry-instrumentation-httpx), not in a reliability-layer wrapper. - Emission errors are swallowed. Observability must never break the underlying call. If a meter or tracer raises, the wrapped call's result is still returned.
- No global-state assumptions. Pass a specific
Meter/Tracerfor tests / multi-tenant; or omit and it picks up the global providers your SDK configured.
Status
0.1.0 — 0.x while the API stabilizes through year one (breaking changes possible at minor bumps, documented in the CHANGELOG; pin exact versions).
The Keel toolkit
Composable, vendor-neutral LLM reliability libraries on PyPI:
keel-llm-reliability · keel-llm-protocol · keel-llm-adapter-openai · keel-llm-adapter-anthropic · keel-llm-adapter-google · keel-circuit-breaker · keel-llm-otel
MIT licensed.
Project details
Release history Release notifications | RSS feed
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 keel_llm_otel-0.1.0.tar.gz.
File metadata
- Download URL: keel_llm_otel-0.1.0.tar.gz
- Upload date:
- Size: 14.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b27aefb8dda19e8c302c8d1203ee8f85a1ec647b051d0c24852e0084ae26ed92
|
|
| MD5 |
900c0a42afb906befaf65bf0d11d3f7e
|
|
| BLAKE2b-256 |
64f834bfb29d675656a93d123acfd3f86c54878434f5e8be5414052ec3862a7c
|
Provenance
The following attestation bundles were made for keel_llm_otel-0.1.0.tar.gz:
Publisher:
publish-py.yml on keelplatform/keel
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
keel_llm_otel-0.1.0.tar.gz -
Subject digest:
b27aefb8dda19e8c302c8d1203ee8f85a1ec647b051d0c24852e0084ae26ed92 - Sigstore transparency entry: 1615202350
- Sigstore integration time:
-
Permalink:
keelplatform/keel@931ec32c4d96b63dd910c02378f0097130f044b0 -
Branch / Tag:
refs/tags/py-llm-otel-v0.1.0 - Owner: https://github.com/keelplatform
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-py.yml@931ec32c4d96b63dd910c02378f0097130f044b0 -
Trigger Event:
push
-
Statement type:
File details
Details for the file keel_llm_otel-0.1.0-py3-none-any.whl.
File metadata
- Download URL: keel_llm_otel-0.1.0-py3-none-any.whl
- Upload date:
- Size: 12.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7927838b3ceb640692538e77013764669f0d5ec1e0070e781b0422200fd27093
|
|
| MD5 |
5a1282c8bf35abe0b6e89c86c46ee5a6
|
|
| BLAKE2b-256 |
f5fe4a513f2bdbf1b1a7c6160b9e3f1e81c096fe35d1a8a18fee5795c04281bb
|
Provenance
The following attestation bundles were made for keel_llm_otel-0.1.0-py3-none-any.whl:
Publisher:
publish-py.yml on keelplatform/keel
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
keel_llm_otel-0.1.0-py3-none-any.whl -
Subject digest:
7927838b3ceb640692538e77013764669f0d5ec1e0070e781b0422200fd27093 - Sigstore transparency entry: 1615202353
- Sigstore integration time:
-
Permalink:
keelplatform/keel@931ec32c4d96b63dd910c02378f0097130f044b0 -
Branch / Tag:
refs/tags/py-llm-otel-v0.1.0 - Owner: https://github.com/keelplatform
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-py.yml@931ec32c4d96b63dd910c02378f0097130f044b0 -
Trigger Event:
push
-
Statement type: