ADR-010: importlib Probe for Optional OpenTelemetry Integration¶
Date: 2026-06-18
Status: Accepted
Deciders: ExecutionKit core team
Technical Story: The team wanted to emit OpenTelemetry spans around LLM calls when opentelemetry-api is installed, without adding a mandatory runtime dependency or scattering try/except ImportError guards throughout the codebase.
Context and Problem Statement¶
ExecutionKit's zero-runtime-dependency constraint (ADR-004) prohibits
unconditional imports of any package that is not part of the Python standard
library. At the same time, users running OTel-instrumented services benefit
from having llm.call spans in their distributed traces without any extra
wrapper code.
The integration mechanism had to satisfy three properties simultaneously.
First, importing executionkit.observability must not raise ImportError
when opentelemetry-api is absent. Second, when OTel is present, spans must
be emitted with zero changes to call sites — the llm_span context manager
must work identically with or without the SDK. Third, the probe must be
evaluated once, not re-evaluated on every span entry, to keep the hot path
overhead negligible.
Decision Drivers¶
- Zero runtime dependency —
opentelemetry-apiis an optional extra only. - Call sites must be identical whether or not OTel is installed.
- The probe must not re-run on every
llm_spancall. mypy --strictmust pass — no possibly-unbound module-level names.
Considered Options¶
- Option A:
try/except ImportErrorguard per call site - Option B:
importlib.util.find_specprobe at module load; local imports insideif _OTEL_AVAILABLE - Option C: Separate opt-in module (
executionkit.otel) that the caller imports explicitly
Decision Outcome¶
Chosen option: Option B (importlib.util.find_spec probe at module load).
At the top of executionkit/observability.py, a single line probes for the
package without binding any module-level name:
llm_span is a @contextmanager. When _OTEL_AVAILABLE is False it yields
None immediately — a no-op. When True, it performs a local import of
opentelemetry.trace and starts a real span. The local import keeps
mypy --strict satisfied (no possibly-unbound names at module scope) and
means the OTel import machinery runs only for users who have the SDK installed.
@contextmanager
def llm_span(model: str) -> Generator[Any, None, None]:
if not _OTEL_AVAILABLE:
yield None
return
from opentelemetry import trace as otel_trace # local import
tracer = otel_trace.get_tracer("executionkit")
with tracer.start_as_current_span("llm.call") as span:
span.set_attribute("llm.model", model)
yield span
Positive Consequences¶
importlib.utilis in the standard library — no new dependency.- The probe runs once at module load; the hot path is a single boolean check.
- Call sites are identical:
with llm_span(model) as span:works whether or not OTel is installed. mypy --strictis satisfied because OTel names only appear inside branches guarded by_OTEL_AVAILABLE.
Negative Consequences¶
find_specprobes at import time; ifopentelemetryis installed afterexecutionkit.observabilityis first imported (unusual in production but possible in some test setups), the flag will be stale. A process restart is required.
Pros and Cons of the Options¶
Option A: try/except ImportError per call site¶
- Good: Familiar Python idiom.
- Bad: Repeated boilerplate at every call site.
- Bad:
ImportErroris caught at runtime on every call rather than once at module load — unnecessary overhead. - Bad: Scattered try/except blocks make it hard to audit which sites participate in OTel instrumentation.
Option B: importlib.util.find_spec probe at module load¶
- Good: Single probe evaluated once; boolean check on the hot path.
- Good: All instrumented sites follow the same pattern — easy to audit.
- Good: Satisfies
mypy --strictvia local imports inside guarded branches. - Bad: Stale flag if OTel is installed after the module is first imported.
Option C: Separate opt-in module¶
- Good: Completely explicit — users import
executionkit.otelto opt in. - Bad: Requires callers to change their import paths when adding instrumentation, which is friction for gradual adoption.
- Bad: Patterns would need to import from a different module when OTel is desired, splitting the implementation.