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, ) : 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 } { 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"