Instrumentation And Traces¶
Autobench supports four collection styles that can be mixed in one run:
- Explicit
RunContextandSpancalls inside a task. - Lightweight method instrumentation for existing application classes.
- Trace-envelope adapters for an external agent or workflow runtime.
- Native Pydantic AI, OpenAI, OpenAI Agents, and HTTPX instrumentors configured from Python or YAML.
OpenTelemetry is not a core dependency. Future OTLP bridges can export Autobench spans, but the evidence model remains owned by Autobench.
See Native Instrumentation for the typed fluent API, YAML DSL, compatibility doctor, privacy defaults, layered traces, and provider examples.
ABP is the native collection protocol underneath these APIs. It owns signal ordering, task-local context, capture policy, instrumentation scope, trace materialization, and compatibility diagnostics. Instrumentors emit ABP evidence directly; they do not create OpenTelemetry spans and then convert them back into Autobench records.
Manual Spans¶
from autobench import DurationMetricSpec, Semantic, SpanKind
def run_case(ctx, case):
with ctx.span(
"support_agent",
kind=SpanKind.AGENT,
input=case.input,
duration_metric=DurationMetricSpec(
name="agent_latency",
semantic_type=Semantic.TIME_LATENCY,
unit="ms",
),
) as agent:
result = call_agent(case.input)
agent.set_output(result)
agent.outcome(result["ok"])
return result
Span duration is calculated when the context manager closes. Nested spans preserve parent-child relationships and retain evidence emitted before an exception.
Span Kinds¶
SpanKind includes:
- agent
- LLM
- tool
- retriever
- parser
- workflow
- custom
Kinds are semantic selectors, not restrictions. A domain can use custom kinds and tags while generic agentic scorers continue selecting standard spans.
Method Instrumentation¶
instrument_method is the high-level helper for one class method. It records evidence only while
a RunContext is active:
from autobench import InstrumentMetricSpec, Semantic, instrument_method
handle = instrument_method(
SearchClient,
"search",
span="search.request",
metrics=[
InstrumentMetricSpec(
name="result_count",
semantic_type="retrieval.result_count",
value_factory=lambda call: len(call.result),
),
InstrumentMetricSpec(
name="request_count",
semantic_type="llm.requests",
value_path="result.usage.requests",
),
],
)
try:
run_benchmark()
finally:
handle.close()
Instrumentation supports:
- instance, static, class, and inherited methods;
- synchronous and asynchronous calls;
- iterators and generators, including
send,throw, and early close; - asynchronous iterators and generators, including
asend,athrow, andaclose; - synchronous and asynchronous context managers.
The wrapper preserves the original descriptor, callable signature, return value, exception identity, and lazy streaming behavior. A stream span ends when the stream actually completes, fails, times out, or closes, so its duration is not merely the time required to construct an iterator.
value_factory is the typed Python extraction seam. It receives an InstrumentCall containing
the bound instance, arguments, result, error, stream item count, and last stream item. value_path
is the declarative alternative for trusted attribute, mapping, and zero-argument accessor paths.
Autobench does not execute arbitrary YAML expressions.
Extraction and lifecycle callback errors are recorded as evidence or compatibility diagnostics. They do not replace the application's result or exception.
The returned InstrumentationHandle is also a context manager and restores the original method on
close.
Scoped Suppression¶
Instrumentation can be suppressed for the current task without changing global process state:
from autobench import suppress_instrumentation
with suppress_instrumentation("search.client"):
result = client.search("internal health check")
Suppression keys can identify an instrumentor or an operation family. Unrelated instrumentors stay
active, nested scopes compose, and context tokens are reset even when application code raises. An
empty suppress_instrumentation() scope suppresses all ABP instrumentation in the current task.
Native Instrumentors¶
Reusable SDK integrations implement the Instrumentor contract:
from autobench import (
AbstractionLayer,
CaptureMechanism,
Compatibility,
InstrumentationHandle,
InstrumentationRuntime,
InstrumentorInfo,
)
class ClientInstrumentor:
info = InstrumentorInfo(
id="example.client",
version="1.0.0",
target_distribution="example-client",
supported_versions=">=2,<3",
mechanism=CaptureMechanism.HOOK,
layer=AbstractionLayer.CLIENT,
span_kinds=("client.request",),
semantic_families=("request", "response"),
)
def check(self) -> Compatibility:
return Compatibility.compatible()
def install(self, runtime: InstrumentationRuntime) -> InstrumentationHandle:
unsubscribe = register_native_callback(...)
return InstrumentationHandle(unsubscribe, info=self.info)
Install instrumentors directly through one manager when building a custom integration:
from autobench import InstrumentationManager
with InstrumentationManager() as manager:
compatibility = manager.check(ClientInstrumentor())
if compatibility.installable:
manager.install(ClientInstrumentor())
run_benchmark()
InstrumentorInfo declares stable identity, target package and version range, mechanism, layer,
semantic families, source convention, optional dependencies, and sync/async/streaming/native-hook
capabilities. Compatibility distinguishes compatible, degraded, unavailable, unsupported, and
conflicting installations. Missing or incompatible optional dependencies degrade only the feature
that needs them; a missing required target package prevents installation.
Installing the same instrumentor version twice increments an owner reference count instead of installing duplicate hooks. Closing the final handle unregisters native callbacks or restores the exact patched descriptor. Competing owners can instrument the same method independently, while an external wrapper replacement produces a conflict diagnostic instead of being overwritten.
Mechanisms should be selected in this order:
- stable native processor or callback;
- stable native wrapper/decorator extension point;
- public method patch;
- explicitly version-pinned private method patch;
- unsupported with a compatibility diagnostic.
Application benchmarks normally use the higher-level lifecycle owner instead:
from autobench import Benchmark, HTTPXInstrumentation, OpenAIInstrumentation
benchmark = Benchmark("chat").instrument(
OpenAIInstrumentation(),
HTTPXInstrumentation(),
)
result = benchmark.run()
Benchmark.instrument(...) installs configured and custom instrumentors before any matrix item,
keeps them active through concurrent runs and streams, and closes them after execution.
Trace Envelopes¶
Adapters can normalize a completed external trace into TraceEnvelope:
from autobench import TraceEnvelope, attach_trace
trace = TraceEnvelope(
trace_id="trace-42",
name="checkout-agent",
input={"cart_id": "c1"},
output={"status": "complete"},
spans=tuple(converted_spans),
attributes={"framework": "custom-agent-runtime"},
)
attach_trace(ctx, trace)
attach_trace preserves spans and errors and projects known usage, model, provider, duration, and
outcome fields into semantic observations. Large native trace payloads should be written as an
artifact and referenced by raw_artifact.
Pydantic AI Usage¶
Install the optional native instrumentor when the application uses Pydantic AI:
pip install 'autobench[pydantic-ai]'
The integration uses Pydantic AI's public capability hooks and only injects its capability while an Autobench run is active:
from autobench import Benchmark, PydanticAIInstrumentation
experiment = benchmark.instrument(PydanticAIInstrumentation()).run()
No manual span or metric calls are required. The instrumentor captures:
- agent runs and streamed execution;
- model requests, requested and response model identities, providers, and direct usage;
- tool argument validation, execution, retry, failure, approval, and deferred control flow;
- structured-output validation;
- first-chunk latency, partial streams, failures, and normal completion;
- tracked prompt, tool, and output-schema versions;
- multimodal metadata, with binary references only when the capture policy requests full content.
The instrumentor composes with user event handlers and Pydantic AI's own
Instrumentation capability. It does not configure, replace, or require
OpenTelemetry. Supported Autobench 0.1.x builds pin the public integration seam
to Pydantic AI 2.22.x; InstrumentationManager.check() reports incompatible
versions before installing hooks.
Application outputs and exceptions are passed through unchanged. Aggregate agent usage and direct model usage retain distinct accounting scopes, and cost remains a downstream derivation. Replaying recorded ABP evidence does not require Pydantic AI to be installed.
See the live Pydantic AI example for a tool-using, structured-output, streaming benchmark with a retry path.
Usage Bridge¶
Pydantic AI usage can be normalized without importing Pydantic AI into core:
from autobench import PydanticAIUsage, record_pydantic_ai_usage
record_pydantic_ai_usage(
ctx,
PydanticAIUsage(
requests=1,
input_tokens=420,
output_tokens=83,
model_name="gemini-3-flash-preview",
provider="openrouter",
),
)
The bridge emits canonical LLM token, model, and provider observations that pricing derivation and reports can consume.
Trace Extraction And Accounting¶
Instrumentors record immutable facts. Extractors turn a completed ABP trace into semantic observations without mutating that trace:
from autobench import (
CompositeExtractor,
SignalExtractor,
SpanExtractor,
UsageExtractor,
replay_extraction,
)
extractor = CompositeExtractor(
SignalExtractor(),
SpanExtractor(),
UsageExtractor(),
)
derived_record = replay_extraction(record, extractor)
The extractors have separate ownership:
SignalExtractorreconstructs measurements and events and preserves their accounting scope, abstraction layer, logical operation ID, and instrumentor identity.SpanExtractorderives generic operation counts, direct durations, maximum depth and fan-out, critical-path makespan, parallelism, incomplete work, retry/recovery, validation, approval, tool-call, message-growth, and reference evidence.UsageExtractorderives LLM request, token, requested-model, response-model, and provider evidence. It never derives cost.
Every extractor has a stable name and version. Replay records both in extraction evidence and RunRecord lineage. Replaying a newer version replaces observations owned by the older version in the derived record; the parent record remains unchanged.
Direct And Aggregate Evidence¶
ABP keeps all raw measurements but prevents framework/client nesting from inflating totals:
- Aggregate parent measurements are never added to direct child measurements.
- Usage totals select one abstraction layer per semantic, preferring client evidence before framework, application, and transport evidence.
- Equivalent direct operations with a shared logical operation ID are counted once.
- Equal equivalent values are deduplicated. Conflicting values require a unique explicit
authority; unresolved conflicts produce
ambiguous_direct_measurementand are excluded from the derived total. - Aggregate values are retained as validation evidence. A disagreement with the direct total
produces
aggregate_measurement_mismatch. - Requested and response model identities remain separate factors.
Reports and ObservationQuery.first_exact() prefer an accounting-safe aggregate summary over
same-source per-operation direct evidence. Raw and projected queries can still inspect every
underlying observation.
Graph timing uses monotonic timestamps only. time.critical_path is the observed trace makespan,
and operation.parallelism is completed leaf work divided by that makespan. Invalid or partial
clock evidence is retained through diagnostics rather than repaired with wall-clock subtraction.
Adapter Boundary¶
Core instrumentation intentionally does not know Pydantic AI, OpenAI Agents, LangChain, DSPy, or OpenTelemetry internals. An integration should:
- Collect from the framework's stable hooks.
- Convert native calls or traces into Autobench spans and observations.
- Store large raw payloads as artifacts.
- Keep native dependencies optional.
This boundary lets applications use existing instrumentation while RunRecords remain portable.