Data Model: OTel GenAI Instrumentation Integration¶
Feature: 022-otel-genai-semconv Date: 2026-03-01
Overview¶
This feature introduces no new Pydantic models, database entities, or YAML schema changes. It extends the existing ClaudeBackend class with two instance attributes for lifecycle management and adds one accessor function to the observability module.
Modified Entities¶
ClaudeBackend (class — claude_backend.py)¶
New Instance Attributes:
| Attribute | Type | Default | Description |
|---|---|---|---|
_instrumentor |
ClaudeAgentSdkInstrumentor \| None |
None |
Reference to the active instrumentor instance. Set during _activate_instrumentation() (called from initialize()). Cleared during teardown(). |
State Transitions:
__init__() → _instrumentor=None
initialize() → _instrumentor=instance (if activated)
_instrumentor=None (if skipped)
teardown() → _instrumentor=None
Global Scope Constraint: instrument() monkey-patches process-global functions. Only one active instrumented ClaudeBackend should exist at a time. Current usage is sequential (BackendSelector creates one backend per invocation).
New Private Method:
| Method | Signature | Description |
|---|---|---|
_activate_instrumentation |
(self) -> None |
Activates GenAI instrumentation if the package is installed and observability is enabled. Called from initialize(). Isolated from the main init flow so that return on ImportError or disabled observability only exits this helper. |
ObservabilityContext (dataclass — providers.py)¶
No changes to the dataclass itself. A new module-level accessor is added:
def get_observability_context() -> ObservabilityContext | None:
"""Return the current ObservabilityContext, or None if not initialized.
Thread-safety note: This accessor reads module-level state that is set
by ``initialize_observability()`` in the CLI layer's main thread, before
``asyncio.run()`` is called. All async tasks (including
``ClaudeBackend.initialize()``) run in the same thread, so no
synchronization is needed. If future code introduces background task
spawning that accesses this state, thread synchronization will be
required.
"""
return _observability_context
Existing Entities (Unchanged)¶
| Entity | Location | Why Unchanged |
|---|---|---|
ObservabilityConfig |
models/observability.py |
Existing traces.enabled, traces.capture_content, metrics.enabled fields already carry all information needed by the instrumentor. |
Agent |
models/agent.py |
agent.observability and agent.name already provide config and agent name. |
ClaudeAgentOptions |
Claude Agent SDK | No changes — hooks are injected by the instrumentation package wrapping query(), not through options. |
translate_observability() |
otel_bridge.py |
Subprocess env vars unchanged — coexists with main-process instrumentation. |
External Entities (Owned by Instrumentation Package)¶
These are documented for reference only — HoloDeck does NOT define or modify them:
| Entity | Owner | Description |
|---|---|---|
ClaudeAgentSdkInstrumentor |
otel-instrumentation-claude-agent-sdk |
OTel Instrumentor class. HoloDeck calls instrument()/uninstrument(). |
invoke_agent span |
Instrumentation package | GenAI parent span created per query() call. |
execute_tool span |
Instrumentation package | Child span per tool invocation (via SDK hooks). |
gen_ai.client.token.usage |
Instrumentation package | Token usage histogram metric. |
gen_ai.client.operation.duration |
Instrumentation package | Operation duration histogram metric. |
Dependency Graph¶
pyproject.toml
└── [claude-otel] optional group
└── otel-instrumentation-claude-agent-sdk >= 0.0.3, < 0.1.0
├── opentelemetry-api ~= 1.12 (satisfied by existing deps)
├── opentelemetry-instrumentation (new transitive)
├── opentelemetry-semantic-conventions (new transitive)
├── wrapt >= 1.0.0 (new transitive)
└── claude-agent-sdk >= 0.1.44 (satisfied: pinned at 0.1.44)