98 lines
2.8 KiB
TypeScript
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"
|