Tracing & instrumentation
Send traces over plain OTLP/HTTP with OpenInference instrumentors — CrewAI, DSPy, Pydantic AI, OpenAI Agents, LlamaIndex, or bare providers. No proprietary SDK.
langprobe ingests plain OTLP/HTTP at POST <host>/v1/traces. Every
integration is the same shape: install an OpenInference instrumentor for your
framework plus the stock opentelemetry-exporter-otlp-proto-http exporter,
wire them into a TracerProvider, point the endpoint at your langprobe host, and
set the API-key header. There is no langprobe-specific runtime library.
The endpoint accepts both application/x-protobuf (what the stock exporter
sends) and application/json. Protobuf works out of the box — don't force JSON.
The fast path: the Python SDK
If you're in Python, the langprobe SDK wires the provider, exporter, and instrumentor for you in four lines:
import langprobe
langprobe.init(
api_key="lt_<public_id>.<secret>",
endpoint="https://app.langprobe.com",
instrumentations=["crewai"],
)
# your framework code runs unchanged; traces flow to /v1/tracesInstall the extras you need: pip install "langprobe[otel,crewai]" (also
dspy, pydantic-ai, openai-agents, llama-index, openai, anthropic,
litellm). See the Python SDK for per-framework
examples.
The manual path: OpenTelemetry directly
Any language with an OTLP exporter can send traces — no SDK required. The shared bootstrap (Python shown) wires a provider once:
# langprobe_tracing.py
import os
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.resources import Resource
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
def build_provider(service_name: str) -> TracerProvider:
endpoint = os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"].rstrip("/") + "/v1/traces"
headers = {"Authorization": f"Bearer {os.environ['LANGPROBE_API_KEY']}"}
provider = TracerProvider(resource=Resource.create({"service.name": service_name}))
provider.add_span_processor(
BatchSpanProcessor(OTLPSpanExporter(endpoint=endpoint, headers=headers))
)
trace.set_tracer_provider(provider)
return providerPoint it at your host (the bootstrap appends /v1/traces):
export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:7080"
export LANGPROBE_API_KEY="lt_<public_id>.<secret>"Scripts must flush before exit or the last batch is lost:
provider.force_flush(); provider.shutdown(). Servers build the provider once
at startup and flush/shutdown once at shutdown (FastAPI lifespan / atexit) —
never per request.
Framework recipes
Install the framework's OpenInference instrumentor and call .instrument() with
the provider. Worked example — CrewAI:
from langprobe_tracing import build_provider
from openinference.instrumentation.crewai import CrewAIInstrumentor
from openinference.instrumentation.openai import OpenAIInstrumentor
provider = build_provider("crew-app")
CrewAIInstrumentor().instrument(tracer_provider=provider)
OpenAIInstrumentor().instrument(tracer_provider=provider) # provider match!
# ... run your crew ...
provider.force_flush(); provider.shutdown()| Framework | Instrumentor package |
|---|---|
| CrewAI | openinference-instrumentation-crewai (+ your LLM provider's instrumentor) |
| DSPy | openinference-instrumentation-dspy |
| Pydantic AI | openinference-instrumentation-pydantic-ai |
| OpenAI Agents | openinference-instrumentation-openai-agents |
| LlamaIndex | openinference-instrumentation-llama-index |
| Bare OpenAI / Anthropic | openinference-instrumentation-openai / -anthropic |
| LiteLLM-routed | openinference-instrumentation-litellm |
CrewAI dispatches the actual LLM call through the provider SDK. The CrewAI
instrumentor gives you the Agent/Task/Crew structure, but the llm-kind span
(model + tokens) comes from the provider instrumentor — match it to your
crewai.LLM(model=...). Installing only the CrewAI instrumentor is the #1
papercut: you get the tree but no model or tokens.
Custom spans
langprobe has no proprietary decorator API — you emit plain OpenTelemetry spans with OpenInference conventions. Reach for this only for code an instrumentor doesn't cover (a hand-rolled tool, a custom retriever, a guardrail):
from opentelemetry import trace
tracer = trace.get_tracer("my-agent")
with tracer.start_as_current_span("web_search") as span:
span.set_attribute("openinference.span.kind", "TOOL") # UPPERCASE
span.set_attribute("input.value", query)
results = search_api.search(query)
span.set_attribute("output.value", results)start_as_current_span nests the span under whatever span is active, so the run
tree in /runs follows your call structure. langprobe classifies the span from
openinference.span.kind first, then OTel gen_ai.operation.name, then a
span-name heuristic — see Core concepts for the
kind set.
End-user identity
Attribute a run to an end user by setting enduser.id on the root span (or
langprobe.identify(...) via the SDK). langprobe reads it so you can filter and
group runs by the user they served.
Once traces are landing, the debugger loop begins: Replay an edit and diff what changed.