Skip to content

Commit 40358d6

Browse files
authored
refactor: add Effect logger for motel observability (#21954)
1 parent 96c1c03 commit 40358d6

4 files changed

Lines changed: 116 additions & 38 deletions

File tree

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { Cause, Effect, Logger, References } from "effect"
2+
import { Log } from "@/util/log"
3+
4+
export namespace EffectLogger {
5+
type Fields = Record<string, unknown>
6+
7+
export interface Handle {
8+
readonly debug: (msg?: unknown, extra?: Fields) => Effect.Effect<void>
9+
readonly info: (msg?: unknown, extra?: Fields) => Effect.Effect<void>
10+
readonly warn: (msg?: unknown, extra?: Fields) => Effect.Effect<void>
11+
readonly error: (msg?: unknown, extra?: Fields) => Effect.Effect<void>
12+
readonly with: (extra: Fields) => Handle
13+
}
14+
15+
const clean = (input?: Fields): Fields =>
16+
Object.fromEntries(Object.entries(input ?? {}).filter((entry) => entry[1] !== undefined && entry[1] !== null))
17+
18+
const text = (input: unknown): string => {
19+
if (Array.isArray(input)) return input.map((item) => String(item)).join(" ")
20+
return input === undefined ? "" : String(input)
21+
}
22+
23+
const call = (run: (msg?: unknown) => Effect.Effect<void>, base: Fields, msg?: unknown, extra?: Fields) => {
24+
const ann = clean({ ...base, ...extra })
25+
const fx = run(msg)
26+
return Object.keys(ann).length ? Effect.annotateLogs(fx, ann) : fx
27+
}
28+
29+
export const logger = Logger.make((opts) => {
30+
const extra = clean(opts.fiber.getRef(References.CurrentLogAnnotations))
31+
const now = opts.date.getTime()
32+
for (const [key, start] of opts.fiber.getRef(References.CurrentLogSpans)) {
33+
extra[`logSpan.${key}`] = `${now - start}ms`
34+
}
35+
if (opts.cause.reasons.length > 0) {
36+
extra.cause = Cause.pretty(opts.cause)
37+
}
38+
39+
const svc = typeof extra.service === "string" ? extra.service : undefined
40+
if (svc) delete extra.service
41+
const log = svc ? Log.create({ service: svc }) : Log.Default
42+
const msg = text(opts.message)
43+
44+
switch (opts.logLevel) {
45+
case "Trace":
46+
case "Debug":
47+
return log.debug(msg, extra)
48+
case "Warn":
49+
return log.warn(msg, extra)
50+
case "Error":
51+
case "Fatal":
52+
return log.error(msg, extra)
53+
default:
54+
return log.info(msg, extra)
55+
}
56+
})
57+
58+
export const layer = Logger.layer([logger], { mergeWithExisting: true })
59+
60+
export const create = (base: Fields = {}): Handle => ({
61+
debug: (msg, extra) => call((item) => Effect.logDebug(item), base, msg, extra),
62+
info: (msg, extra) => call((item) => Effect.logInfo(item), base, msg, extra),
63+
warn: (msg, extra) => call((item) => Effect.logWarning(item), base, msg, extra),
64+
error: (msg, extra) => call((item) => Effect.logError(item), base, msg, extra),
65+
with: (extra) => create({ ...base, ...extra }),
66+
})
67+
}
Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,45 @@
1-
import { Layer } from "effect"
1+
import { Duration, Layer } from "effect"
22
import { FetchHttpClient } from "effect/unstable/http"
33
import { Otlp } from "effect/unstable/observability"
4+
import { EffectLogger } from "@/effect/logger"
45
import { Flag } from "@/flag/flag"
56
import { CHANNEL, VERSION } from "@/installation/meta"
67

78
export namespace Observability {
89
export const enabled = !!Flag.OTEL_EXPORTER_OTLP_ENDPOINT
910

10-
export const layer = !Flag.OTEL_EXPORTER_OTLP_ENDPOINT
11-
? Layer.empty
12-
: Otlp.layerJson({
13-
baseUrl: Flag.OTEL_EXPORTER_OTLP_ENDPOINT,
14-
loggerMergeWithExisting: false,
15-
resource: {
16-
serviceName: "opencode",
17-
serviceVersion: VERSION,
18-
attributes: {
19-
"deployment.environment.name": CHANNEL === "local" ? "local" : CHANNEL,
20-
"opencode.client": Flag.OPENCODE_CLIENT,
21-
},
11+
const base = Flag.OTEL_EXPORTER_OTLP_ENDPOINT
12+
13+
const resource = {
14+
serviceName: "opencode",
15+
serviceVersion: VERSION,
16+
attributes: {
17+
"deployment.environment.name": CHANNEL === "local" ? "local" : CHANNEL,
18+
"opencode.client": Flag.OPENCODE_CLIENT,
19+
},
20+
}
21+
22+
const headers = Flag.OTEL_EXPORTER_OTLP_HEADERS
23+
? Flag.OTEL_EXPORTER_OTLP_HEADERS.split(",").reduce(
24+
(acc, x) => {
25+
const [key, value] = x.split("=")
26+
acc[key] = value
27+
return acc
2228
},
23-
headers: Flag.OTEL_EXPORTER_OTLP_HEADERS
24-
? Flag.OTEL_EXPORTER_OTLP_HEADERS.split(",").reduce(
25-
(acc, x) => {
26-
const [key, value] = x.split("=")
27-
acc[key] = value
28-
return acc
29-
},
30-
{} as Record<string, string>,
31-
)
32-
: undefined,
33-
}).pipe(Layer.provide(FetchHttpClient.layer))
29+
{} as Record<string, string>,
30+
)
31+
: undefined
32+
33+
export const layer = !base
34+
? EffectLogger.layer
35+
: Layer.mergeAll(
36+
EffectLogger.layer,
37+
Otlp.layerJson({
38+
baseUrl: base,
39+
loggerExportInterval: Duration.seconds(5),
40+
loggerMergeWithExisting: true,
41+
resource,
42+
headers,
43+
}),
44+
).pipe(Layer.provide(FetchHttpClient.layer))
3445
}

packages/opencode/src/session/processor.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Config } from "@/config/config"
66
import { Permission } from "@/permission"
77
import { Plugin } from "@/plugin"
88
import { Snapshot } from "@/snapshot"
9-
import { Log } from "@/util/log"
9+
import { EffectLogger } from "@/effect/logger"
1010
import { Session } from "."
1111
import { LLM } from "./llm"
1212
import { MessageV2 } from "./message-v2"
@@ -23,7 +23,7 @@ import { isRecord } from "@/util/record"
2323

2424
export namespace SessionProcessor {
2525
const DOOM_LOOP_THRESHOLD = 3
26-
const log = Log.create({ service: "session.processor" })
26+
const log = EffectLogger.create({ service: "session.processor" })
2727

2828
export type Result = "compact" | "stop" | "continue"
2929

@@ -121,6 +121,7 @@ export namespace SessionProcessor {
121121
reasoningMap: {},
122122
}
123123
let aborted = false
124+
const slog = log.with({ sessionID: input.sessionID, messageID: input.assistantMessage.id })
124125

125126
const parse = (e: unknown) =>
126127
MessageV2.fromError(e, {
@@ -448,7 +449,7 @@ export namespace SessionProcessor {
448449
return
449450

450451
default:
451-
log.info("unhandled", { ...value })
452+
yield* slog.info("unhandled", { event: value.type, value })
452453
return
453454
}
454455
})
@@ -514,7 +515,7 @@ export namespace SessionProcessor {
514515
})
515516

516517
const halt = Effect.fn("SessionProcessor.halt")(function* (e: unknown) {
517-
log.error("process", { error: e, stack: e instanceof Error ? e.stack : undefined })
518+
yield* slog.error("process", { error: errorMessage(e), stack: e instanceof Error ? e.stack : undefined })
518519
const error = parse(e)
519520
if (MessageV2.ContextOverflowError.isInstance(error)) {
520521
ctx.needsCompaction = true
@@ -530,7 +531,7 @@ export namespace SessionProcessor {
530531
})
531532

532533
const process = Effect.fn("SessionProcessor.process")(function* (streamInput: LLM.StreamInput) {
533-
log.info("process")
534+
yield* slog.info("process")
534535
ctx.needsCompaction = false
535536
ctx.shouldBreak = (yield* config.get()).experimental?.continue_loop_on_deny !== true
536537

packages/opencode/src/session/prompt.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { Truncate } from "@/tool/truncate"
4444
import { decodeDataUrl } from "@/util/data-url"
4545
import { Process } from "@/util/process"
4646
import { Cause, Effect, Exit, Layer, Option, Scope, ServiceMap } from "effect"
47+
import { EffectLogger } from "@/effect/logger"
4748
import { InstanceState } from "@/effect/instance-state"
4849
import { makeRuntime } from "@/effect/run-service"
4950
import { TaskTool, type TaskPromptOps } from "@/tool/task"
@@ -64,6 +65,7 @@ const STRUCTURED_OUTPUT_SYSTEM_PROMPT = `IMPORTANT: The user has requested struc
6465

6566
export namespace SessionPrompt {
6667
const log = Log.create({ service: "session.prompt" })
68+
const elog = EffectLogger.create({ service: "session.prompt" })
6769

6870
export interface Interface {
6971
readonly cancel: (sessionID: SessionID) => Effect.Effect<void>
@@ -102,7 +104,7 @@ export namespace SessionPrompt {
102104
const revert = yield* SessionRevert.Service
103105

104106
const cancel = Effect.fn("SessionPrompt.cancel")(function* (sessionID: SessionID) {
105-
log.info("cancel", { sessionID })
107+
yield* elog.info("cancel", { sessionID })
106108
yield* state.cancel(sessionID)
107109
})
108110

@@ -196,11 +198,7 @@ export namespace SessionPrompt {
196198
const t = cleaned.length > 100 ? cleaned.substring(0, 97) + "..." : cleaned
197199
yield* sessions
198200
.setTitle({ sessionID: input.session.id, title: t })
199-
.pipe(
200-
Effect.catchCause((cause) =>
201-
Effect.sync(() => log.error("failed to generate title", { error: Cause.squash(cause) })),
202-
),
203-
)
201+
.pipe(Effect.catchCause((cause) => elog.error("failed to generate title", { error: Cause.squash(cause) })))
204202
})
205203

206204
const insertReminders = Effect.fn("SessionPrompt.insertReminders")(function* (input: {
@@ -1302,13 +1300,14 @@ NOTE: At any point in time through this workflow you should feel free to ask the
13021300
const runLoop: (sessionID: SessionID) => Effect.Effect<MessageV2.WithParts> = Effect.fn("SessionPrompt.run")(
13031301
function* (sessionID: SessionID) {
13041302
const ctx = yield* InstanceState.context
1303+
const slog = elog.with({ sessionID })
13051304
let structured: unknown | undefined
13061305
let step = 0
13071306
const session = yield* sessions.get(sessionID)
13081307

13091308
while (true) {
13101309
yield* status.set(sessionID, { type: "busy" })
1311-
log.info("loop", { step, sessionID })
1310+
yield* slog.info("loop", { step })
13121311

13131312
let msgs = yield* MessageV2.filterCompactedEffect(sessionID)
13141313

@@ -1344,7 +1343,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
13441343
!hasToolCalls &&
13451344
lastUser.id < lastAssistant.id
13461345
) {
1347-
log.info("exiting loop", { sessionID })
1346+
yield* slog.info("exiting loop")
13481347
break
13491348
}
13501349

@@ -1540,7 +1539,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
15401539
)
15411540

15421541
const command = Effect.fn("SessionPrompt.command")(function* (input: CommandInput) {
1543-
log.info("command", input)
1542+
yield* elog.info("command", { sessionID: input.sessionID, command: input.command, agent: input.agent })
15441543
const cmd = yield* commands.get(input.command)
15451544
if (!cmd) {
15461545
const available = (yield* commands.list()).map((c) => c.name)

0 commit comments

Comments
 (0)