Skip to main content

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 around ResilientClient — emits signals from the visible-degradation trail after each call. keel-llm-reliability itself 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 inject traceparent headers into outgoing provider calls. Installing only keel-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 typesOTelObservableClient 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-reliability doesn'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 over ResilientClient — the Attempt trail is the contract.
  • Post-call emission only. v0 emits signals after the wrapped call returns, using latency_ms from each Attempt. 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 / Tracer for tests / multi-tenant; or omit and it picks up the global providers your SDK configured.

Status

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


Download files

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

Source Distribution

keel_llm_otel-0.1.0.tar.gz (14.5 kB view details)

Uploaded Source

Built Distribution

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

keel_llm_otel-0.1.0-py3-none-any.whl (12.0 kB view details)

Uploaded Python 3

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

Hashes for keel_llm_otel-0.1.0.tar.gz
Algorithm Hash digest
SHA256 b27aefb8dda19e8c302c8d1203ee8f85a1ec647b051d0c24852e0084ae26ed92
MD5 900c0a42afb906befaf65bf0d11d3f7e
BLAKE2b-256 64f834bfb29d675656a93d123acfd3f86c54878434f5e8be5414052ec3862a7c

See more details on using hashes here.

Provenance

The following attestation bundles were made for keel_llm_otel-0.1.0.tar.gz:

Publisher: publish-py.yml on keelplatform/keel

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for keel_llm_otel-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7927838b3ceb640692538e77013764669f0d5ec1e0070e781b0422200fd27093
MD5 5a1282c8bf35abe0b6e89c86c46ee5a6
BLAKE2b-256 f5fe4a513f2bdbf1b1a7c6160b9e3f1e81c096fe35d1a8a18fee5795c04281bb

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page