opencode/packages/util/src/observability/otlp.ts

98 lines
2.8 KiB
TypeScript

import { Layer } from "effect"
import { OtlpLogger } from "effect/unstable/observability"
import { runID } from "./shared.js"
export interface Options {
readonly endpoint?: string
readonly headers?: string
}
export interface App {
readonly client: string
readonly version: string
readonly channel: string
}
function parseHeaders(value?: string) {
return value
? value.split(",").reduce(
(acc, entry) => {
const [key, ...value] = entry.split("=")
acc[key] = value.join("=")
return acc
},
{} as Record<string, string>,
)
: undefined
}
function resourceAttributes() {
const value = process.env.OTEL_RESOURCE_ATTRIBUTES
if (!value) return {}
try {
return Object.fromEntries(
value.split(",").map((entry) => {
const index = entry.indexOf("=")
if (index < 1) throw new Error("Invalid OTEL_RESOURCE_ATTRIBUTES entry")
return [decodeURIComponent(entry.slice(0, index)), decodeURIComponent(entry.slice(index + 1))]
}),
)
} catch {
return {}
}
}
export function resource(app: App = { client: "opencode", version: "unknown", channel: "local" }): {
serviceName: string
serviceVersion: string
attributes: Record<string, string>
} {
return {
serviceName: "opencode",
serviceVersion: app.version,
attributes: {
...resourceAttributes(),
"deployment.environment.name": app.channel,
"opencode.client": app.client,
"opencode.run": runID,
"service.instance.id": runID,
},
}
}
export function loggers(options: Options | undefined, app: App) {
if (!options?.endpoint) return []
return [
OtlpLogger.make({
url: `${options.endpoint}/v1/logs`,
resource: resource(app),
headers: parseHeaders(options.headers),
}),
]
}
export async function tracingLayer(options: Options | undefined, app: App) {
if (!options?.endpoint) return Layer.empty
const NodeSdk = await import("@effect/opentelemetry/NodeSdk")
const OTLP = await import("@opentelemetry/exporter-trace-otlp-http")
const SdkBase = await import("@opentelemetry/sdk-trace-base")
const { AsyncLocalStorageContextManager } = await import("@opentelemetry/context-async-hooks")
const { context } = await import("@opentelemetry/api")
// The Effect Node SDK does not register a global context manager, but the AI SDK uses it to parent spans.
const manager = new AsyncLocalStorageContextManager()
manager.enable()
context.setGlobalContextManager(manager)
return NodeSdk.layer(() => ({
resource: resource(app),
spanProcessor: new SdkBase.BatchSpanProcessor(
new OTLP.OTLPTraceExporter({
url: `${options.endpoint}/v1/traces`,
headers: parseHeaders(options.headers),
}),
),
}))
}
export * as Otlp from "./otlp.js"