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.
This commit is contained in:
parent
16c60c9ee7
commit
c5b5d17008
6 changed files with 121 additions and 3 deletions
|
|
@ -117,6 +117,11 @@
|
|||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@openrouter/ai-sdk-provider": "2.4.2",
|
||||
"@opentelemetry/api": "1.9.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "0.214.0",
|
||||
"@opentelemetry/resources": "2.6.1",
|
||||
"@opentelemetry/sdk-trace-base": "2.6.1",
|
||||
"@opentelemetry/semantic-conventions": "1.40.0",
|
||||
"@opentui/core": "0.1.97",
|
||||
"@opentui/solid": "0.1.97",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import PROMPT_COMPACTION from "./prompt/compaction.txt"
|
|||
import PROMPT_EXPLORE from "./prompt/explore.txt"
|
||||
import PROMPT_SUMMARY from "./prompt/summary.txt"
|
||||
import PROMPT_TITLE from "./prompt/title.txt"
|
||||
import { Flag } from "@/flag/flag"
|
||||
import { Permission } from "@/permission"
|
||||
import { mergeDeep, pipe, sortBy, values } from "remeda"
|
||||
import { Global } from "@/global"
|
||||
|
|
@ -347,7 +348,9 @@ export namespace Agent {
|
|||
|
||||
const params = {
|
||||
experimental_telemetry: {
|
||||
isEnabled: cfg.experimental?.openTelemetry,
|
||||
isEnabled: cfg.experimental?.openTelemetry || !!Flag.OTEL_EXPORTER_OTLP_ENDPOINT,
|
||||
recordInputs: true,
|
||||
recordOutputs: true,
|
||||
metadata: {
|
||||
userId: cfg.username ?? "unknown",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import "./otel-bridge.js"
|
||||
import yargs from "yargs"
|
||||
import { hideBin } from "yargs/helpers"
|
||||
import { RunCommand } from "./cli/cmd/run"
|
||||
|
|
|
|||
55
packages/opencode/src/otel-bridge.ts
Normal file
55
packages/opencode/src/otel-bridge.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
/**
|
||||
* Registers a global OTel tracer provider so the AI SDK's
|
||||
* `experimental_telemetry` spans are exported alongside Effect's own spans.
|
||||
*
|
||||
* Effect's Otlp.layerJson (in effect/oltp.ts) handles Effect-service tracing
|
||||
* but does NOT register a global provider — the AI SDK only talks to the
|
||||
* global one. This bridge fills the gap with a lightweight BasicTracerProvider.
|
||||
*
|
||||
* Import this module as the FIRST import in the entry point so the provider
|
||||
* is registered before any AI SDK code runs.
|
||||
*/
|
||||
import { Flag } from "@/flag/flag"
|
||||
|
||||
const endpoint = Flag.OTEL_EXPORTER_OTLP_ENDPOINT
|
||||
if (endpoint) {
|
||||
const { trace } = await import("@opentelemetry/api")
|
||||
const { BasicTracerProvider, BatchSpanProcessor } = await import("@opentelemetry/sdk-trace-base")
|
||||
const { OTLPTraceExporter } = await import("@opentelemetry/exporter-trace-otlp-http")
|
||||
const { resourceFromAttributes } = await import("@opentelemetry/resources")
|
||||
const { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } = await import("@opentelemetry/semantic-conventions")
|
||||
|
||||
const { CHANNEL, VERSION } = await import("@/installation/meta")
|
||||
|
||||
const provider = new BasicTracerProvider({
|
||||
resource: resourceFromAttributes({
|
||||
[ATTR_SERVICE_NAME]: "opencode",
|
||||
[ATTR_SERVICE_VERSION]: VERSION,
|
||||
"deployment.environment.name": CHANNEL === "local" ? "local" : CHANNEL,
|
||||
}),
|
||||
spanProcessors: [
|
||||
new BatchSpanProcessor(
|
||||
new OTLPTraceExporter({
|
||||
url: `${endpoint.replace(/\/$/, "")}/v1/traces`,
|
||||
headers: Flag.OTEL_EXPORTER_OTLP_HEADERS
|
||||
? Flag.OTEL_EXPORTER_OTLP_HEADERS.split(",").reduce(
|
||||
(acc, x) => {
|
||||
const [key, ...rest] = x.split("=")
|
||||
acc[key] = rest.join("=")
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
)
|
||||
: undefined,
|
||||
}),
|
||||
),
|
||||
],
|
||||
})
|
||||
|
||||
trace.setGlobalTracerProvider(provider)
|
||||
|
||||
const shutdown = () => provider.shutdown().catch(() => {})
|
||||
process.on("beforeExit", shutdown)
|
||||
process.on("SIGINT", shutdown)
|
||||
process.on("SIGTERM", shutdown)
|
||||
}
|
||||
|
|
@ -385,7 +385,9 @@ export namespace LLM {
|
|||
],
|
||||
}),
|
||||
experimental_telemetry: {
|
||||
isEnabled: cfg.experimental?.openTelemetry,
|
||||
isEnabled: cfg.experimental?.openTelemetry || !!Flag.OTEL_EXPORTER_OTLP_ENDPOINT,
|
||||
recordInputs: true,
|
||||
recordOutputs: true,
|
||||
metadata: {
|
||||
userId: cfg.username ?? "unknown",
|
||||
sessionId: input.sessionID,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue