Appearance
OpenTelemetry
Agent SDK can push traces, metrics, and logs from the serve process to an OTLP collector you run. Point the process at the collector with standard OTEL_EXPORTER_OTLP_* env, or author agent/otel.ts. Traces cover the inbound request, the session, each turn, and every tool call.
Export is opt-in. Nothing leaves the process until you set an endpoint or a defineOtel config.
What does Agent SDK export?
| Signal | Default | What you get |
|---|---|---|
| Traces | on | agent_sdk.http → agent_sdk.session → agent_sdk.turn → agent_sdk.tool / agent_sdk.subagent |
| Metrics | on | cursor.token.usage, cursor.tool.calls, cursor.cost.usage, plus agent_sdk.* session and turn counts |
| Logs | off | Session events as log records. Prompt text, tool payloads, and failure messages stay off unless you opt in |
Turn off a signal with traces: false, metrics: false, or logs: false on defineOtel. Logs also turn on when you set OTEL_LOGS_EXPORTER to anything other than none, or when you set the content flags below.
How do I turn OpenTelemetry export on?
Set a collector URL in the serve process environment:
bash
export OTEL_EXPORTER_OTLP_ENDPOINT=https://otel.example.com
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer …"The default wire format is OTLP/HTTP protobuf. That matches Cursor enterprise OpenTelemetry Export. Set OTEL_EXPORTER_OTLP_PROTOCOL=http/json when your collector only accepts JSON. The runtime accepts http/protobuf and http/json. grpc falls back to protobuf and logs a warning.
OTEL_EXPORTER_OTLP_ENDPOINT is the base URL. The runtime appends /v1/traces, /v1/metrics, and /v1/logs. If you pass a signal path, it is stripped back to the base first.
To send each signal to a different collector, omit the base URL and set the per-signal vars:
bash
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://traces.example.com/v1/traces
export OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=https://metrics.example.com/v1/metrics
export OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=https://logs.example.com/v1/logsOptional:
| Variable | Effect |
|---|---|
OTEL_SERVICE_NAME | Resource service.name. Default cursor |
OTEL_LOG_USER_PROMPTS=1 | Include user prompt text on logs and span events |
OTEL_LOG_TOOL_CONTENT=1 | Include tool payloads and failure text (truncated) |
serve(dir, { otel: false }) turns export off even when env or agent/otel.ts is set.
How do I author agent/otel.ts?
Use defineOtel when you want the collector URL, headers, or sampling in the project instead of the environment:
ts
import { defineOtel } from "@cursor/july/otel";
export default defineOtel({
serviceName: "cursor",
exporters: [
{
url: "https://otel.example.com",
protocol: "http/protobuf",
headers: { Authorization: "Bearer …" },
},
],
});Multiple exporters fan out to every destination. Restrict one destination with signals: ["traces"].
You can also pass the same object to serve(dir, { otel }). Precedence is serve({ otel }) over agent/otel.ts over env. An empty defineOtel() still enables export when OTEL_EXPORTER_OTLP_* is set.
The companion skill is skills/otel/SKILL.md.
What spans does a session produce?
text
agent_sdk.http inbound channel request (W3C traceparent)
└─ agent_sdk.session cursor.conversation.id = session id
└─ agent_sdk.turn
├─ agent_sdk.tool
└─ agent_sdk.subagent
└─ agent_sdk.toolInbound HTTP extracts W3C traceparent, so a channel request parents the session span when the turn starts in that request. Turns that resume after restore, or a direct callTool with no turn.started in this process, open a synthetic turn span so tool calls still nest.
Which attributes land on the wire?
Every signal carries these resource attributes:
service.name(cursorunless you override it)cursor.entrypoint=sdk_tscursor.surface=unspecifiedagent_sdk.framework=@cursor/july
Shared names (same keys as enterprise export):
cursor.conversation.id: the session idcursor.model.namecursor.token.usagewithcursor.token.typeofinput,output,cache_read,cache_creation, orreasoningcursor.tool.callswithcursor.tool.name,cursor.tool.kind, andcursor.tool.statuscursor.cost.usage(USD)
Agent SDK only (agent_sdk.*): agent name, turn id, channel, call id, subagent name, session mode, and HTTP duration (agent_sdk.http.duration).
How do I emit my own metrics?
ctx.host.otel is always present on tools, hooks, and channel handlers. Counters and histograms no-op when no meter is running. setAttributes still tags the open session when a collector is attached.
Prefix metric names with your team or agent. First-party names (cursor.token.usage, cursor.tool.calls, cursor.cost.usage, agent_sdk.session.count, agent_sdk.turn.count, agent_sdk.subagent.count, agent_sdk.http.duration) and join keys (cursor.conversation.id, agent_sdk.agent, agent_sdk.turn_id, agent_sdk.framework) are reserved. Custom spans are not on this surface.
ts
ctx.host.otel.setAttributes({
"abc.ticket_id": "INC-123",
"abc.queue": "p1",
});
ctx.host.otel.increment("abc.ticket.resolved");
ctx.host.otel.record("abc.approval.duration_ms", 1420, {
outcome: "approved",
});A session-bound host (tools and hooks) adds cursor.conversation.id and agent_sdk.agent for you. Tags merge: later setAttributes calls paint open spans and later first-party metrics.
What stays off the OpenTelemetry wire?
User prompts, tool arguments, tool results, and failure messages are omitted by default. Failure spans still record an error status with a generic message (turn failed / session failed).
Opt in with OTEL_LOG_USER_PROMPTS=1 and OTEL_LOG_TOOL_CONTENT=1, or the matching logs: { userPrompts, toolContent } fields on defineOtel. Opted-in strings truncate at 2,048 characters.
Deployment URLs from agent.bound stay off spans unless toolContent is on.
How does Agent SDK export relate to Cursor enterprise export?
Cursor enterprise OpenTelemetry Export is the team-admin path. Cursor servers send org-wide metrics and logs to a collector you configure in Team Settings.
Agent SDK export is the process-local path. The serve process sends per-run traces, and optional metrics and logs, to a collector you point it at.
Point both at the same collector when you want one view. Group on cursor.conversation.id. Resource defaults (service.name=cursor, cursor.entrypoint=sdk_ts) keep the streams next to each other. The wire reference lists the shared attribute names.
Evals (defineEval) stay the in-product regression check. OpenTelemetry is the graph in your observability stack.
What if another OpenTelemetry SDK is already running?
OTel providers are process-global. If Cursor CLI, an extension host, or another library already registered a TracerProvider, MeterProvider, or LoggerProvider, Agent SDK reuses it. Run serve as its own process when the Agent SDK exporters should own the destination.
Two mounts with different agent/otel.ts files fail at serve start. Use the same defineOtel config on every mount, or configure once through serve({ otel }) or env.
What's next
skills/otel/SKILL.md: compactdefineOtelreference for coding agents- Hooks: observe the same session event stream in-process
- Deployment: env, secrets, and self-hosting
- Cursor enterprise OpenTelemetry Export