OpenTelemetry GenAI integration
Status: stable as of v0.5 (#224) Extra:
pip install 'contextweaver[otel]'
contextweaver emits OpenTelemetry spans and metrics that conform to the
official OpenTelemetry GenAI Semantic Conventions. Modern
agent-observability platforms — Laminar, Phoenix, Langfuse,
LangSmith — speak gen_ai.* natively, so contextweaver activity renders
as agent / tool spans rather than generic ones.
Install
pip install 'contextweaver[otel]'
The extra pulls opentelemetry-api>=1.27, opentelemetry-sdk>=1.27, and
opentelemetry-semantic-conventions>=0.48b0. The core install
(pip install contextweaver) does not pull these — the integration is
strictly opt-in.
Wire the hook
from contextweaver.context.manager import ContextManager
from contextweaver.extras.otel import OTelEventHook
hook = OTelEventHook(service_name="my-agent")
mgr = ContextManager(hook=hook)
Every mgr.build_sync(...) and Router.route(...) call now emits the
appropriate GenAI span automatically. See
contextweaver/extras/otel.py for the full method set.
Span shapes
| Span name | When emitted | Attributes |
|---|---|---|
invoke_agent |
One per ContextManager.build() |
gen_ai.system="contextweaver", gen_ai.operation.name="invoke_agent", gen_ai.usage.input_tokens (= BuildStats.prompt_tokens), plus contextweaver.phase, contextweaver.candidates.* (engine-specific). |
execute_tool |
One per Router.route() |
gen_ai.system="contextweaver", gen_ai.operation.name="execute_tool", gen_ai.tool.name (the rank-1 candidate), plus contextweaver.routing.candidate_count and contextweaver.routing.candidate_ids (full ranked list). |
contextweaver.context.firewall |
One per firewall interception | contextweaver.firewall.reason, contextweaver.item.kind. |
contextweaver.context.exclude |
One per exclusion batch | contextweaver.exclude.reason, contextweaver.exclude.count. |
The two gen_ai.* spans are the load-bearing ones for downstream
platforms; the contextweaver.* spans are engine-specific audit detail
that is not yet covered by upstream SemConv.
Metric shapes
| Metric name | Type | Attributes |
|---|---|---|
gen_ai.client.token.usage |
histogram | gen_ai.operation.name, gen_ai.token.type="input", gen_ai.system="contextweaver" |
contextweaver.firewall.interceptions |
counter | contextweaver.firewall.reason |
contextweaver.items.excluded |
counter | contextweaver.exclude.reason |
contextweaver.budget.exceeded |
counter | contextweaver.budget.requested, contextweaver.budget.limit |
contextweaver.routing.candidates |
histogram | gen_ai.operation.name, gen_ai.system |
Worked example — Phoenix
Phoenix consumes OTLP and renders invoke_agent / execute_tool spans
as collapsible agent traces. A minimal wiring:
from phoenix.otel import register
from contextweaver.context.manager import ContextManager
from contextweaver.extras.otel import OTelEventHook
from contextweaver.types import ContextItem, ItemKind, Phase
register(project_name="my-agent", endpoint="http://localhost:6006/v1/traces")
hook = OTelEventHook(service_name="my-agent")
mgr = ContextManager(hook=hook)
mgr.ingest(
ContextItem(id="u1", kind=ItemKind.user_turn, text="find open invoices")
)
pack = mgr.build_sync(phase=Phase.answer, query="open invoices")
# Phoenix UI now shows a single `invoke_agent` span with prompt_tokens
# under `gen_ai.usage.input_tokens` and `contextweaver.phase=answer`.
Worked example — Laminar
Laminar auto-imports gen_ai.* spans and renders them in its agent
timeline. Wire OTLP the same way:
import os
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
provider = TracerProvider()
provider.add_span_processor(
BatchSpanProcessor(
OTLPSpanExporter(
endpoint="https://api.lmnr.ai/v1/traces",
headers={"Authorization": f"Bearer {os.environ['LMNR_PROJECT_API_KEY']}"},
)
)
)
trace.set_tracer_provider(provider)
# Now any contextweaver build / route call emits via the configured
# exporter under the `contextweaver` service name.
Privacy guidance
The default emission does not include raw query strings, full tool
descriptions, or any args_schema content — those can carry sensitive
payloads in some tool catalogs. Reference: OpenTelemetry GenAI Tracing
AI Agents Without Leaking PII.
To opt into experimental attributes (e.g. raw-prompt content under
gen_ai.prompt), pass otel_emit_experimental=True at construction
time. Only enable it when the observability backend is trusted to
handle PII appropriately.
# Off by default — PII-safe:
hook = OTelEventHook(service_name="my-agent")
# Opt-in when running on a trusted backend with redaction in place:
hook = OTelEventHook(service_name="my-agent", otel_emit_experimental=True)
SemConv version note
The GenAI Semantic Conventions are still flagged Development status
upstream. contextweaver imports them via the
opentelemetry.semconv._incubating.attributes.gen_ai_attributes path,
which is the OTel project's convention for unstable surfaces. When
upstream graduates the conventions, the import path will change but the
emitted attribute names are spec-stable (e.g. gen_ai.system,
gen_ai.operation.name) — your dashboards will keep working.