Skip to content

Commit c5b5d17

Browse files
committed
feat(opencode): bridge global OTel tracer for AI SDK telemetry
The existing Effect Otlp.layerJson (merged via #21387) instruments Effect services but does not register a global OTel tracer provider. The AI SDK's experimental_telemetry reads from the global provider, so its spans were silently dropped. This adds a lightweight BasicTracerProvider bridge that registers globally when OTEL_EXPORTER_OTLP_ENDPOINT is set, using the same endpoint and headers the Effect layer already reads. AI SDK spans (ai.streamText, ai.toolCall, prompt/response content, token counts) now export alongside Effect service spans to any OTLP/HTTP collector. Also enables recordInputs/recordOutputs on both streamText and generateObject call sites so full message content is captured as span attributes.
1 parent 16c60c9 commit c5b5d17

6 files changed

Lines changed: 121 additions & 3 deletions

File tree

bun.lock

Lines changed: 53 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/opencode/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,11 @@
117117
"@opencode-ai/sdk": "workspace:*",
118118
"@opencode-ai/util": "workspace:*",
119119
"@openrouter/ai-sdk-provider": "2.4.2",
120+
"@opentelemetry/api": "1.9.1",
121+
"@opentelemetry/exporter-trace-otlp-http": "0.214.0",
122+
"@opentelemetry/resources": "2.6.1",
123+
"@opentelemetry/sdk-trace-base": "2.6.1",
124+
"@opentelemetry/semantic-conventions": "1.40.0",
120125
"@opentui/core": "0.1.97",
121126
"@opentui/solid": "0.1.97",
122127
"@parcel/watcher": "2.5.1",

packages/opencode/src/agent/agent.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import PROMPT_COMPACTION from "./prompt/compaction.txt"
1313
import PROMPT_EXPLORE from "./prompt/explore.txt"
1414
import PROMPT_SUMMARY from "./prompt/summary.txt"
1515
import PROMPT_TITLE from "./prompt/title.txt"
16+
import { Flag } from "@/flag/flag"
1617
import { Permission } from "@/permission"
1718
import { mergeDeep, pipe, sortBy, values } from "remeda"
1819
import { Global } from "@/global"
@@ -347,7 +348,9 @@ export namespace Agent {
347348

348349
const params = {
349350
experimental_telemetry: {
350-
isEnabled: cfg.experimental?.openTelemetry,
351+
isEnabled: cfg.experimental?.openTelemetry || !!Flag.OTEL_EXPORTER_OTLP_ENDPOINT,
352+
recordInputs: true,
353+
recordOutputs: true,
351354
metadata: {
352355
userId: cfg.username ?? "unknown",
353356
},

packages/opencode/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import "./otel-bridge.js"
12
import yargs from "yargs"
23
import { hideBin } from "yargs/helpers"
34
import { RunCommand } from "./cli/cmd/run"
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* Registers a global OTel tracer provider so the AI SDK's
3+
* `experimental_telemetry` spans are exported alongside Effect's own spans.
4+
*
5+
* Effect's Otlp.layerJson (in effect/oltp.ts) handles Effect-service tracing
6+
* but does NOT register a global provider — the AI SDK only talks to the
7+
* global one. This bridge fills the gap with a lightweight BasicTracerProvider.
8+
*
9+
* Import this module as the FIRST import in the entry point so the provider
10+
* is registered before any AI SDK code runs.
11+
*/
12+
import { Flag } from "@/flag/flag"
13+
14+
const endpoint = Flag.OTEL_EXPORTER_OTLP_ENDPOINT
15+
if (endpoint) {
16+
const { trace } = await import("@opentelemetry/api")
17+
const { BasicTracerProvider, BatchSpanProcessor } = await import("@opentelemetry/sdk-trace-base")
18+
const { OTLPTraceExporter } = await import("@opentelemetry/exporter-trace-otlp-http")
19+
const { resourceFromAttributes } = await import("@opentelemetry/resources")
20+
const { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } = await import("@opentelemetry/semantic-conventions")
21+
22+
const { CHANNEL, VERSION } = await import("@/installation/meta")
23+
24+
const provider = new BasicTracerProvider({
25+
resource: resourceFromAttributes({
26+
[ATTR_SERVICE_NAME]: "opencode",
27+
[ATTR_SERVICE_VERSION]: VERSION,
28+
"deployment.environment.name": CHANNEL === "local" ? "local" : CHANNEL,
29+
}),
30+
spanProcessors: [
31+
new BatchSpanProcessor(
32+
new OTLPTraceExporter({
33+
url: `${endpoint.replace(/\/$/, "")}/v1/traces`,
34+
headers: Flag.OTEL_EXPORTER_OTLP_HEADERS
35+
? Flag.OTEL_EXPORTER_OTLP_HEADERS.split(",").reduce(
36+
(acc, x) => {
37+
const [key, ...rest] = x.split("=")
38+
acc[key] = rest.join("=")
39+
return acc
40+
},
41+
{} as Record<string, string>,
42+
)
43+
: undefined,
44+
}),
45+
),
46+
],
47+
})
48+
49+
trace.setGlobalTracerProvider(provider)
50+
51+
const shutdown = () => provider.shutdown().catch(() => {})
52+
process.on("beforeExit", shutdown)
53+
process.on("SIGINT", shutdown)
54+
process.on("SIGTERM", shutdown)
55+
}

packages/opencode/src/session/llm.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,9 @@ export namespace LLM {
385385
],
386386
}),
387387
experimental_telemetry: {
388-
isEnabled: cfg.experimental?.openTelemetry,
388+
isEnabled: cfg.experimental?.openTelemetry || !!Flag.OTEL_EXPORTER_OTLP_ENDPOINT,
389+
recordInputs: true,
390+
recordOutputs: true,
389391
metadata: {
390392
userId: cfg.username ?? "unknown",
391393
sessionId: input.sessionID,

0 commit comments

Comments
 (0)