Skip to content

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.

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:

Terminal window
pip install opentelemetry-sdk

Framework-specific instrumentation libraries are installed separately — see the Framework Integrations section.

from opentelemetry import trace
from 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.

instrument() does three things:

  1. Installs Scouter as the global OTEL TracerProvider.
  2. Configures Scouter export plus any optional OTEL exporter you pass in.
  3. 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.

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,
)
ParameterTypeDescription
transport_configHttpConfig, GrpcConfig, KafkaConfig, etc.Export transport to the Scouter backend.
exporterHttpSpanExporter, GrpcSpanExporter, etc.Optional exporter for an external OTEL collector.
batch_configBatchConfigControls batch export timing and queue size.
sample_ratiofloatGlobal sampling ratio (0.0–1.0) applied to both Scouter and OTEL exporters.
scouter_queueScouterQueueOptional queue for buffering records alongside spans.
attributesdict[str, Any]Key-value attributes stamped on every span created by this tracer.
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.

instrumentor.is_instrumented # True or False

Returns True if instrument() has been called and the provider is active.

Useful for testing — captures spans in memory instead of exporting them.

MethodDescription
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.

Module-level wrappers around ScouterInstrumentor():

from scouter.tracing import instrument, uninstrument
instrument(transport_config=GrpcConfig(), batch_config=BatchConfig())
# ... later ...
uninstrument()

The recommended OTEL lifecycle is:

  1. instrument() once during process startup.
  2. trace.get_tracer(...) wherever you need a tracer.
  3. 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.

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.

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 ScouterInstrumentor
from 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.

The openai-agents package uses OpenTelemetry natively. After calling instrument(), all agent spans are automatically routed through Scouter.

Terminal window
pip install openai-agents opentelemetry-sdk
from scouter.tracing import ScouterInstrumentor
from scouter import GrpcConfig, BatchConfig
import openai
from agents import Agent, Runner
# Instrument before creating agents
ScouterInstrumentor().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 Scouter

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.

Terminal window
pip install google-adk opentelemetry-sdk
main.py
import asyncio
from google.adk.agents import Agent
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types
from scouter.tracing import ScouterInstrumentor
from 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.

Install opentelemetry-instrumentation-langchain then call instrument() before constructing chains:

Terminal window
pip install opentelemetry-instrumentation-langchain opentelemetry-sdk
from scouter.tracing import ScouterInstrumentor
from scouter import GrpcConfig
from opentelemetry.instrumentation.langchain import LangchainInstrumentor
from langchain_openai import ChatOpenAI
from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
# Register Scouter as the global OTEL provider first
ScouterInstrumentor().instrument(
transport_config=GrpcConfig(),
attributes={"service.name": "langchain-service"},
)
# Then register LangChain instrumentation
LangchainInstrumentor().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 Scouter
result = chain.run(text="OpenTelemetry is a set of APIs and SDKs...")

CrewAI uses OpenTelemetry natively. Register Scouter first and all agent, task, and tool spans flow through automatically.

Terminal window
pip install crewai opentelemetry-sdk
from crewai import Agent, Crew, Task
from scouter.tracing import ScouterInstrumentor
from scouter.transport import GrpcConfig
ScouterInstrumentor().instrument(
transport_config=GrpcConfig(),
attributes={"service.name": "crewai-service"},
)
# Agents and tasks defined after instrument() — spans route through Scouter
researcher = 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)

Use opentelemetry-instrumentation-fastapi alongside Scouter. Register Scouter first so FastAPI spans route through the Scouter provider:

Terminal window
pip install opentelemetry-instrumentation-fastapi opentelemetry-sdk
from contextlib import asynccontextmanager
from fastapi import FastAPI
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from scouter.tracing import ScouterInstrumentor
from scouter import GrpcConfig, BatchConfig
@asynccontextmanager
async 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 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 FastAPI
from 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.

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, Optional
from fastapi import FastAPI
from scouter.tracing import ScouterTracingMiddleware, ScouterInstrumentor
from scouter.transport import GrpcConfig
from opentelemetry import trace as otel_trace
from 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()
@asynccontextmanager
async 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.

For every inbound request, ScouterTracingMiddleware:

  1. Reads the W3C traceparent header (if present)
  2. Opens a SERVER span with parent_span_id set to the upstream caller’s span
  3. Makes that span the active context for the request lifecycle
  4. Records the HTTP method and path on the span
  5. Sets span status to ERROR if the response status is >= 500
  6. 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.

Use local capture to inspect spans produced by your code during tests, without needing a running Scouter backend.

import pytest
from scouter.tracing import ScouterInstrumentor
from 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) > 0

drain_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.