
Langsmith Trace
- 54 installs
- 111 repo stars
- Updated July 29, 2026
- langchain-ai/skills-benchmarks
Helps with ai & agent building tasks.
About
langsmith-trace is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- langsmith-trace
- AI & Agent Building
- AI-coding skill
Langsmith Trace by the numbers
- 54 all-time installs (skills.sh)
- +2 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #6,946 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/langchain-ai/skills-benchmarks --skill langsmith-traceAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 54 |
|---|---|
| repo stars | ★ 111 |
| Last updated | July 29, 2026 |
| Repository | langchain-ai/skills-benchmarks ↗ |
What it does
Helps with ai & agent building tasks.
Files
<oneliner> Two main topics: adding tracing to your application, and querying traces for debugging and analysis. Python and Javascript implementations are both supported. </oneliner>
<setup> Environment Variables
LANGSMITH_API_KEY=lsv2_pt_your_api_key_here # REQUIRED
LANGSMITH_PROJECT=your-project-name # Optional: default project
LANGSMITH_WORKSPACE_ID=your-workspace-id # Optional: for org-scoped keysAuthentication is REQUIRED: either set the LANGSMITH_API_KEY environment variable, or pass the --api-key flag to CLI commands (preferred):
langsmith trace list --project my-project --api-key $LANGSMITH_API_KEYIMPORTANT: Always check the environment variables or .env file for LANGSMITH_PROJECT before querying or interacting with LangSmith. This tells you which project contains the relevant traces and data. If the LangSmith project is not available, use your best judgement to identify the right one.
CLI Tool
curl -sSL https://raw.githubusercontent.com/langchain-ai/langsmith-cli/main/scripts/install.sh | sh</setup>
<trace_langchain_oss> For LangChain/LangGraph apps, tracing is automatic. Just set environment variables:
export LANGSMITH_TRACING=true
export LANGSMITH_API_KEY=<your-api-key>
export OPENAI_API_KEY=<your-openai-api-key> # or your LLM provider's keyOptional variables:
LANGSMITH_PROJECT- specify project name (defaults to "default")LANGCHAIN_CALLBACKS_BACKGROUND=false- use for serverless to ensure traces complete before function exit (Python)
</trace_langchain_oss>
<trace_other_frameworks> For anything other than LangChain/LangGraph, read the matching reference file in `references/` before writing tracing code. Each reference covers install, env vars, setup snippet, and gotchas specific to that framework. The setup is rarely identical across frameworks — picking the wrong pattern (e.g. using @traceable when the framework has native OTel) creates duplicate/missing spans.
Decision order: 1. Framework has a dedicated reference below → use it 2. Framework has native OpenTelemetry but no dedicated reference → references/otel.md 3. No framework, or unsupported framework → references/traceable.md 4. Cannot run a LangSmith SDK at all → references/api.md (last resort)
Routing table:
| If you're tracing… | Read |
|---|---|
| OpenAI / Azure OpenAI / Anthropic / any plain LLM client | references/traceable.md |
| AutoGen | references/autogen.md |
| CrewAI | references/crewai.md |
| Google ADK | references/google-adk.md |
Google Gemini (google-genai SDK directly) | references/google-gemini.md |
| Instructor (structured outputs) | references/instructor.md |
| LiveKit Agents (voice AI) | references/livekit.md |
| Mastra (TypeScript) | references/mastra.md |
| Microsoft Agent Framework | references/microsoft-agent-framework.md |
| Mistral | references/mistral.md |
| n8n (self-hosted) | references/n8n.md |
| OpenAI Agents SDK | references/openai-agents-sdk.md |
| OpenCode | references/opencode.md |
| OpenAI Codex CLI | references/codex.md |
| Pipecat (voice AI) | references/pipecat.md |
| PydanticAI | references/pydantic-ai.md |
| Semantic Kernel | references/semantic-kernel.md |
| Strands Agents | references/strands-agents.md |
| Temporal workflows (Go/Python/TS) | references/temporal.md |
| Vercel AI SDK | references/vercel-ai-sdk.md |
| Any other framework with native OTel | references/otel.md |
| Multi-backend OTel fan-out | references/otel.md (Collector section) |
| Raw REST (no SDK available) | references/api.md |
If the framework you need isn't listed here, check references/ — new integrations are added there, not inline. </trace_other_frameworks>
<traces_vs_runs> Use the langsmith CLI to query trace data.
Understanding the difference is critical:
- Trace = A complete execution tree (root run + all child runs). A trace represents one full agent invocation with all its LLM calls, tool calls, and nested operations.
- Run = A single node in the tree (one LLM call, one tool call, etc.)
Generally, query traces first — they provide complete context and preserve hierarchy needed for trajectory analysis and dataset generation. </traces_vs_runs>
<command_structure> Two command groups with consistent behavior:
langsmith
├── trace (operations on trace trees - USE THIS FIRST)
│ ├── list - List traces (filters apply to root run)
│ ├── get - Get single trace with full hierarchy
│ └── export - Export traces to JSONL files (one file per trace)
│
├── run (operations on individual runs - for specific analysis)
│ ├── list - List runs (flat, filters apply to any run)
│ ├── get - Get single run
│ └── export - Export runs to single JSONL file (flat)
│
├── dataset (dataset operations)
│ ├── list - List datasets
│ ├── get - Get dataset details
│ ├── create - Create empty dataset
│ ├── delete - Delete dataset
│ ├── export - Export dataset to file
│ └── upload - Upload local JSON as dataset
│
├── example (example operations)
│ ├── list - List examples in a dataset
│ ├── create - Add example to a dataset
│ └── delete - Delete an example
│
├── evaluator (evaluator operations)
│ ├── list - List evaluators
│ ├── upload - Upload evaluator
│ └── delete - Delete evaluator
│
├── experiment (experiment operations)
│ ├── list - List experiments
│ └── get - Get experiment results
│
├── thread (thread operations)
│ ├── list - List conversation threads
│ └── get - Get thread details
│
└── project (project operations)
└── list - List tracing projectsKey differences:
traces * | runs * | |
|---|---|---|
| Filters apply to | Root run only | Any matching run |
--run-type | Not available | Available |
| Returns | Full hierarchy | Flat list |
| Export output | Directory (one file/trace) | Single file |
</command_structure>
<querying_traces> Query traces using the langsmith CLI. Commands are language-agnostic.
# List recent traces (most common operation)
langsmith trace list --limit 10 --project my-project --api-key $LANGSMITH_API_KEY
# List traces with metadata (timing, tokens, costs)
langsmith trace list --limit 10 --include-metadata --api-key $LANGSMITH_API_KEY
# Filter traces by time
langsmith trace list --last-n-minutes 60 --api-key $LANGSMITH_API_KEY
langsmith trace list --since 2025-01-20T10:00:00Z --api-key $LANGSMITH_API_KEY
# Get specific trace with full hierarchy
langsmith trace get <trace-id> --api-key $LANGSMITH_API_KEY
# List traces and show hierarchy inline
langsmith trace list --limit 5 --show-hierarchy --api-key $LANGSMITH_API_KEY
# Export traces to JSONL (one file per trace, includes all runs)
langsmith trace export ./traces --limit 20 --full --api-key $LANGSMITH_API_KEY
# Filter traces by performance
langsmith trace list --min-latency 5.0 --limit 10 --api-key $LANGSMITH_API_KEY # Slow traces (>= 5s)
langsmith trace list --error --last-n-minutes 60 --api-key $LANGSMITH_API_KEY # Failed traces
# List specific run types (flat list)
langsmith run list --run-type llm --limit 20 --api-key $LANGSMITH_API_KEY</querying_traces>
<filters> All commands support these filters (all AND together):
Basic filters:
--trace-ids abc,def- Filter to specific traces--limit N- Max results--project NAME- Project name--last-n-minutes N- Time filter--since TIMESTAMP- Time filter (ISO format)--error / --no-error- Error status--name PATTERN- Name contains (case-insensitive)
Performance filters:
--min-latency SECONDS- Minimum latency (e.g.,5for >= 5s)--max-latency SECONDS- Maximum latency--min-tokens N- Minimum total tokens--tags tag1,tag2- Has any of these tags
Advanced filter:
--filter QUERY- Raw LangSmith filter query for complex cases (feedback, metadata, etc.)
# Filter traces by feedback score using raw LangSmith query
langsmith trace list --filter 'and(eq(feedback_key, "correctness"), gte(feedback_score, 0.8))' --api-key $LANGSMITH_API_KEY</filters>
<export_format> Export creates .jsonl files (one run per line) with these fields:
{"run_id": "...", "trace_id": "...", "name": "...", "run_type": "...", "parent_run_id": "...", "inputs": {...}, "outputs": {...}}Use --include-io or --full to include inputs/outputs (required for dataset generation). </export_format>
<tips>
- Start with traces — they provide complete context needed for trajectory and dataset generation
- Use
traces export --fullfor bulk data destined for datasets - Always specify
--projectto avoid mixing data from different projects - Use
/tmpfor temporary exports - Include
--include-metadatafor performance/cost analysis - Stitch files:
cat ./traces/*.jsonl > all.jsonl
</tips>
Tracing via raw REST API
Last resort — only use when you can't run a LangSmith SDK (e.g. unsupported language/runtime). Synchronous REST calls block your app's request path; the SDKs do batched background sending and have lighter rate limits.
For Python/TS see traceable.md. For OTel-native frameworks see otel.md.
Auth
x-api-key: <LANGSMITH_API_KEY>
x-tenant-id: <LANGSMITH_WORKSPACE_ID> # if API key spans multiple workspacesBase URL
| Region | URL |
|---|---|
| US (default) | https://api.smith.langchain.com |
| EU (GCP) | https://eu.api.smith.langchain.com |
| US (AWS SaaS) | https://aws.api.smith.langchain.com |
| Self-hosted | https://<your-host>/api |
Run IDs
Use UUID v7 for id. UUIDv7 embeds a timestamp so runs sort correctly within a trace. The LangSmith SDK exports a uuid7 helper, or use the uuid_utils package directly.
Basic tracing — POST /runs + PATCH /runs/{id}
POST /runs to start, PATCH /runs/{id} to finish. Server auto-computes dotted_order and trace_id — you only set parent_run_id to nest. Slower, lower rate limits than batch.
import os, requests
from datetime import datetime, timezone
from langsmith import uuid7
headers = {
"x-api-key": os.environ["LANGSMITH_API_KEY"],
"x-tenant-id": os.environ.get("LANGSMITH_WORKSPACE_ID", ""),
}
BASE = "https://api.smith.langchain.com"
def post_run(run_id, name, run_type, inputs, parent_id=None):
data = {
"id": str(run_id),
"name": name,
"run_type": run_type,
"inputs": inputs,
"start_time": datetime.now(timezone.utc).isoformat(),
# "session_name": "<project>", # or "session_id": "<project-uuid>"
}
if parent_id:
data["parent_run_id"] = str(parent_id)
requests.post(f"{BASE}/runs", json=data, headers=headers)
def patch_run(run_id, outputs):
requests.patch(f"{BASE}/runs/{run_id}", json={
"outputs": outputs,
"end_time": datetime.now(timezone.utc).isoformat(),
}, headers=headers)
parent = uuid7()
post_run(parent, "Chat Pipeline", "chain", {"question": "…"})
child = uuid7()
post_run(child, "OpenAI Call", "llm", {"messages": [...]}, parent_id=parent)
# ... do work, get response ...
patch_run(child, {"choices": [...]})
patch_run(parent, {"answer": "…"})Batch ingestion — POST /runs/multipart
Higher throughput, higher rate limits. You must compute `dotted_order` and `trace_id` yourself.
trace_id— UUID of the root run.dotted_order—<YYYYMMDDTHHMMSSffffffZ><uuid>per run, joined by dots, e.g.
20240101T000000000000Z<root>.20240101T000001000000Z<child>.
The format encodes both ordering and parent-child relationships. The id field of a run equals the last 36 chars of its dotted order (after the final Z); trace_id equals the first UUID; parent_run_id equals the penultimate UUID.
Python deps: requests-toolbelt, uuid-utils.
The multipart body sends each run's main JSON plus separate parts for inputs, outputs, and events, all in one request:
post.<run_id> -> run JSON (without inputs/outputs/events)
post.<run_id>.inputs -> inputs JSON
post.<run_id>.outputs -> outputs JSON (optional)
post.<run_id>.events -> events JSON (optional)
patch.<run_id> -> patch JSON (for updates)Sketch:
import json, os, uuid, requests
from datetime import datetime, timezone
from requests_toolbelt import MultipartEncoder
from uuid_utils.compat import uuid7
def dotted(start_time, run_id):
return f"{start_time.strftime('%Y%m%dT%H%M%S%fZ')}{run_id}"
def make_run(name, run_type, inputs, parent_dotted=None):
rid = uuid7()
st = datetime.now(timezone.utc)
run = {
"id": str(rid),
"trace_id": str(rid),
"name": name,
"run_type": run_type,
"inputs": inputs,
"start_time": st.isoformat(),
"dotted_order": dotted(st, rid),
}
if parent_dotted:
run["dotted_order"] = f"{parent_dotted}.{run['dotted_order']}"
run["trace_id"] = parent_dotted.split(".")[0].split("Z")[1]
run["parent_run_id"] = parent_dotted.split(".")[-1].split("Z")[1]
return run
def serialize(op, run):
rid = run["id"]
inputs = run.pop("inputs", None)
outputs = run.pop("outputs", None)
events = run.pop("events", None)
parts = [(f"{op}.{rid}", (None, json.dumps(run).encode(), "application/json"))]
for k, v in [("inputs", inputs), ("outputs", outputs), ("events", events)]:
if v is not None:
parts.append((f"{op}.{rid}.{k}", (None, json.dumps(v).encode(), "application/json")))
return parts
def batch(posts=None, patches=None):
parts = []
for op, runs in (("post", posts or []), ("patch", patches or [])):
for r in runs:
parts.extend(serialize(op, dict(r)))
enc = MultipartEncoder(fields=parts, boundary=uuid.uuid4().hex)
requests.post(
"https://api.smith.langchain.com/runs/multipart",
data=enc,
headers={"Content-Type": enc.content_type, "x-api-key": os.environ["LANGSMITH_API_KEY"]},
).raise_for_status()
parent = make_run("Parent", "chain", {"q": "…"})
child = make_run("Child", "llm", {"messages": [...]}, parent_dotted=parent["dotted_order"])
batch(posts=[parent, child])
# Later: patch with end_time + outputs
batch(patches=[
{**parent, "end_time": datetime.now(timezone.utc).isoformat(), "outputs": {"answer": "…"}},
{**child, "end_time": datetime.now(timezone.utc).isoformat(), "outputs": {"choices": [...]}},
])Getting dotted_order wrong silently breaks the trace tree. Use the SDK if you can.
Run schema fields (most-used)
| Field | Type | Notes |
|---|---|---|
id | UUID | UUIDv7 recommended. |
name | string | Display name. |
run_type | string | chain, llm, tool, retriever, embedding, prompt, parser. |
inputs / outputs | object | Free-form JSON. For llm, typically { "messages": [...] }. |
start_time / end_time | ISO 8601 | Required on POST / PATCH respectively. |
parent_run_id | UUID | Set to nest under a parent. |
trace_id | UUID | Required for multipart. Equals root run's id. |
dotted_order | string | Required for multipart. <ts>Z<uuid> joined by dots. |
session_name | string | Project name to log to. |
session_id | UUID | Project ID (alternative to session_name). |
tags | string[] | Free-form. |
extra.metadata | object | Free-form metadata dict. |
events | object[] | Streaming / intermediate events. |
error | string | Error message; sets status to error. |
status | string | pending, success, error. |
reference_example_id | UUID | For evaluation runs. |
Full schema: see the LangSmith API reference (/runs POST/PATCH) and the run data format reference. Token-usage / cost fields (prompt_tokens, completion_tokens, total_tokens, total_cost, first_token_time) are populated by the server from outputs for run_type="llm".
Rate limits
Per service key / PAT, per 1-minute window:
| Endpoints | Limit |
|---|---|
POST or PATCH /runs* | 5000 / min |
GET /runs/:id | 30 / min |
POST /feedbacks* | 5000 / min |
DELETE /sessions* | 30 / min |
Exceeding returns 429. The SDK batches up to 100 runs per session into a single request to stay well under these limits — direct REST callers should implement retry with exponential backoff and jitter.
Tracing AutoGen applications
AutoGen exposes OpenTelemetry spans. Use the OtelSpanProcessor from LangSmith plus the OpenAI OTel instrumentor.
Install
pip install langsmith autogen-agentchat autogen-ext opentelemetry-instrumentation-openaiEnv
LANGSMITH_API_KEY=<key>
LANGSMITH_PROJECT=<project>
OPENAI_API_KEY=<key>Setup
from langsmith.integrations.otel import OtelSpanProcessor
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.instrumentation.openai import OpenAIInstrumentor
tracer_provider = TracerProvider()
tracer_provider.add_span_processor(OtelSpanProcessor())
trace.set_tracer_provider(tracer_provider)
OpenAIInstrumentor().instrument()Pass tracer_provider into the runtime
For multi-agent/group-chat coverage:
from autogen_core import SingleThreadedAgentRuntime
from autogen_agentchat.teams import SelectorGroupChat
runtime = SingleThreadedAgentRuntime(tracer_provider=trace.get_tracer_provider())
runtime.start()
team = SelectorGroupChat([...], runtime=runtime, ...)Without this, only the OpenAI calls are captured — agent/team coordination spans are missing.
Custom metadata
with tracer.start_as_current_span("autogen_workflow") as span:
span.set_attribute("langsmith.metadata.session_type", "multi_agent")
span.set_attribute("langsmith.span.tags", "autogen,planning")Combine with other instrumentors
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
OpenAIInstrumentor().instrument()
HTTPXClientInstrumentor().instrument()Tracing OpenAI Codex sessions
The langsmith-codex-plugins marketplace provides a tracing plugin for Codex CLI v0.128+. Tracing is disabled until either TRACE_TO_LANGSMITH=true or enabled: true is set in a config file.
Prerequisites
- Codex CLI v0.128 or later
- A LangSmith API key
Install and enable
codex plugin marketplace add langchain-ai/langsmith-codex-pluginsEnable plugin hooks and the tracing plugin in ~/.codex/config.toml (global) or <project>/.codex/config.toml (project):
[features]
plugin_hooks = true
[plugins."tracing@langsmith-codex-plugins"]
enabled = trueEnv
export TRACE_TO_LANGSMITH="true"
export LANGSMITH_CODEX_API_KEY="<key>" # or LANGSMITH_API_KEY
export LANGSMITH_CODEX_PROJECT="codex"Codex-specific overrides take precedence over generic LangSmith vars:
| Variable | Required | Default | Falls back to | Description |
|---|---|---|---|---|
TRACE_TO_LANGSMITH | Yes | — | — | Set to "true" to enable tracing |
LANGSMITH_CODEX_API_KEY | Conditional | — | LANGSMITH_API_KEY | Required unless every replica supplies its own key |
LANGSMITH_CODEX_ENDPOINT | No | https://api.smith.langchain.com | LANGSMITH_ENDPOINT | LangSmith API URL |
LANGSMITH_CODEX_PROJECT | No | codex | LANGSMITH_PROJECT | Project name |
LANGSMITH_CODEX_METADATA | No | — | LANGSMITH_METADATA | JSON object merged into root trace metadata |
LANGSMITH_CODEX_RUNS_ENDPOINTS | No | — | LANGSMITH_RUNS_ENDPOINTS | JSON array of replica destinations |
Config file
<project>/.codex/langsmith.json (project) or ~/.codex/langsmith.json (global):
{
"enabled": true,
"api_key": "<key>",
"api_url": "https://api.smith.langchain.com",
"project": "codex",
"metadata": {"team": "agents", "environment": "dev"}
}Loading order: global file → project file → environment variables. Each layer overrides the prior. Keep config files with API keys out of version control.
| Field | Env var | Default | Description |
|---|---|---|---|
enabled | TRACE_TO_LANGSMITH | false | Enable tracing |
api_key | LANGSMITH_CODEX_API_KEY, LANGSMITH_API_KEY | — | LangSmith API key |
api_url | LANGSMITH_CODEX_ENDPOINT, LANGSMITH_ENDPOINT | LangSmith default | API URL |
project | LANGSMITH_CODEX_PROJECT, LANGSMITH_PROJECT | codex | Project name |
metadata | LANGSMITH_CODEX_METADATA, LANGSMITH_METADATA | — | Root trace metadata |
replicas | LANGSMITH_CODEX_RUNS_ENDPOINTS, LANGSMITH_RUNS_ENDPOINTS | — | Replica destinations |
Multi-destination replicas
When replicas is set, it replaces (not augments) the single-destination client settings. Useful for prod+staging fan-out, multi-workspace tracing, or per-destination metadata.
Config file:
{
"enabled": true,
"replicas": [
{
"apiUrl": "https://api.smith.langchain.com",
"apiKey": "lsv2_pt_workspace_a",
"projectName": "project-prod"
},
{
"apiUrl": "https://api.smith.langchain.com",
"apiKey": "lsv2_pt_workspace_b",
"projectName": "project-staging",
"updates": {"metadata": {"environment": "staging"}}
}
]
}Shell env-var alternative:
export LANGSMITH_CODEX_RUNS_ENDPOINTS='[{"apiUrl":"https://api.smith.langchain.com","apiKey":"lsv2_pt_workspace_a","projectName":"project-prod"},{"apiUrl":"https://api.smith.langchain.com","apiKey":"lsv2_pt_workspace_b","projectName":"project-staging","updates":{"metadata":{"environment":"staging"}}}]'Generate the escaped JSON string with jq -c .:
echo '[{"apiUrl":"...","apiKey":"...","projectName":"..."}]' | jq -c .Each replica object:
| Field | Required | Description |
|---|---|---|
apiUrl | Yes | LangSmith API URL |
apiKey | Yes | API key for the destination workspace |
projectName | Yes | Project name in the destination |
updates | No | Optional run-field overrides (e.g. extra metadata) |
What gets traced
- Per LLM run: accumulated messages (inputs), assistant content (outputs), provider/model/stop-reason/token-usage metadata
- Tool calls: function calls, shell calls, computer calls, file reads, web searches — with inputs/outputs
- Subagent threads as nested child runs under the parent turn
- Cancelled/interrupted turns are still uploaded once the session completes
The plugin uploads full Codex transcripts. Don't enable for sessions containing data you don't want stored in LangSmith.
Troubleshooting
- Confirm
plugin_hooks = trueand the tracing plugin is enabled inconfig.toml - Confirm
TRACE_TO_LANGSMITH=trueis visible to the Codex process - Confirm
LANGSMITH_CODEX_API_KEYorLANGSMITH_API_KEYis set and valid - Wrong project? set
LANGSMITH_CODEX_PROJECTorprojectin config - Custom endpoint not used? set
LANGSMITH_CODEX_ENDPOINTorapi_urlin config
Tracing CrewAI applications
CrewAI is captured via two OTel instrumentors (crewai + openai) routed through LangSmith's OtelSpanProcessor.
Install
pip install langsmith crewai opentelemetry-instrumentation-crewai opentelemetry-instrumentation-openaiEnv
LANGSMITH_API_KEY=<key>
LANGSMITH_PROJECT=<project>
OPENAI_API_KEY=<key>Setup
from langsmith.integrations.otel import OtelSpanProcessor
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.instrumentation.crewai import CrewAIInstrumentor
from opentelemetry.instrumentation.openai import OpenAIInstrumentor
# Reuse existing TracerProvider if one is already set
current = trace.get_tracer_provider()
if isinstance(current, TracerProvider):
tracer_provider = current
else:
tracer_provider = TracerProvider()
trace.set_tracer_provider(tracer_provider)
tracer_provider.add_span_processor(OtelSpanProcessor())
CrewAIInstrumentor().instrument(tracer_provider=tracer_provider)
OpenAIInstrumentor().instrument(tracer_provider=tracer_provider)Pass tracer_provider= to both instrumentors. Skipping it on one of them causes mixed spans where some calls land in the global provider instead of LangSmith.
Custom metadata
with tracer.start_as_current_span("crewai_workflow") as span:
span.set_attribute("langsmith.metadata.crew_type", "code_generation")
span.set_attribute("langsmith.span.tags", "crewai,code-generation")
crew.kickoff()Tracing Google ADK applications
Use LangSmith's first-party configure_google_adk() helper — no manual OTel wiring needed.
Install
pip install "langsmith[google-adk]"Env
LANGSMITH_TRACING=true
LANGSMITH_ENDPOINT=https://api.smith.langchain.com
LANGSMITH_API_KEY=<key>
LANGSMITH_PROJECT=<project>
GOOGLE_API_KEY=<key>Setup
from langsmith.integrations.google_adk import configure_google_adk
configure_google_adk(
project_name="my-adk-project", # optional, defaults to LANGSMITH_PROJECT
name="google_adk.session", # optional root trace name
metadata={"environment": "production", "team": "ml-platform"},
tags=["adk", "v2"],
)Call once at startup, before creating any Agent. The helper installs a TracerProvider + LangSmith exporter automatically.
configure_google_adk() parameters:
project_name— LangSmith project. Defaults toLANGSMITH_PROJECT.name— root trace name. Defaults to"google_adk.session".metadata— dict of key-value context.tags— list of strings.
Run
from google.adk.agents import Agent
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types
agent = Agent(
name="weather_agent",
model="gemini-2.0-flash",
instruction="Use the get_weather tool to answer weather questions.",
tools=[get_weather],
)
runner = Runner(agent=agent, app_name="weather_app", session_service=InMemorySessionService())
async for event in runner.run_async(user_id="u", session_id="s",
new_message=types.Content(role="user", parts=[types.Part(text="...")])):
...Multi-agent workflows
SequentialAgent and parallel agent compositions are auto-traced under the same root — no extra config:
from google.adk.agents import Agent, SequentialAgent
translator = Agent(name="translator", model="gemini-2.0-flash",
description="Translates text to English.")
summarizer = Agent(name="summarizer", model="gemini-2.0-flash",
description="Summarizes text concisely.")
pipeline = SequentialAgent(
name="translate_and_summarize",
sub_agents=[translator, summarizer],
)What gets traced
- Agent invocations (full flow through ADK agents)
- Tool calls (individual function invocations)
- Gemini LLM requests/responses
- Multi-agent workflows (sequential + parallel compositions)
Tracing Google Gemini applications
Wrap the google-genai (Python) or @google/genai (JS) client with LangSmith's wrap_gemini / wrapGemini. Beta — API may change.
Python
pip install langsmith google-genaiLANGSMITH_TRACING=true
LANGSMITH_API_KEY=<key>
LANGSMITH_PROJECT=<project>
GOOGLE_API_KEY=<key>from google import genai
from langsmith import wrappers
client = wrappers.wrap_gemini(
genai.Client(),
tracing_extra={
"tags": ["gemini", "python"],
"metadata": {"integration": "google-genai"},
},
)
response = client.models.generate_content(
model="gemini-2.5-flash",
contents="Explain quantum computing in simple terms.",
)JavaScript / TypeScript
npm install langsmith @google/genaiimport { GoogleGenAI } from "@google/genai";
import { wrapGemini } from "langsmith/wrappers/gemini";
const client = wrapGemini(new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY }), {
tags: ["gemini", "javascript"],
metadata: { integration: "google-genai" },
});
const response = await client.models.generateContent({
model: "gemini-2.5-flash",
contents: "Explain quantum computing in simple terms.",
});Config (applies to all calls on the wrapped client)
tags— array of stringsmetadata— key-value objectclient— custom LangSmithClientinstance (use to share auth/config)
For per-call control, nest with @traceable / traceable (see traceable.md).
Tracing Instructor applications
Instructor patches an OpenAI client. Wrap with LangSmith first, then patch with Instructor.
Install
pip install -U langsmith instructor openaiEnv
LANGSMITH_API_KEY=<key>
LANGSMITH_WORKSPACE_ID=<workspace> # only if API key spans multiple workspacesSetup
import instructor
from openai import OpenAI
from langsmith import wrappers, traceable
from pydantic import BaseModel
# Order matters: wrap with LangSmith first, then patch with Instructor.
client = wrappers.wrap_openai(OpenAI())
client = instructor.patch(client)
class UserDetail(BaseModel):
name: str
age: int
user = client.chat.completions.create(
model="gpt-4o-mini",
response_model=UserDetail,
messages=[{"role": "user", "content": "Extract: Jason is 25"}],
)Nested traces
Wrap callers with @traceable to get a parent span around Instructor calls:
@traceable(name="Extract User Details")
def my_function(text: str) -> UserDetail:
return client.chat.completions.create(
model="gpt-4o-mini",
response_model=UserDetail,
messages=[{"role": "user", "content": f"Extract {text}"}],
)Gotcha
Patching order is the only common bug. If you call instructor.patch(OpenAI()) and then wrap_openai(...), the wrapping wins but Instructor's response_model handling can break. Always wrap first.
Tracing LiveKit applications
LiveKit Agents emits OTel spans, but they need a custom processor (LangSmithSpanProcessor) to be readable in LangSmith. Available via the LiveKit demo repo.
Python 3.9+.
Install
pip install langsmith livekit livekit-agents \
livekit-plugins-openai livekit-plugins-silero livekit-plugins-turn-detector \
opentelemetry-exporter-otlp python-dotenvOr with uv:
uv add langsmith livekit livekit-agents \
livekit-plugins-openai livekit-plugins-silero livekit-plugins-turn-detector \
opentelemetry-exporter-otlp python-dotenvEnv (.env)
OTEL_EXPORTER_OTLP_ENDPOINT=https://api.smith.langchain.com/otel
OTEL_EXPORTER_OTLP_HEADERS=x-api-key=<key>, Langsmith-Project=<project>
LIVEKIT_URL=<url>
LIVEKIT_API_KEY=<key>
LIVEKIT_API_SECRET=<secret>
OPENAI_API_KEY=<key>Setup
Get langsmith_processor.py from the LiveKit demo repo, drop it next to your agent, then enable tracing before creating AgentServer:
import os
from dotenv import load_dotenv
from livekit.agents.telemetry import set_tracer_provider
from opentelemetry.sdk.trace import TracerProvider
from langsmith_processor import LangSmithSpanProcessor
load_dotenv()
def setup_langsmith():
if not os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT") or not os.getenv("OTEL_EXPORTER_OTLP_HEADERS"):
print("OTEL env vars not set; tracing disabled.")
return
provider = TracerProvider()
provider.add_span_processor(LangSmithSpanProcessor())
set_tracer_provider(provider)
setup_langsmith() # call BEFORE creating AgentServerThe processor:
- Maps LiveKit span types (
stt,llm,tts,agent,session,job) to LangSmith run types - Adds
gen_ai.prompt.*/gen_ai.completion.*for message rendering - Aggregates conversation messages across turns
- Uses multiple extraction strategies for varying LiveKit attribute formats
Agent skeleton
import sys
from livekit import agents
from livekit.agents import AgentServer, AgentSession, Agent
from livekit.plugins import openai, silero
from livekit.plugins.turn_detector.multilingual import MultilingualModel
class Assistant(Agent):
def __init__(self):
super().__init__(instructions="You are a helpful voice AI assistant.")
server = AgentServer()
@server.rtc_session()
async def my_agent(ctx: agents.JobContext):
session = AgentSession(
stt="deepgram/nova-2:en",
llm="openai/gpt-4o-mini",
tts=openai.TTS(model="tts-1", voice="alloy"),
vad=silero.VAD.load(),
turn_detection=MultilingualModel(),
)
await session.start(room=ctx.room, agent=Assistant())
if __name__ == "__main__":
sys.argv = [sys.argv[0], "console"]
agents.cli.run_app(server)Run locally: python agent.py console.
Custom metadata and tags
from opentelemetry import trace
span = trace.get_current_span()
span.set_attribute("langsmith.metadata.agent_type", "voice_assistant")
span.set_attribute("langsmith.metadata.version", "1.0")
span.set_attribute("langsmith.span.tags", "livekit,voice-ai,production")Gotchas
- Spans missing: confirm
OTEL_EXPORTER_OTLP_ENDPOINTandOTEL_EXPORTER_OTLP_HEADERSare set, and thatsetup_langsmith()runs beforeAgentServer(). - Messages not rendering: confirm
LangSmithSpanProcessoris imported and registered; setLANGSMITH_PROCESSOR_DEBUG=truefor verbose logs. - API key permissions: LangSmith key needs write access on the target workspace.
- Connection issues: verify
LIVEKIT_URL,LIVEKIT_API_KEY,LIVEKIT_API_SECRET; test with the LiveKit CLI first. - Agent not responding: check provider keys (OpenAI/Deepgram/etc.) and that STT/LLM/TTS endpoints are reachable.
- Import errors: ensure all
livekit-plugins-*packages match the providers yourAgentSessionreferences; Python 3.9+ required.
Tracing Mastra applications (TypeScript only)
Mastra ships a first-party @mastra/langsmith exporter. Configure it on the Mastra constructor.
Install
npm install @mastra/core @mastra/langsmith @mastra/observability @mastra/libsqlEnv
LANGSMITH_API_KEY=<key>
LANGCHAIN_PROJECT=<project> # optional
OPENAI_API_KEY=<key> # if using OpenAI modelsSetup (mastra.ts)
import { Mastra } from "@mastra/core";
import { LibSQLStore } from "@mastra/libsql";
import { LangSmithExporter } from "@mastra/langsmith";
import { echoAgent } from "./agent";
export const mastra = new Mastra({
agents: { echoAgent },
storage: new LibSQLStore({ url: "file:./mastra.db" }), // required, even if exporting elsewhere
observability: {
configs: {
langsmith: {
serviceName: "mastra-langsmith-demo",
exporters: [new LangSmithExporter({ apiKey: process.env.LANGSMITH_API_KEY })],
},
},
},
telemetry: { enabled: false }, // disable deprecated telemetry to avoid double-emitting
});Define and run an agent
// agent.ts
import { Agent } from "@mastra/core/agent";
export const echoAgent = new Agent({
name: "echoAgent",
instructions: "You are a helpful assistant.",
model: "openai/gpt-4o-mini", // string-based ID, not provider object
});// index.ts
import { mastra } from "./mastra";
const result = await mastra.getAgent("echoAgent").generate("Say hello.");Notes
- Storage is required even when exporting traces externally
- Disable the deprecated
telemetryblock to avoid warnings + double traces - Use string-based model IDs (
"openai/gpt-4o-mini") — provider object literals can break tracing - No instrumentation file needed when running outside the Mastra server
Tracing Microsoft Agent Framework applications
MS Agent Framework has built-in OTel — point its OTLP exporter at LangSmith and call configure_otel_providers().
Install
pip install agent-framework opentelemetry-exporter-otlp-proto-httpEnv
ENABLE_INSTRUMENTATION=true
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_EXPORTER_OTLP_ENDPOINT=https://api.smith.langchain.com/otel/v1/traces
OTEL_EXPORTER_OTLP_HEADERS="x-api-key=<key>,Langsmith-Project=<project>"Setup
from agent_framework import ChatAgent
from agent_framework.observability import configure_otel_providers
from agent_framework.openai import OpenAIChatClient
configure_otel_providers(enable_sensitive_data=True) # False to redact prompts/completions
agent = ChatAgent(chat_client=OpenAIChatClient(model_id="gpt-4o"))
result = await agent.run("What's the capital of Bavaria?")Notes
- Set
enable_sensitive_data=Falseif you can't ship prompts/completions to LangSmith (e.g. PII). - The endpoint includes
/v1/traces— don't add it again. ENABLE_INSTRUMENTATION=trueis required; without it the framework's OTel hooks are inert.
Tracing Mistral applications
Mistral has no native instrumentation. Wrap calls with @traceable (Python) or traceable (TS) and tag with ls_provider / ls_model_name for cost tracking.
Install
# Python
pip install mistralai langsmith# JavaScript
npm install @mistralai/mistralai langsmith dotenvEnv
MISTRAL_API_KEY=<key>
LANGSMITH_TRACING=true # required — without it nothing is recorded
LANGSMITH_API_KEY=<key>
LANGSMITH_PROJECT=<project> # optional, defaults to "default"Python
import os
from mistralai import Mistral
from langsmith import traceable
client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])
@traceable(
run_type="llm",
metadata={"ls_provider": "mistral", "ls_model_name": "mistral-medium-latest"},
)
def query_mistral(prompt: str):
response = client.chat.complete(
model="mistral-medium-latest",
messages=[{"role": "user", "content": prompt}],
)
return response.choices[0].message
result = query_mistral("Hello, how are you?")
print(result.content)Run: python mistral_trace.py.
TypeScript
import { Mistral } from "@mistralai/mistralai";
import { traceable } from "langsmith/traceable";
import "dotenv/config";
const mistral = new Mistral({ apiKey: process.env.MISTRAL_API_KEY });
const tracedChat = traceable(
async (params: { model: string; messages: { role: string; content: string }[] }) => {
const response = await mistral.chat.complete(params);
return response.choices[0].message.content; // return content for LangSmith capture
},
{
name: "Mistral Chat Completion",
run_type: "llm",
metadata: { ls_provider: "mistral", ls_model_name: "mistral-small-latest" },
}
);
await tracedChat({
model: "mistral-small-latest",
messages: [{ role: "user", content: "Say hello in one short sentence." }],
});Run: node index.js.
Cost tracking
ls_provider + ls_model_name are what LangSmith uses to attach pricing to traced LLM calls — without them runs log but cost is null. Token counts come from the recorded prompt and response messages. Enable model pricing in your LangSmith workspace settings to see costs in the run UI. See "Automatically track costs based on token counts" in the LangSmith docs.
Gotchas
LANGSMITH_TRACING=truemust be set; otherwise@traceableis a no-op and nothing reaches LangSmith.- TS: return the message content from the traced function (not the full response object) for clean input/output rendering.
- Update
ls_model_namewhenever you switch models — it's used for cost lookup, not auto-detected.
Tracing n8n workflows
n8n's AI nodes are built on LangChain, so tracing is env-var-only — no code changes. Self-hosted n8n only.
Required env (on the n8n host)
LANGCHAIN_TRACING_V2=true
LANGCHAIN_API_KEY=<your-langsmith-api-key>Optional
LANGCHAIN_ENDPOINT=https://api.smith.langchain.com # set for EU/AWS/self-hosted LangSmith
# EU: https://eu.api.smith.langchain.com
# AWS: https://aws.api.smith.langchain.com
LANGCHAIN_PROJECT=my-project
LANGCHAIN_CALLBACKS_BACKGROUND=true # async upload (default); false for syncRestart the n8n instance for env vars to take effect.
Notes
- The variable names use the legacy
LANGCHAIN_*prefix (notLANGSMITH_*) — this is what n8n's AI runtime reads. - Cloud-hosted n8n does not support tracing.
- All AI workflow runs land in the configured project; non-AI workflows are not traced.
Tracing OpenAI Agents SDK applications
LangSmith ships OpenAIAgentsTracingProcessor for both Python and JS. Register it as a trace processor before running agents.
Version requirements
- Python:
langsmith>=0.3.15—pip install "langsmith[openai-agents]" - JS/TS:
langsmith>=0.5.25—npm install langsmith @openai/agents zod
Env
LANGSMITH_API_KEY=<key>
OPENAI_API_KEY=<key>
LANGSMITH_PROJECT=<project> # optional
LANGSMITH_WORKSPACE_ID=<workspace> # only if API key spans multiple workspacesInstalling the processor is an explicit opt-in — it posts traces even when LANGSMITH_TRACING is not set.
Python
pip install "langsmith[openai-agents]"
# or: uv add "langsmith[openai-agents]"import asyncio
from agents import Agent, Runner, set_trace_processors
from langsmith.integrations.openai_agents_sdk import OpenAIAgentsTracingProcessor
async def main():
agent = Agent(
name="Captain Obvious",
instructions="You are Captain Obvious, the world's most literal technical support agent.",
)
result = await Runner.run(agent, "Why is my code failing when I try to divide by zero?")
print(result.final_output)
if __name__ == "__main__":
set_trace_processors([OpenAIAgentsTracingProcessor()])
asyncio.run(main())JavaScript / TypeScript
npm install langsmith @openai/agents zod
# or: yarn add langsmith @openai/agents zod
# or: pnpm add langsmith @openai/agents zodimport { Agent, run, setTraceProcessors, tool } from "@openai/agents";
import { z } from "zod";
import { OpenAIAgentsTracingProcessor } from "langsmith/wrappers/openai_agents";
setTraceProcessors([new OpenAIAgentsTracingProcessor()]);
const getWeather = tool({
name: "get_weather",
description: "Get the current weather for a city",
parameters: z.object({ city: z.string() }),
execute: async ({ city }) => `The weather in ${city} is sunny.`,
});
const agent = new Agent({
name: "WeatherAgent",
instructions: "Use get_weather when asked about weather.",
model: "gpt-5-nano",
tools: [getWeather],
});
const result = await run(agent, "What's the weather in San Francisco?");
console.log(result.finalOutput);The trace contains the root agent run, response spans, handoffs, and nested tool call spans.
Configure the processor
import { Client } from "langsmith";
import { OpenAIAgentsTracingProcessor } from "langsmith/wrappers/openai_agents";
const client = new Client();
const processor = new OpenAIAgentsTracingProcessor({
client, // optional langsmith Client
projectName: "openai-agents-demo",
name: "Support agent workflow", // root trace name
tags: ["openai-agents"],
metadata: { environment: "development" },
});
setTraceProcessors([processor]);Nest traceable inside tools
traceable calls inside tool execute handlers nest under the active tool span automatically.
import { traceable } from "langsmith/traceable";
const lookupOrder = traceable(
async (orderId: string) => ({ orderId, status: "shipped" }),
{ name: "lookup_order" }
);
const orderStatus = tool({
name: "order_status",
description: "Look up the status of an order",
parameters: z.object({ orderId: z.string() }),
execute: async ({ orderId }) => JSON.stringify(await lookupOrder(orderId)),
});Serverless flush
Flush pending traces before the process exits:
const processor = new OpenAIAgentsTracingProcessor({ client });
setTraceProcessors([processor]);
try {
await run(agent, "Help me reset my password.");
} finally {
await processor.forceFlush();
}Notes
- Installing the processor is an explicit opt-in — it posts traces even when
LANGSMITH_TRACINGis not set. - Nested
traceablecalls inside agent tools inherit the active trace context. - The processor logs the root agent run, response spans, handoffs, and nested tool spans.
Tracing OpenCode sessions
The @langchain/langsmith-opencode plugin captures OpenCode session turns, tool calls, and subagent activity. Tracing is disabled by default — enable via env var or config file.
Prerequisites
- OpenCode installed and configured
- A LangSmith API key
- Access to edit
opencode.jsonor~/.config/opencode/opencode.json
Install and enable the plugin
Add the plugin to opencode.json (project) or ~/.config/opencode/opencode.json (global):
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@langchain/langsmith-opencode"]
}Env
TRACE_TO_LANGSMITH=true
LANGSMITH_API_KEY=<key>
LANGSMITH_PROJECT=opencodeRun OpenCode as usual; the plugin sends completed user turns to the configured project.
OpenCode-specific overrides take precedence over generic LangSmith vars:
| Variable | Required | Default | Falls back to | Description |
|---|---|---|---|---|
TRACE_TO_LANGSMITH | Yes | false | — | Set to "true" to enable tracing |
LANGSMITH_OPENCODE_API_KEY | Conditional | — | LANGSMITH_API_KEY | Required unless every replica supplies its own key |
LANGSMITH_OPENCODE_ENDPOINT | No | LangSmith SDK default | LANGSMITH_ENDPOINT | LangSmith API URL |
LANGSMITH_OPENCODE_PROJECT | No | opencode | LANGSMITH_PROJECT | Project name |
LANGSMITH_OPENCODE_METADATA | No | — | — | JSON object merged into root trace metadata |
LANGSMITH_OPENCODE_RUNS_ENDPOINTS | No | — | — | JSON array of replica destinations |
export LANGSMITH_OPENCODE_METADATA='{"team":"agents","environment":"dev"}'Config file
.opencode/langsmith.json (project) or ~/.config/opencode/langsmith.json (global):
{
"enabled": true,
"api_key": "<key>",
"api_url": "https://api.smith.langchain.com",
"project": "opencode",
"metadata": {"team": "agents", "environment": "dev"}
}| Field | Required | Default | Description |
|---|---|---|---|
enabled | Yes | false | Set to true to enable from config |
api_key | Conditional | — | Required unless provided via env or replicas |
api_url | No | LangSmith SDK default | Usually https://api.smith.langchain.com |
project | No | opencode | Project name |
metadata | No | — | Object merged into root trace metadata |
replicas | No | — | Additional destinations to fan-out to |
Keep config files containing API keys out of version control.
Multi-destination replicas
Set replicas in langsmith.json or LANGSMITH_OPENCODE_RUNS_ENDPOINTS to send the same trace to additional workspaces or projects:
{
"enabled": true,
"api_key": "<key>",
"project": "opencode",
"replicas": [
{
"api_url": "https://api.smith.langchain.com",
"api_key": "<replica-key>",
"project": "opencode-replica",
"updates": {"metadata": {"replica": true}}
}
]
}Replica objects accept both snake_case and SDK-style camelCase field names. Prefer snake_case in config files.
| Field | Description |
|---|---|
api_url / apiUrl | LangSmith API URL for the destination |
api_key / apiKey | API key for the destination workspace |
project / projectName | Project name in the destination |
updates | Optional run-field overrides (e.g. extra metadata) |
What gets traced
- Root:
opencode.sessionruns (one per completed user turn) - Children:
opencode.assistant.turn, tool calls (inputs/outputs/errors/timing/attachments), nested subagent sessions - Metadata: model, provider, invocation params, token usage, thread/session ID
- Messages: user, assistant, reasoning blocks, file parts, system prompts
- Trace closes on
step-finishevents; pending batches flush on shutdown - Session ID is stored as
thread_idmetadata — filter/group related turns in LangSmith with it
Troubleshooting
- Confirm
TRACE_TO_LANGSMITH=true(or"enabled": truein config) - Confirm the API key is set in the same shell/config OpenCode uses
- Confirm the plugin package is resolvable by OpenCode
- Check the configured project — if none, traces go to
opencode - Restart OpenCode after editing
opencode.json,langsmith.json, or env vars - The plugin only sends completed turns — incomplete turns are dropped
Tracing with OpenTelemetry
LangSmith ingests OTel traces. Use this path when:
- You're tracing a non-LangChain framework that has built-in OTel
- You need to fan out traces to LangSmith and another backend (hybrid)
- You want to bring your own
TracerProvider - You need distributed tracing across services
For the simple "Python/JS function with @traceable" path, see traceable.md.
Version requirements
- LangChain/LangGraph integration:
langsmith>=0.3.18 - Hybrid mode (
LANGSMITH_OTEL_ONLY, alternate providers):langsmith>=0.4.1 - Recommended throughout: `langsmith>=0.4.25` (important OTel fixes around export and hybrid fan-out stability)
Endpoints
| Region | Endpoint |
|---|---|
| US (default) | https://api.smith.langchain.com/otel |
| EU (GCP) | https://eu.api.smith.langchain.com/otel |
| US (AWS SaaS) | https://aws.api.smith.langchain.com/otel |
| Self-hosted | https://<your-host>/api/v1/otel |
Append /v1/traces if your exporter sends traces only (not metrics/logs). Most OTLP HTTP exporters do.
LangChain / LangGraph apps (auto-OTel export)
pip install "langsmith[otel]" # langsmith>=0.3.18 minimum, >=0.4.25 recommended
pip install langchainLANGSMITH_OTEL_ENABLED=true
LANGSMITH_TRACING=true
LANGSMITH_ENDPOINT=https://api.smith.langchain.com
LANGSMITH_API_KEY=<key>
LANGSMITH_WORKSPACE_ID=<workspace-id> # only if API key spans multiple workspacesRun your app — spans are exported automatically. Set LANGSMITH_OTEL_ONLY=true (requires >=0.4.1) to skip the native LangSmith exporter and emit OTel only.
Non-LangChain apps (manual OTel SDK)
pip install openai opentelemetry-sdk opentelemetry-exporter-otlpOTEL_EXPORTER_OTLP_ENDPOINT=https://api.smith.langchain.com/otel
OTEL_EXPORTER_OTLP_HEADERS="x-api-key=<key>,Langsmith-Project=<project>"For self-hosted, the endpoint takes the form <your-host>/api/v1/otel (then append /v1/traces if exporting traces only):
OTEL_EXPORTER_OTLP_ENDPOINT=https://ai-company.com/api/v1/otelimport os
from openai import OpenAI
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
trace.set_tracer_provider(TracerProvider())
trace.get_tracer_provider().add_span_processor(
BatchSpanProcessor(OTLPSpanExporter(timeout=10))
)
tracer = trace.get_tracer(__name__)
def call_openai():
model = "gpt-4o-mini"
with tracer.start_as_current_span("call_open_ai") as span:
span.set_attribute("langsmith.span.kind", "LLM")
span.set_attribute("langsmith.metadata.user_id", "user_123")
span.set_attribute("gen_ai.system", "OpenAI")
span.set_attribute("gen_ai.request.model", model)
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Write a haiku about recursion."},
]
for i, m in enumerate(messages):
span.set_attribute(f"gen_ai.prompt.{i}.role", m["role"])
span.set_attribute(f"gen_ai.prompt.{i}.content", m["content"])
completion = client.chat.completions.create(model=model, messages=messages)
span.set_attribute("gen_ai.response.model", completion.model)
span.set_attribute("gen_ai.completion.0.role", "assistant")
span.set_attribute("gen_ai.completion.0.content", completion.choices[0].message.content)
span.set_attribute("gen_ai.usage.prompt_tokens", completion.usage.prompt_tokens)
span.set_attribute("gen_ai.usage.completion_tokens", completion.usage.completion_tokens)
span.set_attribute("gen_ai.usage.total_tokens", completion.usage.total_tokens)
return completion.choices[0].messageGlobal OTel env vars
The LangSmith exporter respects standard OTel env vars:
| Var | Notes |
|---|---|
OTEL_EXPORTER_OTLP_ENDPOINT | Override endpoint |
OTEL_EXPORTER_OTLP_HEADERS | LangSmith API key + project added automatically when using LangSmith exporter |
OTEL_SERVICE_NAME | Defaults to "langsmith" |
You can also set a global TracerProvider before initializing LangChain components — LangSmith detects it and uses it instead of creating its own.
SDK helper: configure()
from langsmith.integrations.otel import configure
configure(project_name="my-project")Wires endpoint, headers, and TracerProvider for LangSmith automatically. Use when:
- You want zero env-var setup (e.g. PydanticAI, Semantic Kernel, Google ADK)
- You're combining with an instrumentor like
GoogleADKInstrumentorthat creates the spans
Send traces to an alternate / hybrid provider
import os
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
os.environ["LANGSMITH_OTEL_ENABLED"] = "true"
os.environ["LANGSMITH_TRACING"] = "true"
provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(
endpoint="https://otel.your-provider.com/v1/traces",
headers={"api-key": "<key>"},
)))
trace.set_tracer_provider(provider)
# LangChain app runs as normal; spans go to BOTH LangSmith and the other provider
chain = ChatPromptTemplate.from_template("Joke about {topic}") | ChatOpenAI()
chain.invoke({"topic": "programming"})To send to only the alternate provider (skip LangSmith): set LANGSMITH_OTEL_ONLY=true (requires langsmith>=0.4.1).
OTel Collector fan-out
For multi-destination at scale, emit OTel once and let a Collector fan out:
# otel-collector-config.yaml
receivers:
otlp:
protocols:
grpc: { endpoint: 0.0.0.0:4317 }
http: { endpoint: 0.0.0.0:4318 }
processors:
batch:
exporters:
otlphttp/langsmith:
endpoint: https://api.smith.langchain.com/otel/v1/traces
headers:
x-api-key: ${env:LANGSMITH_API_KEY}
Langsmith-Project: my_project
otlphttp/other_provider:
endpoint: https://otel.your-provider.com/v1/traces
headers:
api-key: ${env:OTHER_PROVIDER_API_KEY}
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlphttp/langsmith, otlphttp/other_provider]App points to the collector (http://localhost:4318/v1/traces); collector routes. Use this when you'd otherwise be configuring multiple exporters in app code.
Distributed tracing (context propagation)
When a request crosses service boundaries, propagate trace context via HTTP headers using OTel's inject/extract. Both services share the same trace ID.
# Service A
from opentelemetry.propagate import inject
with tracer.start_as_current_span("service_a_operation"):
result = chain.invoke({"text": "..."})
headers = {}
inject(headers) # injects traceparent / tracestate
requests.post("http://service-b/process", headers=headers, json={...})# Service B
from opentelemetry.propagate import extract
from flask import request
@app.route("/process", methods=["POST"])
def endpoint():
context = extract(request.headers)
with tracer.start_as_current_span("service_b_operation", context=context):
return jsonify({"analysis": chain.invoke({...}).content})The propagated context carries: trace ID, span ID, sampling decision. Service B's spans nest under Service A's root in LangSmith.
Attachments (multimodal inputs/outputs)
Attach files to a span by writing JSON to langsmith.attachments in a custom SpanProcessor.on_end(). The custom processor must run before OtelSpanProcessor so the attribute is on the span when LangSmith sees it.
import base64, json
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider, SpanProcessor
from langsmith.integrations.otel import OtelSpanProcessor
class AttachmentSpanProcessor(SpanProcessor):
def __init__(self):
self.attachment_data = None
def set_attachment(self, data):
self.attachment_data = data
def on_end(self, span):
if span.name == "invocation" and self.attachment_data:
span._attributes["langsmith.attachments"] = json.dumps([self.attachment_data])
provider = TracerProvider()
trace.set_tracer_provider(provider)
attachment_processor = AttachmentSpanProcessor()
provider.add_span_processor(attachment_processor) # FIRST: mutates span
provider.add_span_processor(OtelSpanProcessor(project="…")) # SECOND: reads + exports
with open("receipt.png", "rb") as f:
attachment_processor.set_attachment({
"name": "receipt",
"content": base64.b64encode(f.read()).decode("ascii"),
"mime_type": "image/jpeg",
})
# ...run your agent; the parent span gets the attachment...Order matters: span processors fire in registration order. Reverse the order and OtelSpanProcessor exports the span before the attachment is set.
Attribute mapping
LangSmith maps OTel attributes from several conventions onto its run model. Set these on spans (or rely on instrumentors that emit them) to get rich rendering.
Core LangSmith attributes
| OTel attribute | LangSmith field | Notes |
|---|---|---|
langsmith.trace.name | run name | Overrides span name |
langsmith.span.kind | run type | llm, chain, tool, retriever, embedding, prompt, parser |
langsmith.trace.session_id | session ID | |
langsmith.trace.session_name | session name | |
langsmith.span.tags | tags | Comma-separated |
langsmith.metadata.{key} | metadata.{key} | |
langsmith.attachments | attachments | JSON array (see above) |
GenAI standard attributes
| OTel attribute | LangSmith field |
|---|---|
gen_ai.system | metadata.ls_provider (e.g. "openai") |
gen_ai.operation.name | run type (chat/completion→llm, embedding→embedding) |
gen_ai.prompt | inputs |
gen_ai.completion | outputs |
gen_ai.prompt.{n}.role / .content | inputs.messages[n].role / .content |
gen_ai.prompt.{n}.message.role / .content | (alternative form, same target) |
gen_ai.completion.{n}.role / .content | outputs.messages[n].role / .content |
gen_ai.completion.{n}.message.role / .content | (alternative form) |
gen_ai.input.messages | inputs.messages (array) |
gen_ai.output.messages | outputs.messages (array) |
gen_ai.tool.name | sets run type to tool + invocation_params.tool_name |
GenAI request parameters
gen_ai.request.{model, temperature, top_p, top_k, max_tokens, frequency_penalty, presence_penalty, seed, stop_sequences, encoding_formats} → invocation_params.{...}. gen_ai.response.model also maps to invocation_params.model.
GenAI usage metrics
| OTel attribute | LangSmith field |
|---|---|
gen_ai.usage.input_tokens | usage_metadata.input_tokens |
gen_ai.usage.output_tokens | usage_metadata.output_tokens |
gen_ai.usage.total_tokens | usage_metadata.total_tokens |
gen_ai.usage.prompt_tokens | usage_metadata.input_tokens (deprecated) |
gen_ai.usage.completion_tokens | usage_metadata.output_tokens (deprecated) |
gen_ai.usage.details.reasoning_tokens | usage_metadata.reasoning_tokens |
TraceLoop attributes
| OTel attribute | LangSmith field |
|---|---|
traceloop.entity.input | inputs |
traceloop.entity.output | outputs |
traceloop.entity.name | run name |
traceloop.span.kind | run type |
traceloop.llm.request.type | run type (embedding→embedding, else llm) |
traceloop.association.properties.{key} | metadata.{key} |
OpenInference attributes (Arize/Phoenix)
| OTel attribute | LangSmith field |
|---|---|
input.value | inputs (string or JSON) |
output.value | outputs (string or JSON) |
openinference.span.kind | run type |
llm.system | metadata.ls_provider |
llm.model_name | metadata.ls_model_name |
tool.name | run name (when span kind is TOOL) |
metadata | metadata.* (JSON string, merged) |
LLM attributes
| OTel attribute | LangSmith field |
|---|---|
llm.input_messages | inputs.messages |
llm.output_messages | outputs.messages |
llm.token_count.prompt / .completion / .total | usage_metadata.* |
llm.invocation_parameters | invocation_params.* (JSON string) |
llm.presence_penalty / llm.frequency_penalty | invocation_params.* |
llm.request.functions | invocation_params.functions |
Prompt template / Retriever / Tool / Logfire
llm.prompt_template.variables→ run typeprompt(withinput.value)retrieval.documents.{n}.document.content→outputs.documents[n].page_contentretrieval.documents.{n}.document.metadata→outputs.documents[n].metadatatools→invocation_params.tools;tool_arguments→invocation_params.tool_arguments- Logfire:
prompt→ inputs,all_messages_events→ outputs,events→ split into inputs/outputs
Event mapping
LangSmith also reads OTel events (distinct from attributes). Useful for instrumentors that emit message events instead of gen_ai.prompt.{n}.* attributes.
| Event name | LangSmith field |
|---|---|
gen_ai.content.prompt | inputs |
gen_ai.content.completion | outputs |
gen_ai.system.message | inputs.messages[] (system) |
gen_ai.user.message | inputs.messages[] (user) |
gen_ai.assistant.message | outputs.messages[] (assistant) |
gen_ai.tool.message | outputs.messages[] (tool response) |
gen_ai.choice | outputs (with finish reason) |
exception | sets status to error, extracts exception.message + exception.stacktrace |
Event attribute extraction
For message events: content → message content, role → role, id → tool_call_id (tool messages), gen_ai.event.content → full message JSON.
For gen_ai.choice events: finish_reason, message.content, message.role, tool_calls.{n}.id, tool_calls.{n}.function.name, tool_calls.{n}.function.arguments, tool_calls.{n}.type.
For exception events: exception.message → error message; exception.stacktrace → appended to message.
Tracing Pipecat applications
Pipecat emits OTel spans but needs a custom langsmith_processor to map them to LangSmith. Available from the Pipecat demo repo.
Install
pip install langsmith "pipecat-ai[whisper,openai,local]" \
opentelemetry-exporter-otlp python-dotenv
# or: uv add langsmith "pipecat-ai[whisper,openai,local]" opentelemetry-exporter-otlp python-dotenv
# Optional, for audio attachments:
pip install scipy numpyRequires Python 3.9+.
Env (.env)
OTEL_EXPORTER_OTLP_ENDPOINT=https://api.smith.langchain.com/otel
OTEL_EXPORTER_OTLP_HEADERS=x-api-key=<key>, Langsmith-Project=pipecat-voice
OPENAI_API_KEY=<key>Setup
Drop langsmith_processor.py next to your agent (the demo repo file). Importing it auto-registers the processor.
import asyncio, uuid
from dotenv import load_dotenv
load_dotenv() # MUST run before importing Pipecat components
from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
from pipecat.services.whisper.stt import WhisperSTTService
from pipecat.services.openai import OpenAILLMService, OpenAITTSService
from pipecat.transports.local.audio import LocalAudioTransport, LocalAudioTransportParams
from langsmith_processor import span_processor # auto-registers on import
async def main():
conversation_id = str(uuid.uuid4())
transport = LocalAudioTransport(LocalAudioTransportParams(
audio_in_enabled=True, audio_out_enabled=True,
vad_analyzer=SileroVADAnalyzer(),
))
stt = WhisperSTTService()
llm = OpenAILLMService(model="gpt-4o-mini")
tts = OpenAITTSService(voice="alloy")
context = OpenAILLMContext(messages=[
{"role": "system", "content": "You are a helpful voice assistant."}
])
ctx = llm.create_context_aggregator(context)
pipeline = Pipeline([
transport.input(), stt, ctx.user(),
llm, tts,
transport.output(), ctx.assistant(),
])
task = PipelineTask(
pipeline,
params=PipelineParams(enable_metrics=True),
enable_tracing=True,
enable_turn_tracking=True, # required for per-turn audio
conversation_id=conversation_id,
)
await PipelineRunner().run(task)
if __name__ == "__main__":
asyncio.run(main())What the processor does
- Maps Pipecat span types (
stt,llm,tts,turn,conversation) to LangSmith run types - Adds
gen_ai.prompt.*/gen_ai.completion.*so messages render - Aggregates messages across turns
- Handles audio file attachments
Custom metadata and tags
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("voice_conversation") as span:
span.set_attribute("langsmith.metadata.session_type", "voice_assistant")
span.set_attribute("langsmith.metadata.user_id", "user_123")
span.set_attribute("langsmith.span.tags", "pipecat,voice-ai,stt-llm-tts")Audio attachments
AudioRecorder (full conversation) and TurnAudioRecorder (per-turn) — both register with span_processor and attach .wav files to the trace. AudioRecorder handles sample-rate mismatches between mic input and TTS output.
from pathlib import Path
from datetime import datetime
from audio_recorder import AudioRecorder
recordings_dir = Path(__file__).parent / "recordings"
recordings_dir.mkdir(exist_ok=True)
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
recording_path = recordings_dir / f"conversation_{ts}.wav"
audio_recorder = AudioRecorder(str(recording_path))
span_processor.register_recording(conversation_id, str(recording_path), audio_recorder=audio_recorder)
pipeline = Pipeline([
transport.input(), stt, ctx.user(),
llm, tts,
audio_recorder, # full conversation
transport.output(), ctx.assistant(),
])
try:
await PipelineRunner().run(task)
finally:
audio_recorder.save_recording() # MUST run before conversation span closesPer-turn:
from turn_audio_recorder import TurnAudioRecorder
turn_audio_recorder = TurnAudioRecorder(
span_processor=span_processor,
conversation_id=conversation_id,
recordings_dir=recordings_dir,
turn_tracker=None,
)
span_processor.register_turn_audio_recorder(conversation_id, turn_audio_recorder)
pipeline = Pipeline([
transport.input(), stt, ctx.user(),
llm, tts,
audio_recorder,
turn_audio_recorder, # per-turn snippets
transport.output(), ctx.assistant(),
])
# After PipelineTask creation:
if task.turn_tracking_observer:
turn_audio_recorder.connect_to_turn_tracker(task.turn_tracking_observer)Common issues
- Spans missing: verify
OTEL_EXPORTER_OTLP_ENDPOINT+OTEL_EXPORTER_OTLP_HEADERSin.env; confirm API key has write permissions; ensurelangsmith_processoris imported. - `load_dotenv()` order: must run before importing Pipecat components.
- Messages don't render: confirm
langsmith_processor.pyis present and imported; set a uniqueconversation_id; passenable_turn_tracking=TruetoPipelineTask. - Per-turn audio: requires
enable_turn_tracking=True. - Audio not working: check mic permissions, test devices in another app, adjust
SileroVADAnalyzer()settings, validate OpenAI API access for Whisper/TTS. - Slow responses: use
gpt-4o-mini, check network, consider local Whisper.
Tracing PydanticAI applications
PydanticAI has built-in OTel. Use langsmith.integrations.otel.configure() + Agent.instrument_all().
Install
pip install langsmith pydantic-ai opentelemetry-exporter-otlp
# or: uv add langsmith pydantic-ai opentelemetry-exporter-otlplangsmith>=0.4.26 recommended for optimal OTel support.
Env
LANGSMITH_API_KEY=<key>
LANGSMITH_PROJECT=<project>
OPENAI_API_KEY=<key>configure() wires the LangSmith OTel exporter automatically — no need to set OTEL_EXPORTER_OTLP_* env vars or build exporters manually.
Setup
from langsmith.integrations.otel import configure
from pydantic_ai import Agent
configure(project_name="pydantic-ai-demo")
Agent.instrument_all()
agent = Agent("openai:gpt-4o")
result = agent.run_sync("What is the capital of France?")
print(result.output)Call configure() + Agent.instrument_all() once at startup, before constructing agents. instrument_all() patches every PydanticAI Agent class. Per-agent instrumentation is also available via Agent(..., instrument=True).
Custom metadata and tags
Add metadata via OTel span attributes:
from opentelemetry import trace
from pydantic_ai import Agent
from langsmith.integrations.otel import configure
configure(project_name="pydantic-ai-metadata")
Agent.instrument_all()
tracer = trace.get_tracer(__name__)
agent = Agent("openai:gpt-4o")
with tracer.start_as_current_span("pydantic_ai_workflow") as span:
span.set_attribute("langsmith.metadata.user_id", "user_123")
span.set_attribute("langsmith.metadata.workflow_type", "question_answering")
span.set_attribute("langsmith.span.tags", "pydantic-ai,production")
result = agent.run_sync("Explain quantum computing in simple terms")
print(result.output)See otel.md for the full attribute mapping table (langsmith.metadata.*, langsmith.span.tags, langsmith.span.kind, etc.).
Tracing Semantic Kernel applications
Semantic Kernel has built-in OTel — wire LangSmith via configure() + the OpenAI instrumentor.
Install
pip install langsmith semantic-kernel opentelemetry-instrumentation-openai
# or: uv add langsmith semantic-kernel opentelemetry-instrumentation-openaiEnv
LANGSMITH_API_KEY=<key>
LANGSMITH_PROJECT=<project>
OPENAI_API_KEY=<key>Setup
from langsmith.integrations.otel import configure
from opentelemetry.instrumentation.openai import OpenAIInstrumentor
configure(project_name="semantic-kernel-demo")
OpenAIInstrumentor().instrument()configure() handles endpoint/headers; you don't need OTEL_EXPORTER_OTLP_* env vars.
Run
from semantic_kernel import Kernel
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion
kernel = Kernel()
kernel.add_service(OpenAIChatCompletion())
# ... add prompt template / function ...
result = await kernel.invoke(my_function, input=...)Custom metadata
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
async def analyze_with_metadata(code: str):
with tracer.start_as_current_span("semantic_kernel_workflow") as span:
span.set_attribute("langsmith.metadata.workflow_type", "code_analysis")
span.set_attribute("langsmith.metadata.user_id", "developer_123")
span.set_attribute("langsmith.span.tags", "semantic-kernel,code-analysis")
return await kernel.invoke(code_analyzer, code=code)See otel.md for the full attribute mapping table (langsmith.metadata.*, langsmith.span.kind, langsmith.span.tags, etc.).
Combine with other instrumentors
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
OpenAIInstrumentor().instrument()
HTTPXClientInstrumentor().instrument()Tracing Strands Agents applications
LangSmith ships langsmith.integrations.strands_agents.setup_langsmith_telemetry() — sets up Strands' OTel pipeline pointing at LangSmith.
Install
pip install "langsmith[strands-agents]"
# or: uv add "langsmith[strands-agents]"The extra pulls in langsmith, strands-agents, strands-agents-tools, and the OTLP-HTTP exporter.
Env
OTEL_EXPORTER_OTLP_ENDPOINT=https://api.smith.langchain.com/otel/v1/traces
OTEL_EXPORTER_OTLP_HEADERS="x-api-key=<key>,Langsmith-Project=<project>"
AWS_REGION=<region> # if using Amazon Bedrock as the model providerSetup
from langsmith.integrations.strands_agents import setup_langsmith_telemetry
from strands import Agent
setup_langsmith_telemetry() # call once at startup
# setup_langsmith_telemetry(console=True) # also print spans to stdout for debugging
agent = Agent(system_prompt="You are a concise assistant.")
response = agent("Explain LangSmith tracing in one sentence.")Custom OTLP exporter
If you need to set exporter options in code (instead of env vars):
from langsmith.integrations.strands_agents import create_langsmith_exporter
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from strands.telemetry import StrandsTelemetry
telemetry = StrandsTelemetry()
exporter = create_langsmith_exporter(
endpoint="https://api.smith.langchain.com/otel/v1/traces",
headers={"x-api-key": "<key>", "Langsmith-Project": "<project>"},
)
telemetry.tracer_provider.add_span_processor(BatchSpanProcessor(exporter))What gets traced
Agent invocations, event-loop cycle spans, LLM call spans (prompts, completions, token usage), tool call spans (inputs/outputs).
Tracing Temporal workflows
Use Temporal's native OTel interceptors with LangSmith as the OTLP destination. Supported in Go, Python, and TypeScript.
Both client and worker need the interceptor — Temporal propagates trace context across process boundaries via workflow headers, so client-initiated spans nest under the worker's activity spans automatically.
Env (all languages)
| Var | Required | Notes |
|---|---|---|
LANGSMITH_API_KEY | yes | From LangSmith Settings |
LANGSMITH_PROJECT | no | Defaults to default |
LANGCHAIN_BASE_URL | EU / self-hosted | Override for non-US LangSmith instances |
Python
pip install temporalio langsmith opentelemetry-sdk opentelemetry-exporter-otlp-proto-httpfrom opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource, SERVICE_NAME
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from temporalio.client import Client
from temporalio.contrib.opentelemetry import TracingInterceptor
def init_tracer_provider() -> TracerProvider:
exporter = OTLPSpanExporter(
endpoint="https://api.smith.langchain.com/otel/v1/traces",
headers={
"x-api-key": os.environ["LANGSMITH_API_KEY"],
"Langsmith-Project": os.environ.get("LANGSMITH_PROJECT", "default"),
},
)
provider = TracerProvider(resource=Resource.create({SERVICE_NAME: "temporal-worker"}))
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)
return provider
# Then on both worker and client:
client = await Client.connect("localhost:7233", interceptors=[TracingInterceptor()])
worker = Worker(client, task_queue="my-task-queue", workflows=[MyWorkflow], activities=[process_activity])Inside an activity, decorate the active span with gen_ai.* attributes so LangSmith renders it as an LLM run:
from opentelemetry import trace
from temporalio import activity
@activity.defn
async def process_activity(input: str) -> str:
span = trace.get_current_span()
span.set_attribute("gen_ai.prompt", input)
span.set_attribute("gen_ai.operation.name", "chat")
result = f"Processed: {input}"
span.set_attribute("gen_ai.completion", result)
return resultTypeScript
npm install @temporalio/client @temporalio/worker @temporalio/activity @temporalio/workflow \
@temporalio/interceptors-opentelemetry \
@opentelemetry/sdk-trace-node @opentelemetry/sdk-trace-base \
@opentelemetry/exporter-trace-otlp-http \
@opentelemetry/resources @opentelemetry/semantic-conventions @opentelemetry/api// tracer.ts
import { Resource } from "@opentelemetry/resources";
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
export function initTracerProvider(): NodeTracerProvider {
const exporter = new OTLPTraceExporter({
url: "https://api.smith.langchain.com/otel/v1/traces",
headers: {
"x-api-key": process.env.LANGSMITH_API_KEY!,
"Langsmith-Project": process.env.LANGSMITH_PROJECT ?? "default",
},
});
const provider = new NodeTracerProvider({
resource: new Resource({ [ATTR_SERVICE_NAME]: "temporal-worker" }),
});
provider.addSpanProcessor(new BatchSpanProcessor(exporter));
provider.register();
return provider;
}// activities.ts — add gen_ai.* attributes for LangSmith visibility
import { trace } from "@opentelemetry/api";
export async function processActivity(input: string): Promise<string> {
const span = trace.getActiveSpan();
span?.setAttribute("gen_ai.prompt", input);
span?.setAttribute("gen_ai.operation.name", "chat");
const result = `Processed: ${input}`;
span?.setAttribute("gen_ai.completion", result);
return result;
}// worker.ts — workflow exporter + activity interceptor
import { Worker, NativeConnection } from "@temporalio/worker";
import { Resource } from "@opentelemetry/resources";
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
import { trace } from "@opentelemetry/api";
import {
makeWorkflowExporter,
OpenTelemetryActivityInboundInterceptor,
} from "@temporalio/interceptors-opentelemetry";
import * as activities from "./activities";
import { initTracerProvider } from "./tracer";
const provider = initTracerProvider();
try {
const connection = await NativeConnection.connect({ address: "localhost:7233" });
const worker = await Worker.create({
connection,
namespace: "default",
taskQueue: "my-task-queue",
workflowsPath: require.resolve("./workflows"),
activities,
sinks: {
exporter: makeWorkflowExporter(
trace.getTracer("temporal-app"),
new Resource({ [ATTR_SERVICE_NAME]: "temporal-worker" }),
),
},
interceptors: {
activity: [() => ({ inbound: new OpenTelemetryActivityInboundInterceptor() })],
},
});
await worker.run();
} finally {
await provider.shutdown();
}Go
go get github.com/langchain-ai/langsmith-go@v0.1.0-alpha.7
go get go.temporal.io/sdk go.temporal.io/sdk/contrib/opentelemetryimport (
"context"
"github.com/langchain-ai/langsmith-go"
"go.temporal.io/sdk/client"
"go.temporal.io/sdk/contrib/opentelemetry"
"go.temporal.io/sdk/interceptor"
"go.temporal.io/sdk/worker"
)
ctx := context.Background()
ls, _ := langsmith.NewTracer(langsmith.WithServiceName("temporal-worker"))
defer ls.Shutdown(ctx)
tracingInterceptor, _ := opentelemetry.NewTracingInterceptor(
opentelemetry.TracerOptions{Tracer: ls.Tracer("temporal-app")},
)
c, _ := client.Dial(client.Options{
Interceptors: []interceptor.ClientInterceptor{tracingInterceptor},
})
defer c.Close()
w := worker.New(c, "my-task-queue", worker.Options{})
w.RegisterWorkflow(MyWorkflow)
w.RegisterActivity(MyActivity)
_ = w.Run(worker.InterruptCh())Inside an activity, attach gen_ai.* attributes to the span Temporal's interceptor created:
import (
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
func MyActivity(ctx context.Context, input string) (string, error) {
span := trace.SpanFromContext(ctx)
span.SetAttributes(
attribute.String("gen_ai.prompt", input),
attribute.String("gen_ai.operation.name", "chat"),
)
result := "Processed: " + input
span.SetAttributes(attribute.String("gen_ai.completion", result))
return result, nil
}The client that submits workflows needs the same interceptor — initialize a separate tracer (langsmith.WithServiceName("temporal-client")) and pass tracingInterceptor into client.Dial.
Gotchas
- Provider must be initialized before
Client.connect()and the worker — interceptors capture spans only against the active provider. provider.shutdown()(ordefer ls.Shutdown(ctx)in Go) is mandatory to flush pending traces.- Both client and worker need the interceptor; otherwise the workflow span won't link to the activity span.
- For LangSmith to render activities as LLM runs, decorate the activity's active span with
gen_ai.prompt,gen_ai.operation.name, andgen_ai.completion(see per-language snippets above). Full attribute mapping inotel.md. - TypeScript: workflow spans go through
makeWorkflowExporter(sandboxed), activity spans throughOpenTelemetryActivityInboundInterceptor— both are required.
Tracing with @traceable / wrap_openai
The default path for any non-LangChain app without native OTel support. Wrap your LLM client and/or decorate functions you want as spans. LangSmith handles context propagation across nested calls automatically.
For OTel-instrumented apps see otel.md. For raw REST (no SDK), see api.md.
Install
pip install langsmith # Python
npm install langsmith # TypeScript / JavaScriptEnvironment variables
| Var | Notes |
|---|---|
LANGSMITH_TRACING | true to enable. Required even when only using wrap_*. |
LANGSMITH_API_KEY | Required. |
LANGSMITH_PROJECT | Optional; defaults to default. |
LANGSMITH_WORKSPACE_ID | Set if your API key is linked to multiple workspaces. |
LANGSMITH_ENDPOINT | Override the base URL (EU, AWS SaaS, self-hosted). |
LANGSMITH_HIDE_INPUTS / LANGSMITH_HIDE_OUTPUTS | true to strip inputs/outputs before send. |
LANGSMITH_HIDE_METADATA | true to strip run metadata (Python). |
LANGCHAIN_CALLBACKS_BACKGROUND | false for serverless (Python) so spans flush before the process exits. |
@traceable / traceable
Apply to any function to make it a traced run. Nested traceable calls auto-nest as child runs.
Python
from langsmith import traceable
from openai import Client
openai = Client()
@traceable
def format_prompt(subject):
return [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": f"Name a store that sells {subject}?"},
]
@traceable(run_type="llm")
def invoke_llm(messages):
return openai.chat.completions.create(
model="gpt-4o-mini", messages=messages, temperature=0
)
@traceable
def parse_output(response):
return response.choices[0].message.content
@traceable
def run_pipeline():
return parse_output(invoke_llm(format_prompt("colorful socks")))
run_pipeline()TypeScript
import { traceable } from "langsmith/traceable";
import OpenAI from "openai";
const openai = new OpenAI();
const formatPrompt = traceable((subject: string) => [
{ role: "system" as const, content: "You are a helpful assistant." },
{ role: "user" as const, content: `Name a store that sells ${subject}?` },
], { name: "formatPrompt" });
const invokeLLM = traceable(
async ({ messages }: { messages: { role: string; content: string }[] }) =>
openai.chat.completions.create({ model: "gpt-4o-mini", messages, temperature: 0 }),
{ run_type: "llm", name: "invokeLLM" }
);
const runPipeline = traceable(async () => {
const messages = await formatPrompt("colorful socks");
const response = await invokeLLM({ messages });
return response.choices[0].message.content;
}, { name: "runPipeline" });
await runPipeline();When you wrap a sync function with traceable in JS, await it on call so the span flushes before the parent ends.
Decorator / config options
| Option | Notes |
|---|---|
name | Display name in the UI. Defaults to the function name. |
run_type | chain (default), llm, tool, retriever, embedding, prompt, parser. Drives UI rendering. |
tags | List of strings for filtering. |
metadata | Dict merged into extra.metadata. Use ls_provider / ls_model_name to enable cost tracking on non-OpenAI LLMs. |
project_name | Route this run (and its children) to a named project. |
client | Pass a pre-configured Client (e.g. for flush() control). |
process_inputs | Function (inputs: dict) -> dict to transform inputs before logging. |
process_outputs | Function (output) -> dict to transform outputs before logging (Python langsmith>=0.1.98). |
reduce_fn (Python) / aggregator (JS) | Aggregate streamed chunks into a single output value. |
Wrap an LLM client
wrap_* auto-traces every call on a client — no decorator needed. Renders messages, tool calls, and multimodal content blocks correctly. Composable with @traceable.
OpenAI (and OpenAI-compatible: Azure OpenAI, Together, Groq, ...)
from openai import OpenAI
from langsmith.wrappers import wrap_openai
client = wrap_openai(OpenAI())
client.chat.completions.create(model="gpt-4o-mini", messages=[...])import OpenAI from "openai";
import { wrapOpenAI } from "langsmith/wrappers";
const client = wrapOpenAI(new OpenAI());Anthropic
import anthropic
from langsmith.wrappers import wrap_anthropic
client = wrap_anthropic(anthropic.Anthropic())import Anthropic from "@anthropic-ai/sdk";
import { wrapAnthropic } from "langsmith/wrappers/anthropic";
const client = wrapAnthropic(new Anthropic());Google Gemini
See google-gemini.md. Python: wrap_gemini from langsmith.wrappers.
Other providers
If LangSmith doesn't ship a dedicated wrapper, wrap your own call with @traceable(run_type="llm", metadata={"ls_provider": "...", "ls_model_name": "..."}) to enable cost tracking and the LLM span renderer.
trace context manager (Python only)
Useful when you can't decorate (e.g. dynamic project name, partial blocks). Composes with @traceable and wrap_openai.
import langsmith as ls
from langsmith.wrappers import wrap_openai
from openai import Client
client = wrap_openai(Client())
with ls.trace("Chat Pipeline", "chain", project_name="my_test", inputs={"q": q}) as rt:
output = client.chat.completions.create(...).choices[0].message.content
rt.end(outputs={"output": output})RunTree API (low-level)
Manually post and patch runs — equivalent to the SDK's internal model. Not recommended unless you need precise control over dotted_order / parent-child links. LANGSMITH_API_KEY is required; LANGSMITH_TRACING is not (RunTree always sends).
from langsmith.run_trees import RunTree
pipeline = RunTree(name="Chat Pipeline", run_type="chain", inputs={"q": q})
pipeline.post()
child = pipeline.create_child(name="OpenAI Call", run_type="llm", inputs={"messages": ...})
child.post()
# ...
child.end(outputs=...); child.patch()
pipeline.end(outputs=...); pipeline.patch()import { RunTree } from "langsmith";
const pipeline = new RunTree({ name: "Chat Pipeline", run_type: "chain", inputs: { q } });
await pipeline.postRun();
const child = await pipeline.createChild({ name: "OpenAI Call", run_type: "llm", inputs: { messages } });
await child.postRun();
child.end(result); await child.patchRun();
pipeline.end({ outputs: { answer } }); await pipeline.patchRun();Streaming / generator functions
@traceable natively traces generators. Outputs are aggregated into a list by default; pass reduce_fn (Python) / aggregator (JS) to fold them.
from langsmith import traceable
@traceable(reduce_fn=lambda chunks: "".join(chunks))
def my_generator():
for chunk in ["Hello", " ", "World"]:
yield chunkconst myGenerator = traceable(function* () {
for (const chunk of ["Hello", " ", "World"]) yield chunk;
}, { aggregator: (chunks: string[]) => chunks.join("") });Aggregation only changes how the trace stores the output — your function still yields chunks.
Custom run IDs (UUIDv7)
Override the run ID when you need to attach feedback immediately or correlate with an external system. Use UUIDv7 so timestamp ordering is preserved.
from langsmith import traceable, uuid7
run_id = uuid7()
my_pipeline("…", langsmith_extra={"run_id": run_id})import { traceable, uuid7 } from "langsmith";
const myPipeline = traceable(async (q: string) => "…", { name: "my-pipeline", id: uuid7() });uuid7 requires langsmith>=0.4.43 (Python) or >=0.3.80 (JS).
Access the current run
from langsmith import traceable, get_current_run_tree
@traceable
def step():
rt = get_current_run_tree()
print(rt.trace_id)import { traceable, getCurrentRunTree } from "langsmith/traceable";
const step = traceable(() => {
const rt = getCurrentRunTree();
console.log(rt.trace_id);
});Flush before exit (serverless / short-lived processes)
Tracing runs in a background thread. AWS Lambda / Vercel Functions / scripts can exit before runs flush.
from langsmith import Client
client = Client()
@traceable(client=client)
async def handler(): ...
try:
await handler()
finally:
await client.flush()import { Client } from "langsmith";
const client = new Client();
const handler = traceable(async () => { /* … */ }, { client });
try { await handler(); } finally { await client.flush(); }Python alternative: LANGCHAIN_CALLBACKS_BACKGROUND=false makes calls block until flushed.
Tips
- Apply
@traceableto every nested function you want as its own span — without it, nested calls collapse into one. - Set
run_type="llm"+metadata={"ls_provider": "...", "ls_model_name": "..."}on raw LLM calls to enable cost tracking. - For nested instrumentation (e.g.
instructorpatching a wrapped OpenAI client) the wrapped client should be patched last — seeinstructor.md. wrap_*and@traceablecompose — use both in the same app.
Tracing Vercel AI SDK applications (TS/JS only)
Wrap the AI SDK's exported methods with wrapAISDK. Requires AI SDK v5 and langsmith>=0.3.63. (For older AI SDK versions, fall back to OTel — see otel.md.)
Install
npm install ai @ai-sdk/openai zodEnv
LANGSMITH_TRACING=true
LANGSMITH_API_KEY=<key>
LANGSMITH_WORKSPACE_ID=<workspace-id> # only if API key spans multiple workspaces
OPENAI_API_KEY=<key>Basic setup
import { openai } from "@ai-sdk/openai";
import * as ai from "ai";
import { wrapAISDK } from "langsmith/experimental/vercel";
const { generateText, streamText, generateObject, streamObject } = wrapAISDK(ai);
await generateText({
model: openai("gpt-5-nano"),
prompt: "Write a vegetarian lasagna recipe.",
});Tool calls and multi-step runs (stopWhen: stepCountIs(N)) are traced as nested spans automatically.
Group runs with traceable
import { traceable } from "langsmith/traceable";
const wrapper = traceable(async (input: string) => {
const { text } = await generateText({ model: openai("gpt-5-nano"), prompt: input, tools: {...} });
return text;
}, { name: "wrapper" });Per-call config
import { createLangSmithProviderOptions } from "langsmith/experimental/vercel";
const lsConfig = createLangSmithProviderOptions({
metadata: { individual_key: "value" },
name: "my_individual_run",
});
await generateText({
model: openai("gpt-5-nano"),
prompt: "...",
providerOptions: { langsmith: lsConfig },
});Pass options to wrapAISDK(ai, { ... }) for config that applies to all calls.
Serverless flush
import { Client } from "langsmith";
const client = new Client();
const { generateText } = wrapAISDK(ai, { client });
try {
await generateText({ ... });
} finally {
await client.awaitPendingTraceBatches();
}Next.js: use the after() hook from next/server to call awaitPendingTraceBatches() after the response is sent.
Pre-specified run IDs
import { uuid7 } from "langsmith";
const runId = uuid7();
const lsConfig = createLangSmithProviderOptions({ id: runId });
await generateText({ model: openai("gpt-5.4-mini"), prompt: "...", providerOptions: { langsmith: lsConfig } });
// Later: attach feedback / look up run by runIdRedacting inputs/outputs
createLangSmithProviderOptions accepts processInputs, processOutputs, processChildLLMRunInputs, processChildLLMRunOutputs. The actual return value is unaffected — only what's sent to LangSmith gets redacted. For tool input/output redaction, wrap the tool's execute in traceable({ processInputs, processOutputs, run_type: "tool" }).