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
parent 0a7cb20e66
commit c06ad7c881
152 changed files with 698 additions and 2243 deletions
+1 -4
View File
@@ -1,4 +1,3 @@
import * as Log from "@opencode-ai/core/util/log"
import { Effect } from "effect"
import { effectCmd } from "../effect-cmd"
import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"
@@ -7,8 +6,6 @@ import { createOpencodeClient } from "@opencode-ai/sdk/v2"
import { withNetworkOptions, resolveNetworkOptions } from "../network"
import { ACPProfile } from "@/acp/profile"
const log = Log.create({ service: "acp-command" })
export const AcpCommand = effectCmd({
command: "acp",
describe: "start ACP (Agent Client Protocol) server",
@@ -63,7 +60,7 @@ export const AcpCommand = effectCmd({
return agent.create(conn)
}, stream)
log.info("setup connection")
yield* Effect.logInfo("setup connection")
process.stdin.resume()
yield* Effect.promise(
() =>
+2 -3
View File
@@ -2,7 +2,6 @@ import { LSP } from "@/lsp/lsp"
import { Effect } from "effect"
import { effectCmd } from "../../effect-cmd"
import { cmd } from "../cmd"
import * as Log from "@opencode-ai/core/util/log"
import { EOL } from "os"
export const LSPCommand = cmd({
@@ -33,7 +32,7 @@ export const SymbolsCommand = effectCmd({
describe: "search workspace symbols",
builder: (yargs) => yargs.positional("query", { type: "string", demandOption: true }),
handler: Effect.fn("Cli.debug.lsp.symbols")(function* (args) {
using _ = Log.Default.time("symbols")
yield* Effect.logInfo("symbols")
const results = yield* LSP.Service.use((lsp) => lsp.workspaceSymbol(args.query))
process.stdout.write(JSON.stringify(results, null, 2) + EOL)
}),
@@ -44,7 +43,7 @@ export const DocumentSymbolsCommand = effectCmd({
describe: "get symbols from a document",
builder: (yargs) => yargs.positional("uri", { type: "string", demandOption: true }),
handler: Effect.fn("Cli.debug.lsp.documentSymbols")(function* (args) {
using _ = Log.Default.time("document-symbols")
yield* Effect.logInfo("document-symbols")
const results = yield* LSP.Service.use((lsp) => lsp.documentSymbol(args.uri))
process.stdout.write(JSON.stringify(results, null, 2) + EOL)
}),
@@ -1,5 +1,4 @@
import { EOL } from "os"
import * as Log from "@opencode-ai/core/util/log"
import { cmd } from "../cmd"
export const ScrapCommand = cmd({
@@ -10,9 +9,7 @@ export const ScrapCommand = cmd({
const { Project } = await import("@/project/project")
const { makeRuntime } = await import("@opencode-ai/core/effect/runtime")
const runtime = makeRuntime(Project.Service, Project.defaultLayer)
const timer = Log.Default.time("scrap")
const list = await runtime.runPromise((project) => project.list())
process.stdout.write(JSON.stringify(list, null, 2) + EOL)
timer.stop()
},
})
+6 -30
View File
@@ -30,7 +30,6 @@ import { render } from "@opentui/solid"
import { createComponent, createSignal, type Accessor, type Setter } from "solid-js"
import { createStore, reconcile } from "solid-js/store"
import { OpencodeKeymapProvider } from "@opencode-ai/tui/keymap"
import { withRunSpan } from "./otel"
import { RUN_COMMAND_PANEL_ROWS, RUN_SUBAGENT_PANEL_ROWS } from "./footer.command"
import { SUBAGENT_INSPECTOR_ROWS } from "./footer.subagent"
import { PROMPT_MAX_ROWS, TEXTAREA_MIN_ROWS } from "./footer.prompt"
@@ -513,20 +512,8 @@ export class RunFooter implements FooterApi {
}
private completeScrollback(): void {
const phase = this.state().phase
this.flushing = this.flushing
.then(() =>
withRunSpan(
"RunFooter.completeScrollback",
{
"opencode.footer.phase": phase,
"session.id": this.options.sessionID() || undefined,
},
async () => {
await this.scrollback.complete()
},
),
)
.then(() => this.scrollback.complete())
.catch((error) => {
this.flushError = error
})
@@ -1129,23 +1116,12 @@ export class RunFooter implements FooterApi {
}
const batch = this.queue.splice(0)
const phase = this.state().phase
this.flushing = this.flushing
.then(() =>
withRunSpan(
"RunFooter.flush",
{
"opencode.batch.commits": batch.length,
"opencode.footer.phase": phase,
"session.id": this.options.sessionID() || undefined,
},
async () => {
for (const item of batch) {
await this.scrollback.append(item)
}
},
),
)
.then(async () => {
for (const item of batch) {
await this.scrollback.append(item)
}
})
.catch((error) => {
this.flushError = error
})
-117
View File
@@ -1,117 +0,0 @@
import { INVALID_SPAN_CONTEXT, context, trace, SpanStatusCode, type Span } from "@opentelemetry/api"
import { Effect, ManagedRuntime } from "effect"
import { memoMap } from "@opencode-ai/core/effect/memo-map"
import { Observability } from "@opencode-ai/core/effect/observability"
type AttributeValue = string | number | boolean | undefined
export type RunSpanAttributes = Record<string, AttributeValue>
const noop = trace.wrapSpanContext(INVALID_SPAN_CONTEXT)
const tracer = trace.getTracer("opencode.run")
const runtime = ManagedRuntime.make(Observability.layer, { memoMap })
let ready: Promise<void> | undefined
function attributes(input?: RunSpanAttributes): Record<string, string | number | boolean> | undefined {
if (!input) {
return undefined
}
const out = Object.entries(input).flatMap(([key, value]) => (value === undefined ? [] : [[key, value] as const]))
if (out.length === 0) {
return undefined
}
return Object.fromEntries(out)
}
function message(error: unknown) {
if (typeof error === "string") {
return error
}
if (error instanceof Error) {
return error.message || error.name
}
return String(error)
}
function ensure() {
if (!Observability.enabled) {
return Promise.resolve()
}
if (ready) {
return ready
}
ready = runtime.runPromise(Effect.void).then(
() => undefined,
(error) => {
ready = undefined
throw error
},
)
return ready
}
function finish<A>(span: Span, out: Promise<A>) {
return out.then(
(value) => {
span.end()
return value
},
(error) => {
recordRunSpanError(span, error)
span.end()
throw error
},
)
}
export function setRunSpanAttributes(span: Span, input?: RunSpanAttributes): void {
const next = attributes(input)
if (!next) {
return
}
span.setAttributes(next)
}
export function recordRunSpanError(span: Span, error: unknown): void {
const next = message(error)
span.recordException(error instanceof Error ? error : next)
span.setStatus({
code: SpanStatusCode.ERROR,
message: next,
})
}
export function withRunSpan<A>(
name: string,
input: RunSpanAttributes | undefined,
fn: (span: Span) => Promise<A> | A,
): A | Promise<A> {
if (!Observability.enabled) {
return fn(noop)
}
return ensure().then(
() => {
const span = tracer.startSpan(name, {
attributes: attributes(input),
})
return context.with(trace.setSpan(context.active(), span), () =>
finish(
span,
new Promise<A>((resolve) => {
resolve(fn(span))
}),
),
)
},
() => fn(noop),
)
}
@@ -16,7 +16,6 @@ import { openEditor } from "@opencode-ai/tui/editor"
import { registerOpencodeKeymap } from "@opencode-ai/tui/keymap"
import { Session as SessionApi } from "@/session/session"
import * as Locale from "@/util/locale"
import { withRunSpan } from "./otel"
import { resolveInteractiveStdin } from "./runtime.stdin"
import { entrySplash, exitSplash, splashMeta } from "./splash"
import { resolveRunTheme } from "./theme"
@@ -175,18 +174,6 @@ function queueSplash(
// scrollback commits and footer repaints happen in the same frame. After
// the entry splash, RunFooter takes over the footer region.
export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lifecycle> {
return withRunSpan(
"RunLifecycle.boot",
{
"opencode.agent.name": input.agent,
"opencode.directory": input.directory,
"opencode.first": input.first,
"opencode.model.provider": input.model?.providerID,
"opencode.model.id": input.model?.modelID,
"opencode.model.variant": input.variant,
"session.id": input.getSessionID?.() || input.sessionID || undefined,
},
async () => {
const source = resolveInteractiveStdin()
let unregisterKeymap: (() => void) | undefined
@@ -326,13 +313,6 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
}
closed = true
return withRunSpan(
"RunLifecycle.close",
{
"opencode.show_exit": next.showExit,
"session.id": next.sessionID || input.getSessionID?.() || input.sessionID || undefined,
},
async () => {
detachSigint()
let wroteExit = false
@@ -368,8 +348,6 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
}
source.cleanup?.()
}
},
)
}
return {
@@ -425,6 +403,4 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
source.cleanup?.()
throw error
}
},
)
}
+1 -89
View File
@@ -18,7 +18,6 @@ import { MessageID } from "@/session/schema"
import { createRunDemo } from "./demo"
import { resolveModelInfo, resolveRunTuiConfig, resolveSessionInfo } from "./runtime.boot"
import { createRuntimeLifecycle } from "./runtime.lifecycle"
import { recordRunSpanError, setRunSpanAttributes, withRunSpan } from "./otel"
import { trace } from "./trace"
import { cycleVariant, formatModelLabel, resolveSavedVariant, resolveVariant, saveVariant } from "./variant.shared"
import type { LocalReplayAnchor, LocalReplayRow, RunInput, RunPrompt, RunProvider, StreamCommit } from "./types"
@@ -180,14 +179,6 @@ async function resolveExitTitle(
// Files only attach on the first prompt turn -- after that, includeFiles
// flips to false so subsequent turns don't re-send attachments.
async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDeps = {}): Promise<void> {
return withRunSpan(
"RunInteractive.session",
{
"opencode.mode": input.resolveSession ? "local" : "attach",
"opencode.initial_input": !!input.initialInput,
"opencode.demo": input.demo,
},
async (span) => {
const start = performance.now()
const log = trace()
const tuiConfigTask = resolveRunTuiConfig()
@@ -217,15 +208,6 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
sessionTitle: ctx.sessionTitle,
agent: ctx.agent,
}
setRunSpanAttributes(span, {
"opencode.directory": ctx.directory,
"opencode.resume": ctx.resume === true,
"opencode.agent.name": state.agent,
"opencode.model.provider": state.model?.providerID,
"opencode.model.id": state.model?.modelID,
"opencode.model.variant": state.activeVariant,
"session.id": state.sessionID || undefined,
})
const ensureSession = () => {
if (!input.resolveSession || state.sessionID) {
return Promise.resolve()
@@ -239,10 +221,6 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
state.sessionID = next.sessionID
state.sessionTitle = next.sessionTitle ?? state.sessionTitle
state.agent = next.agent
setRunSpanAttributes(span, {
"opencode.agent.name": state.agent,
"session.id": state.sessionID,
})
})
return state.session
}
@@ -297,9 +275,6 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
state.activeVariant = cycleVariant(state.activeVariant, state.variants)
saveVariant(state.model, state.activeVariant)
setRunSpanAttributes(span, {
"opencode.model.variant": state.activeVariant,
})
return {
status: state.activeVariant ? `variant ${state.activeVariant}` : "variant default",
modelLabel: formatModelLabel(state.model, state.activeVariant, state.providers),
@@ -333,11 +308,6 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
return
}
setRunSpanAttributes(span, {
"opencode.model.provider": model.providerID,
"opencode.model.id": model.modelID,
"opencode.model.variant": state.activeVariant,
})
return {
modelLabel: formatModelLabel(model, state.activeVariant, state.providers),
status: `model ${model.modelID}`,
@@ -360,9 +330,6 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
state.activeVariant = variant
saveVariant(state.model, state.activeVariant)
setRunSpanAttributes(span, {
"opencode.model.variant": state.activeVariant,
})
return {
status: state.activeVariant ? `variant ${state.activeVariant}` : "variant default",
modelLabel: formatModelLabel(state.model, state.activeVariant, state.providers),
@@ -468,9 +435,6 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
const next = resolveVariant(ctx.variant, session.variant, savedVariant, state.variants)
if (next !== state.activeVariant) {
state.activeVariant = next
setRunSpanAttributes(span, {
"opencode.model.variant": state.activeVariant,
})
}
if (footer.isClosed) {
@@ -624,13 +588,6 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
limits: () => state.limits,
})
: undefined
setRunSpanAttributes(span, {
"opencode.agent.name": state.agent,
"opencode.model.provider": state.model?.providerID,
"opencode.model.id": state.model?.modelID,
"opencode.model.variant": state.activeVariant,
"session.id": state.sessionID,
})
log?.write("session.new", {
sessionID: state.sessionID,
})
@@ -688,29 +645,8 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
await state.switching?.catch(() => {})
let outputAnchor: LocalReplayAnchor | undefined
return withRunSpan(
"RunInteractive.turn",
{
"opencode.agent.name": state.agent,
"opencode.model.provider": state.model?.providerID,
"opencode.model.id": state.model?.modelID,
"opencode.model.variant": state.activeVariant,
"opencode.prompt.chars": prompt.text.length,
"opencode.prompt.parts": prompt.parts.length,
"opencode.prompt.include_files": includeFiles,
"opencode.prompt.file_parts": includeFiles ? input.files.length : 0,
"session.id": state.sessionID || undefined,
},
async (span) => {
try {
const next = await ensureStream()
setRunSpanAttributes(span, {
"opencode.agent.name": state.agent,
"opencode.model.provider": state.model?.providerID,
"opencode.model.id": state.model?.modelID,
"opencode.model.variant": state.activeVariant,
"session.id": state.sessionID || undefined,
})
await next.handle.runPromptTurn({
agent: state.agent,
model: state.model,
@@ -734,7 +670,6 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
return
}
recordRunSpanError(span, error)
const text =
(await state.stream?.then((item) => item.mod).catch(() => undefined))?.formatUnknownError(error) ??
(error instanceof Error ? error.message : String(error))
@@ -748,8 +683,6 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
rememberLocal(commit, outputAnchor)
footer.append(commit)
}
},
)
},
})
}
@@ -795,21 +728,11 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
history: state.history,
})
}
},
)
}
// Local in-process mode. Creates an SDK client backed by a direct fetch to
// the in-process server, so no external HTTP server is needed.
export async function runInteractiveLocalMode(input: RunLocalInput): Promise<void> {
return withRunSpan(
"RunInteractive.localMode",
{
"opencode.directory": input.directory,
"opencode.initial_input": !!input.initialInput,
"opencode.demo": input.demo,
},
async () => {
const sdk = createOpencodeClient({
baseUrl: "http://opencode.internal",
fetch: input.fetch,
@@ -858,8 +781,6 @@ export async function runInteractiveLocalMode(input: RunLocalInput): Promise<voi
}
},
})
},
)
}
// Attach mode. Uses the caller-provided SDK client directly.
@@ -867,15 +788,7 @@ export async function runInteractiveMode(
input: RunInput & { createSession?: CreateSession },
deps?: RunRuntimeDeps,
): Promise<void> {
return withRunSpan(
"RunInteractive.attachMode",
{
"opencode.directory": input.directory,
"opencode.initial_input": !!input.initialInput,
"session.id": input.sessionID,
},
async () =>
runInteractiveRuntime(
return runInteractiveRuntime(
{
files: input.files,
initialInput: input.initialInput,
@@ -897,6 +810,5 @@ export async function runInteractiveMode(
createSession: createSessionResolver(input.createSession),
},
deps,
),
)
}
@@ -14,7 +14,6 @@ import {
type ScrollbackSurface,
} from "@opentui/core"
import { entryBody, entryCanStream, entryDone, entryFlags } from "./entry.body"
import { withRunSpan } from "./otel"
import { entryColor, entryLook, entrySyntax } from "./scrollback.shared"
import { turnSummaryCommit } from "./turn-summary"
import { entryWriter, sameEntryGroup, separatorRows, spacerWriter, turnSummaryWriter } from "./scrollback.writer"
@@ -424,17 +423,7 @@ export class RunScrollbackStream {
}
public async complete(trailingNewline = false): Promise<void> {
return withRunSpan(
"RunScrollbackStream.complete",
{
"opencode.entry.active": !!this.active,
"opencode.trailing_newline": trailingNewline,
"session.id": this.sessionID?.() || undefined,
},
async () => {
this.markRendered(await this.finishActive(trailingNewline))
},
)
this.markRendered(await this.finishActive(trailingNewline))
}
public async writeTurnSummary(input: { agent: string; model: string; duration: string }): Promise<void> {
+4 -44
View File
@@ -4,7 +4,6 @@ import { type rpc } from "../tui/worker"
import path from "path"
import { fileURLToPath } from "url"
import { UI } from "@/cli/ui"
import * as Log from "@opencode-ai/core/util/log"
import { errorMessage } from "@opencode-ai/tui/util/error"
import { withTimeout } from "@/util/timeout"
import { withNetworkOptions, resolveNetworkOptionsNoConfig } from "@/cli/network"
@@ -12,12 +11,6 @@ import { Filesystem } from "@/util/filesystem"
import type { GlobalEvent } from "@opencode-ai/sdk/v2"
import type { EventSource } from "@opencode-ai/tui/context/sdk"
import { writeHeapSnapshot } from "v8"
import {
OPENCODE_PROCESS_ROLE,
OPENCODE_RUN_ID,
ensureRunID,
sanitizedProcessEnv,
} from "@opencode-ai/core/util/opencode-process"
import { validateSession } from "../tui/validate-session"
import { win32InstallCtrlCGuard } from "@opencode-ai/tui/terminal-win32"
@@ -132,51 +125,20 @@ export const TuiThreadCommand = cmd({
return
}
const cwd = Filesystem.resolve(process.cwd())
const env = sanitizedProcessEnv({
[OPENCODE_PROCESS_ROLE]: "worker",
[OPENCODE_RUN_ID]: ensureRunID(),
})
const worker = new Worker(file, {
env,
})
worker.onerror = (e) => {
Log.Default.error("thread error", {
message: e.message,
filename: e.filename,
lineno: e.lineno,
colno: e.colno,
error: e.error,
})
}
const worker = new Worker(file)
const client = Rpc.client<typeof rpc>(worker)
const error = (e: unknown) => {
Log.Default.error("process error", { error: errorMessage(e) })
}
const reload = () => {
client.call("reload", undefined).catch((err) => {
Log.Default.warn("worker reload failed", {
error: errorMessage(err),
})
})
client.call("reload", undefined).catch(() => {})
}
process.on("uncaughtException", error)
process.on("unhandledRejection", error)
process.on("SIGUSR2", reload)
let stopped = false
const stop = async () => {
if (stopped) return
stopped = true
process.off("uncaughtException", error)
process.off("unhandledRejection", error)
process.off("SIGUSR2", reload)
await withTimeout(client.call("shutdown", undefined), 5000).catch((error) => {
Log.Default.warn("worker shutdown failed", {
error: errorMessage(error),
})
})
await withTimeout(client.call("shutdown", undefined), 5000).catch(() => {})
worker.terminate()
}
@@ -255,9 +217,7 @@ export const TuiThreadCommand = cmd({
} finally {
try {
unguard?.()
} catch (error) {
Log.Default.warn("failed to restore terminal guard", { error: errorMessage(error) })
}
} catch {}
}
},
})
+1 -15
View File
@@ -2,9 +2,6 @@ import path from "path"
import { writeHeapSnapshot } from "node:v8"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Global } from "@opencode-ai/core/global"
import * as Log from "@opencode-ai/core/util/log"
const log = Log.create({ service: "heap" })
const MINUTE = 60_000
const LIMIT = 2 * 1024 * 1024 * 1024
@@ -32,20 +29,9 @@ export function start() {
Global.Path.log,
`heap-${process.pid}-${new Date().toISOString().replace(/[:.]/g, "")}.heapsnapshot`,
)
log.warn("heap usage exceeded limit", {
rss: stat.rss,
heap: stat.heapUsed,
file,
})
await Promise.resolve()
.then(() => writeHeapSnapshot(file))
.catch((err) => {
log.error("failed to write heap snapshot", {
error: err instanceof Error ? err.message : String(err),
file,
})
})
.catch(() => {})
lock = false
}
-28
View File
@@ -1,6 +1,4 @@
import { Installation } from "@/installation"
import { Server } from "@/server/server"
import * as Log from "@opencode-ai/core/util/log"
import { InstanceRuntime } from "@/project/instance-runtime"
import { Rpc } from "@/util/rpc"
import { upgrade } from "@/cli/upgrade"
@@ -10,35 +8,11 @@ import { ServerAuth } from "@/server/auth"
import { writeHeapSnapshot } from "node:v8"
import { Heap } from "@/cli/heap"
import { AppRuntime } from "@/effect/app-runtime"
import { ensureProcessMetadata } from "@opencode-ai/core/util/opencode-process"
import { Effect } from "effect"
import { disposeAllInstancesAndEmitGlobalDisposed } from "@/server/global-lifecycle"
ensureProcessMetadata("worker")
await Log.init({
print: process.argv.includes("--print-logs"),
dev: Installation.isLocal(),
level: (() => {
if (Installation.isLocal()) return "DEBUG"
return "INFO"
})(),
})
Heap.start()
process.on("unhandledRejection", (e) => {
Log.Default.error("rejection", {
e: e instanceof Error ? e.message : e,
})
})
process.on("uncaughtException", (e) => {
Log.Default.error("exception", {
e: e instanceof Error ? e.message : e,
})
})
// Subscribe to global events and forward them via RPC
GlobalBus.on("event", (event) => {
Rpc.emit("global.event", event)
@@ -89,8 +63,6 @@ export const rpc = {
)
},
async shutdown() {
Log.Default.info("worker shutting down")
await InstanceRuntime.disposeAllInstances()
if (server) await server.stop(true)
},