Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions packages/opencode/src/util/effect-log.ts
Original file line number Diff line number Diff line change
@@ -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)
}
99 changes: 99 additions & 0 deletions packages/opencode/test/util/effect-log.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { afterEach, expect, mock, spyOn, test } from "bun:test"
import { Cause, Effect } from "effect"
import { CurrentLogAnnotations, CurrentLogSpans } from "effect/References"

import * as EffectLog from "../../src/util/effect-log"
import { Log } from "../../src/util/log"

const debug = mock(() => {})
const info = mock(() => {})
const warn = mock(() => {})
const error = mock(() => {})

const logger = {
debug,
info,
warn,
error,
tag() {
return logger
},
clone() {
return logger
},
time() {
return {
stop() {},
[Symbol.dispose]() {},
}
},
}

afterEach(() => {
debug.mockClear()
info.mockClear()
warn.mockClear()
error.mockClear()
})

test("EffectLog.layer routes info logs through util/log", async () => {
using create = spyOn(Log, "create").mockReturnValue(logger)

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 () => {
using create = spyOn(Log, "create").mockReturnValue(logger)

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(create).toHaveBeenCalledWith({ 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", () => {
using create = spyOn(Log, "create").mockReturnValue(logger)
const effect = EffectLog.make({ service: "effect-test-struct" })

effect.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(create).toHaveBeenCalledWith({ service: "effect-test-struct" })
expect(warn).toHaveBeenCalledWith(
'{"hello":"world"}',
expect.objectContaining({
fiber: 123n,
}),
)
})
Loading