mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-24 05:35:15 +00:00
feat(log): add Effect logger compatibility layer
Add a small Effect logger bridge that routes Effect logs through the existing util/log backend so Effect-native services can adopt structured logging without changing the app's current file and stderr logging setup. Preserve level mapping and forward annotations, spans, and causes into the legacy logger metadata.
This commit is contained in:
parent
dce7eceb28
commit
9bbc874927
2 changed files with 153 additions and 0 deletions
56
packages/opencode/src/util/effect-log.ts
Normal file
56
packages/opencode/src/util/effect-log.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { Cause, Logger } from "effect"
|
||||
import { CurrentLogAnnotations, CurrentLogSpans } from "effect/References"
|
||||
|
||||
import { Log } from "./log"
|
||||
|
||||
function text(input: unknown): string {
|
||||
if (Array.isArray(input)) return input.map(text).join(" ")
|
||||
if (input instanceof Error) return input.message
|
||||
if (typeof input === "string") return input
|
||||
if (typeof input === "object" && input !== null) {
|
||||
try {
|
||||
return JSON.stringify(input)
|
||||
} catch {
|
||||
return String(input)
|
||||
}
|
||||
}
|
||||
return String(input)
|
||||
}
|
||||
|
||||
export function make(tags?: Record<string, unknown>) {
|
||||
const log = Log.create(tags)
|
||||
|
||||
return Logger.make<unknown, void>((options) => {
|
||||
const annotations = options.fiber.getRef(CurrentLogAnnotations as never) as Readonly<Record<string, unknown>>
|
||||
const spans = options.fiber.getRef(CurrentLogSpans as never) as ReadonlyArray<readonly [string, number]>
|
||||
const extra = {
|
||||
...annotations,
|
||||
fiber: options.fiber.id,
|
||||
spans: spans.length
|
||||
? spans.map(([label, start]) => ({
|
||||
label,
|
||||
duration: options.date.getTime() - start,
|
||||
}))
|
||||
: undefined,
|
||||
cause: options.cause.reasons.length ? Cause.pretty(options.cause) : undefined,
|
||||
}
|
||||
|
||||
if (options.logLevel === "Debug" || options.logLevel === "Trace") {
|
||||
return log.debug(text(options.message), extra)
|
||||
}
|
||||
|
||||
if (options.logLevel === "Info") {
|
||||
return log.info(text(options.message), extra)
|
||||
}
|
||||
|
||||
if (options.logLevel === "Warn") {
|
||||
return log.warn(text(options.message), extra)
|
||||
}
|
||||
|
||||
return log.error(text(options.message), extra)
|
||||
})
|
||||
}
|
||||
|
||||
export function layer(tags?: Record<string, unknown>, options?: { mergeWithExisting?: boolean }) {
|
||||
return Logger.layer([make(tags)], options)
|
||||
}
|
||||
97
packages/opencode/test/util/effect-log.test.ts
Normal file
97
packages/opencode/test/util/effect-log.test.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import { beforeEach, expect, mock, test } from "bun:test"
|
||||
import { Cause, Effect } from "effect"
|
||||
import { CurrentLogAnnotations, CurrentLogSpans } from "effect/References"
|
||||
|
||||
const debug = mock(() => {})
|
||||
const info = mock(() => {})
|
||||
const warn = mock(() => {})
|
||||
const error = mock(() => {})
|
||||
const create = mock(() => ({
|
||||
debug,
|
||||
info,
|
||||
warn,
|
||||
error,
|
||||
tag() {
|
||||
return this
|
||||
},
|
||||
clone() {
|
||||
return this
|
||||
},
|
||||
time() {
|
||||
return {
|
||||
stop() {},
|
||||
[Symbol.dispose]() {},
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
mock.module("../../src/util/log", () => ({
|
||||
Log: {
|
||||
create,
|
||||
},
|
||||
}))
|
||||
|
||||
const EffectLog = await import("../../src/util/effect-log")
|
||||
|
||||
beforeEach(() => {
|
||||
create.mockClear()
|
||||
debug.mockClear()
|
||||
info.mockClear()
|
||||
warn.mockClear()
|
||||
error.mockClear()
|
||||
})
|
||||
|
||||
test("EffectLog.layer routes info logs through util/log", async () => {
|
||||
await Effect.runPromise(Effect.logInfo("hello").pipe(Effect.provide(EffectLog.layer({ service: "effect-test" }))))
|
||||
|
||||
expect(create).toHaveBeenCalledWith({ service: "effect-test" })
|
||||
expect(info).toHaveBeenCalledWith("hello", expect.any(Object))
|
||||
})
|
||||
|
||||
test("EffectLog.layer forwards annotations and spans to util/log", async () => {
|
||||
await Effect.runPromise(
|
||||
Effect.logInfo("hello").pipe(
|
||||
Effect.annotateLogs({ requestId: "req-123" }),
|
||||
Effect.withLogSpan("provider-auth"),
|
||||
Effect.provide(EffectLog.layer({ service: "effect-test-meta" })),
|
||||
),
|
||||
)
|
||||
|
||||
expect(info).toHaveBeenCalledWith(
|
||||
"hello",
|
||||
expect.objectContaining({
|
||||
requestId: "req-123",
|
||||
spans: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
label: "provider-auth",
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
test("EffectLog.make formats structured messages and causes for legacy logger", () => {
|
||||
const logger = EffectLog.make({ service: "effect-test-struct" })
|
||||
|
||||
logger.log({
|
||||
message: { hello: "world" },
|
||||
logLevel: "Warn",
|
||||
cause: Cause.fail(new Error("boom")),
|
||||
fiber: {
|
||||
id: 123n,
|
||||
getRef(ref: unknown) {
|
||||
if (ref === CurrentLogAnnotations) return {}
|
||||
if (ref === CurrentLogSpans) return []
|
||||
return undefined
|
||||
},
|
||||
},
|
||||
date: new Date(),
|
||||
} as never)
|
||||
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
'{"hello":"world"}',
|
||||
expect.objectContaining({
|
||||
fiber: 123n,
|
||||
}),
|
||||
)
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue