ScouterInstrumentor
ScouterInstrumentor implements the standard OpenTelemetry BaseInstrumentor interface. Once instrumented, it registers Scouter’s TracerProvider as the global OTEL provider, so any library that calls opentelemetry.trace.get_tracer() routes spans through Scouter automatically.
See the overview comparison table for when to use ScouterInstrumentor vs init_tracer().
For normal application code, this is the recommended tracing entrypoint. Call instrument() once at startup, get tracers from opentelemetry.trace, and shut the provider down once at process exit.
Installation
Section titled “Installation”opentelemetry-sdk is required. If the package is missing, instrument() raises:
ImportError: OpenTelemetry is required for instrumentation.Install with: pip install scouter[opentelemetry] if using scouter, or opsml[opentelemetry] if using opsml.Install the dependency:
pip install opentelemetry-sdkFramework-specific instrumentation libraries are installed separately — see the Framework Integrations section.
Basic Usage
Section titled “Basic Usage”from opentelemetry import tracefrom scouter.tracing import ScouterInstrumentor
instrumentor = ScouterInstrumentor()instrumentor.instrument()
tracer = trace.get_tracer("my-service")Or use the convenience function:
from scouter.tracing import instrument
instrument()ScouterInstrumentor is a singleton. Multiple calls to ScouterInstrumentor() return the same instance. Calling instrument() when already instrumented is a no-op.
What It Installs
Section titled “What It Installs”instrument() does three things:
- Installs Scouter as the global OTEL
TracerProvider. - Configures Scouter export plus any optional OTEL exporter you pass in.
- Makes
trace.get_tracer(...)return a Scouter-backed tracer implementation.
That means manual spans, framework spans, and spans from OTEL-aware libraries all flow through the same provider and share the same context propagation behavior.
API Reference
Section titled “API Reference”instrument()
Section titled “instrument()”instrumentor.instrument( transport_config: Optional[Any] = None, exporter: Optional[Any] = None, batch_config: Optional[BatchConfig] = None, sample_ratio: Optional[float] = None, scouter_queue: Optional[Any] = None, attributes: Optional[Attributes] = None,)| Parameter | Type | Description |
|---|---|---|
transport_config | HttpConfig, GrpcConfig, KafkaConfig, etc. | Export transport to the Scouter backend. |
exporter | HttpSpanExporter, GrpcSpanExporter, etc. | Optional exporter for an external OTEL collector. |
batch_config | BatchConfig | Controls batch export timing and queue size. |
sample_ratio | float | Global sampling ratio (0.0–1.0) applied to both Scouter and OTEL exporters. |
scouter_queue | ScouterQueue | Optional queue for buffering records alongside spans. |
attributes | dict[str, Any] | Key-value attributes stamped on every span created by this tracer. |
uninstrument()
Section titled “uninstrument()”instrumentor.uninstrument()Flushes pending spans, shuts down the TracerProvider, and resets the global OTEL provider. The singleton is also reset, so the next call to ScouterInstrumentor() creates a fresh instance.
Treat tracer handles acquired before uninstrument() as stale. If you re-instrument later, get a fresh tracer from trace.get_tracer(...) instead of reusing an old handle.
is_instrumented
Section titled “is_instrumented”instrumentor.is_instrumented # True or FalseReturns True if instrument() has been called and the provider is active.
Local Capture Methods
Section titled “Local Capture Methods”Useful for testing — captures spans in memory instead of exporting them.
| Method | Description |
|---|---|
enable_local_capture() | Buffer spans locally instead of exporting. |
disable_local_capture() | Stop local capture and discard buffered spans. |
drain_local_spans() | Return and clear all buffered spans as List[TraceSpanRecord]. |
get_local_spans_by_trace_ids(trace_ids) | Return buffered spans matching the given trace IDs without clearing the buffer. |
Convenience Functions
Section titled “Convenience Functions”Module-level wrappers around ScouterInstrumentor():
from scouter.tracing import instrument, uninstrument
instrument(transport_config=GrpcConfig(), batch_config=BatchConfig())# ... later ...uninstrument()Lifecycle Guidance
Section titled “Lifecycle Guidance”The recommended OTEL lifecycle is:
instrument()once during process startup.trace.get_tracer(...)wherever you need a tracer.uninstrument()once during shutdown.
Re-instrumenting within the same process is supported for tests and tightly controlled reconfiguration, but it should not be your normal runtime pattern.
Low-level Alternative
Section titled “Low-level Alternative”If you need to manage tracing without the global OTEL provider, use init_tracer() directly. That path is still useful for low-level tests and manual setups, but it is not the preferred application-facing API anymore.
Default Attributes
Section titled “Default Attributes”The attributes parameter stamps a set of key-value pairs on every span created by the tracer. Use this to tag spans with environment or deployment metadata without modifying application code.
from scouter.tracing import ScouterInstrumentorfrom scouter import GrpcConfig
ScouterInstrumentor().instrument( transport_config=GrpcConfig(), attributes={ "deployment.environment": "production", "service.version": "2.4.1", "team": "ml-platform", },)Every span produced by any OTEL-instrumented library will carry these attributes.
Framework Integrations
Section titled “Framework Integrations”OpenAI Agents SDK
Section titled “OpenAI Agents SDK”The openai-agents package uses OpenTelemetry natively. After calling instrument(), all agent spans are automatically routed through Scouter.
pip install openai-agents opentelemetry-sdkfrom scouter.tracing import ScouterInstrumentorfrom scouter import GrpcConfig, BatchConfigimport openaifrom agents import Agent, Runner
# Instrument before creating agentsScouterInstrumentor().instrument( transport_config=GrpcConfig(), batch_config=BatchConfig(scheduled_delay_ms=500), attributes={"service.name": "openai-agent-service"},)
agent = Agent( name="ResearchAgent", instructions="Search and summarize the given topic.", tools=[...],)
result = Runner.run_sync(agent, "What are the latest advances in transformer architectures?")# Spans from the agent run are exported to ScouterGoogle ADK
Section titled “Google ADK”Google ADK uses OpenTelemetry natively — it calls opentelemetry.trace.get_tracer_provider().get_tracer(name) internally. Calling instrument() before any ADK code is all that’s needed; there’s no separate instrumentation library to register.
pip install google-adk opentelemetry-sdkimport asyncio
from google.adk.agents import Agentfrom google.adk.runners import Runnerfrom google.adk.sessions import InMemorySessionServicefrom google.genai import typesfrom scouter.tracing import ScouterInstrumentorfrom scouter.transport import GrpcConfig
# Sets Scouter as the global OTel TracerProvider before any ADK code runs.# ADK calls get_tracer_provider() internally — it picks this up automatically.ScouterInstrumentor().instrument( transport_config=GrpcConfig(), attributes={"service.name": "adk-hello-agent"},)
def get_current_time(city: str) -> dict: """Returns the current time in a specified city.""" return {"city": city, "time": "10:30 AM"}
root_agent = Agent( model="gemini-2.0-flash", name="hello_agent", description="Tells the current time in a specified city.", instruction="You are a helpful assistant. Use get_current_time to answer questions about the current time.", tools=[get_current_time],)
async def main() -> None: session_service = InMemorySessionService() runner = Runner( agent=root_agent, app_name="hello_app", session_service=session_service, )
session = await session_service.create_session( app_name="hello_app", user_id="user_1", )
message = types.Content( role="user", parts=[types.Part(text="What time is it in New York?")], )
async for event in runner.run_async( user_id="user_1", session_id=session.id, new_message=message, ): if event.is_final_response(): print(event.content.parts[0].text)
ScouterInstrumentor().uninstrument()
if __name__ == "__main__": asyncio.run(main())run_async is an async generator that yields events for each step (tool calls, intermediate responses, the final answer). Filtering on is_final_response() gives you the displayable text. All spans — agent turns, tool invocations, LLM calls — are exported to Scouter automatically.
LangChain
Section titled “LangChain”Install opentelemetry-instrumentation-langchain then call instrument() before constructing chains:
pip install opentelemetry-instrumentation-langchain opentelemetry-sdkfrom scouter.tracing import ScouterInstrumentorfrom scouter import GrpcConfigfrom opentelemetry.instrumentation.langchain import LangchainInstrumentorfrom langchain_openai import ChatOpenAIfrom langchain.chains import LLMChainfrom langchain.prompts import PromptTemplate
# Register Scouter as the global OTEL provider firstScouterInstrumentor().instrument( transport_config=GrpcConfig(), attributes={"service.name": "langchain-service"},)
# Then register LangChain instrumentationLangchainInstrumentor().instrument()
llm = ChatOpenAI(model="gpt-4o-mini")prompt = PromptTemplate.from_template("Summarize the following: {text}")chain = LLMChain(llm=llm, prompt=prompt)
# LangChain spans flow through Scouterresult = chain.run(text="OpenTelemetry is a set of APIs and SDKs...")CrewAI
Section titled “CrewAI”CrewAI uses OpenTelemetry natively. Register Scouter first and all agent, task, and tool spans flow through automatically.
pip install crewai opentelemetry-sdkfrom crewai import Agent, Crew, Taskfrom scouter.tracing import ScouterInstrumentorfrom scouter.transport import GrpcConfig
ScouterInstrumentor().instrument( transport_config=GrpcConfig(), attributes={"service.name": "crewai-service"},)
# Agents and tasks defined after instrument() — spans route through Scouterresearcher = Agent( role="Researcher", goal="Summarize what OpenTelemetry is in one paragraph", backstory="Expert at reading technical documentation", allow_delegation=False, verbose=True,)
task = Task( description="Write a one-paragraph summary of what OpenTelemetry is.", expected_output="A concise paragraph suitable for a developer README.", agent=researcher,)
crew = Crew(agents=[researcher], tasks=[task], verbose=True)result = crew.kickoff()print(result)FastAPI
Section titled “FastAPI”Use opentelemetry-instrumentation-fastapi alongside Scouter. Register Scouter first so FastAPI spans route through the Scouter provider:
pip install opentelemetry-instrumentation-fastapi opentelemetry-sdkfrom contextlib import asynccontextmanagerfrom fastapi import FastAPIfrom opentelemetry.instrumentation.fastapi import FastAPIInstrumentorfrom scouter.tracing import ScouterInstrumentorfrom scouter import GrpcConfig, BatchConfig
@asynccontextmanagerasync def lifespan(app: FastAPI): # Instrument Scouter as the global OTEL provider ScouterInstrumentor().instrument( transport_config=GrpcConfig(), batch_config=BatchConfig(scheduled_delay_ms=200), attributes={ "service.name": "my-api", "deployment.environment": "production", }, ) # Instrument FastAPI — spans from request handling go through Scouter FastAPIInstrumentor.instrument_app(app)
yield
ScouterInstrumentor().uninstrument()
app = FastAPI(lifespan=lifespan)
@app.get("/predict")async def predict(input: str): return {"result": run_model(input)}HTTP request spans from FastAPI will appear in Scouter alongside any manual spans you create inside route handlers.
ScouterTracingMiddleware
Section titled “ScouterTracingMiddleware”ScouterTracingMiddleware is a Starlette-compatible middleware that extracts W3C traceparent context from inbound HTTP requests and opens a SERVER span as the root for each request. Route handlers get that span as their active context, so any spans created inside are correctly nested as children — no per-endpoint header parsing required.
The middleware works with any Starlette-based framework (FastAPI, Starlette, etc.).
from fastapi import FastAPIfrom scouter.tracing import ScouterTracingMiddleware
app = FastAPI(lifespan=lifespan)app.add_middleware(ScouterTracingMiddleware, tracer=tracer)tracer should be the tracer returned by otel_trace.get_tracer("name") after instrument() has been called. If you are on the low-level manual path, a directly constructed ScouterTracer also works.
Deferred initialization
Section titled “Deferred initialization”FastAPI evaluates add_middleware() at import time, before the lifespan event runs. If your tracer is set during lifespan, you need a proxy:
from typing import Any, Optionalfrom fastapi import FastAPIfrom scouter.tracing import ScouterTracingMiddleware, ScouterInstrumentorfrom scouter.transport import GrpcConfigfrom opentelemetry import trace as otel_tracefrom contextlib import asynccontextmanager
class _TracerProxy: """Forwards attribute access to the real tracer once lifespan sets _inner."""
def __init__(self) -> None: self._inner: Optional[Any] = None
def __getattr__(self, name: str) -> Any: if self._inner is None: raise RuntimeError("Tracer not initialized — instrument() must run first") return getattr(self._inner, name)
_instrumentor = ScouterInstrumentor()_tracer = _TracerProxy()
@asynccontextmanagerasync def lifespan(app: FastAPI): _instrumentor.instrument( transport_config=GrpcConfig(), attributes={"service.name": "my-service"}, ) _tracer._inner = otel_trace.get_tracer("my-service")
yield
_instrumentor.uninstrument() _tracer._inner = None
app = FastAPI(lifespan=lifespan)
# add_middleware runs at import time — _tracer is a proxy here,# not yet initialized. That's fine: it only resolves _inner at request time.app.add_middleware(ScouterTracingMiddleware, tracer=_tracer)This is the same pattern used in the distributed tracing examples.
What the middleware produces
Section titled “What the middleware produces”For every inbound request, ScouterTracingMiddleware:
- Reads the W3C
traceparentheader (if present) - Opens a
SERVERspan withparent_span_idset to the upstream caller’s span - Makes that span the active context for the request lifecycle
- Records the HTTP method and path on the span
- Sets span status to
ERRORif the response status is>= 500 - Closes and exports the span when the response is sent
If no traceparent header is present, the middleware opens a root SERVER span instead — so uninstrumented callers still get traced.
Local Span Capture (Testing)
Section titled “Local Span Capture (Testing)”Use local capture to inspect spans produced by your code during tests, without needing a running Scouter backend.
import pytestfrom scouter.tracing import ScouterInstrumentorfrom scouter import GrpcConfig
@pytest.fixture(autouse=True)def scouter_local(): """Set up ScouterInstrumentor with local span capture for tests.""" inst = ScouterInstrumentor() inst.instrument(transport_config=GrpcConfig()) inst.enable_local_capture() yield inst inst.disable_local_capture() inst.uninstrument()
def test_agent_produces_search_span(scouter_local): run_my_agent("What is the weather in NYC?")
spans = scouter_local.drain_local_spans() span_names = [s.name for s in spans] assert "web_search" in span_names
def test_filter_by_trace_id(scouter_local): trace_id = run_my_agent_and_return_trace_id("query")
matched = scouter_local.get_local_spans_by_trace_ids([trace_id]) assert len(matched) > 0drain_local_spans() clears the buffer. get_local_spans_by_trace_ids() reads without clearing — useful when multiple tests share a buffer or when you want to inspect a specific trace within a larger batch.