refactor(core): replace legacy logger with Effect logging (#31310)

This commit is contained in:
Dax 2026-06-08 15:41:56 -04:00 committed by GitHub
commit c06ad7c881
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
152 changed files with 697 additions and 2242 deletions

View 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"

View 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"

View file

@ -0,0 +1 @@
export const runID = crypto.randomUUID().slice(0, 8)