refactor(core): replace legacy logger with Effect logging (#31310)
This commit is contained in:
parent
0a7cb20e66
commit
c06ad7c881
152 changed files with 697 additions and 2242 deletions
66
packages/core/src/observability/logging.ts
Normal file
66
packages/core/src/observability/logging.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { Formatter, Logger, type LogLevel } from "effect"
|
||||
import path from "path"
|
||||
import { Global } from "../global"
|
||||
import { runID } from "./shared"
|
||||
|
||||
function formatter(id: string = runID) {
|
||||
return Logger.map(Logger.formatStructured, (output) => {
|
||||
const messages = Array.isArray(output.message) ? output.message : [output.message]
|
||||
return [
|
||||
["timestamp", output.timestamp],
|
||||
["level", output.level],
|
||||
["run", id],
|
||||
...messages.flatMap((value) => (plain(value) ? flatten(value) : [["message", value] as const])),
|
||||
...(output.cause === undefined ? [] : [["cause", output.cause] as const]),
|
||||
...flatten(output.spans),
|
||||
...flatten(output.annotations),
|
||||
]
|
||||
.map(([key, value]) => `${key}=${format(value)}`)
|
||||
.join(" ")
|
||||
})
|
||||
}
|
||||
|
||||
function flatten(input: Record<string, unknown>, prefix = "", seen = new WeakSet<object>()): Array<readonly [string, unknown]> {
|
||||
if (seen.has(input)) return [[prefix, "[Circular]"]]
|
||||
seen.add(input)
|
||||
const entries = Object.entries(input)
|
||||
if (entries.length === 0 && prefix) return [[prefix, input]]
|
||||
return entries.flatMap(([key, value]) => {
|
||||
const path = prefix ? `${prefix}.${key}` : key
|
||||
return plain(value) ? flatten(value, path, seen) : [[path, value] as const]
|
||||
})
|
||||
}
|
||||
|
||||
function plain(input: unknown): input is Record<string, unknown> {
|
||||
if (input === null || typeof input !== "object" || Array.isArray(input)) return false
|
||||
const prototype = Object.getPrototypeOf(input)
|
||||
return prototype === Object.prototype || prototype === null
|
||||
}
|
||||
|
||||
function format(input: unknown) {
|
||||
const value = typeof input === "string" ? input : Formatter.format(input)
|
||||
return /^[^\s="\\]+$/.test(value) ? value : JSON.stringify(value)
|
||||
}
|
||||
|
||||
export function fileLogger(file = path.join(Global.Path.log, "opencode.log"), id: string = runID) {
|
||||
return Logger.toFile(formatter(id), file, { flag: "a", batchWindow: 0 })
|
||||
}
|
||||
|
||||
const stderrLogger = Logger.make((options) => process.stderr.write(formatter().log(options) + "\n"))
|
||||
|
||||
export function minimumLogLevel() {
|
||||
const value = process.env.OPENCODE_LOG_LEVEL?.toUpperCase()
|
||||
const levels = {
|
||||
DEBUG: "Debug",
|
||||
INFO: "Info",
|
||||
WARN: "Warn",
|
||||
ERROR: "Error",
|
||||
} as const satisfies Record<string, LogLevel.LogLevel>
|
||||
return value && value in levels ? levels[value as keyof typeof levels] : levels.INFO
|
||||
}
|
||||
|
||||
export function loggers() {
|
||||
return process.env.OPENCODE_PRINT_LOGS === "1" ? [fileLogger(), stderrLogger] : [fileLogger()]
|
||||
}
|
||||
|
||||
export * as Logging from "./logging"
|
||||
79
packages/core/src/observability/otlp.ts
Normal file
79
packages/core/src/observability/otlp.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import { Layer } from "effect"
|
||||
import { OtlpLogger } from "effect/unstable/observability"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { InstallationChannel, InstallationVersion } from "../installation/version"
|
||||
import { runID } from "./shared"
|
||||
|
||||
const endpoint = Flag.OTEL_EXPORTER_OTLP_ENDPOINT
|
||||
|
||||
const headers = Flag.OTEL_EXPORTER_OTLP_HEADERS
|
||||
? Flag.OTEL_EXPORTER_OTLP_HEADERS.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(): { serviceName: string; serviceVersion: string; attributes: Record<string, string> } {
|
||||
return {
|
||||
serviceName: "opencode",
|
||||
serviceVersion: InstallationVersion,
|
||||
attributes: {
|
||||
...resourceAttributes(),
|
||||
"deployment.environment.name": InstallationChannel,
|
||||
"opencode.client": Flag.OPENCODE_CLIENT,
|
||||
"opencode.run": runID,
|
||||
"service.instance.id": runID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function loggers() {
|
||||
if (!endpoint) return []
|
||||
return [OtlpLogger.make({ url: `${endpoint}/v1/logs`, resource: resource(), headers })]
|
||||
}
|
||||
|
||||
export async function tracingLayer() {
|
||||
if (!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(),
|
||||
spanProcessor: new SdkBase.BatchSpanProcessor(
|
||||
new OTLP.OTLPTraceExporter({
|
||||
url: `${endpoint}/v1/traces`,
|
||||
headers,
|
||||
}),
|
||||
),
|
||||
}))
|
||||
}
|
||||
|
||||
export * as Otlp from "./otlp"
|
||||
1
packages/core/src/observability/shared.ts
Normal file
1
packages/core/src/observability/shared.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const runID = crypto.randomUUID().slice(0, 8)
|
||||
Loading…
Add table
Add a link
Reference in a new issue