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,11 +1,8 @@
import { GlobalBus } from "@/bus/global"
import { InstanceStore } from "@/project/instance-store"
import * as Log from "@opencode-ai/core/util/log"
import { Effect } from "effect"
import { Event } from "./event"
const log = Log.create({ service: "server" })
export const emitGlobalDisposed = Effect.sync(() =>
GlobalBus.emit("event", {
directory: "global",
@@ -21,13 +18,7 @@ export const disposeAllInstancesAndEmitGlobalDisposed = Effect.fn("Server.dispos
const store = yield* InstanceStore.Service
yield* Effect.gen(function* () {
yield* options?.swallowErrors
? store.disposeAll().pipe(
Effect.catchCause((cause) =>
Effect.sync(() => {
log.warn("global disposal failed", { cause })
}),
),
)
? store.disposeAll().pipe(Effect.catchCause((cause) => Effect.logWarning("global disposal failed", { cause })))
: store.disposeAll()
yield* emitGlobalDisposed
}).pipe(Effect.uninterruptible)
+3 -16
View File
@@ -1,8 +1,5 @@
import * as Log from "@opencode-ai/core/util/log"
import { Bonjour } from "bonjour-service"
const log = Log.create({ service: "mdns" })
let bonjour: Bonjour | undefined
let currentPort: number | undefined
@@ -22,17 +19,10 @@ export function publish(port: number, domain?: string) {
txt: { path: "/" },
})
service.on("up", () => {
log.info("mDNS service published", { name, port })
})
service.on("error", (err) => {
log.error("mDNS service error", { error: err })
})
service.on("error", () => {})
currentPort = port
} catch (err) {
log.error("mDNS publish failed", { error: err })
} catch {
if (bonjour) {
try {
bonjour.destroy()
@@ -48,12 +38,9 @@ export function unpublish() {
try {
bonjour.unpublishAll()
bonjour.destroy()
} catch (err) {
log.error("mDNS unpublish failed", { error: err })
}
} catch {}
bonjour = undefined
currentPort = undefined
log.info("mDNS service unpublished")
}
}
@@ -1,6 +1,5 @@
import { Auth } from "@/auth"
import * as Log from "@opencode-ai/core/util/log"
import { Effect } from "effect"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { RootHttpApi } from "../api"
@@ -27,8 +26,15 @@ export const controlHandlers = HttpApiBuilder.group(RootHttpApi, "control", (han
})
const log = Effect.fn("ControlHttpApi.log")(function* (ctx: { payload: typeof LogInput.Type }) {
const logger = Log.create({ service: ctx.payload.service })
logger[ctx.payload.level](ctx.payload.message, ctx.payload.extra)
const write =
ctx.payload.level === "debug"
? Effect.logDebug
: ctx.payload.level === "info"
? Effect.logInfo
: ctx.payload.level === "warn"
? Effect.logWarning
: Effect.logError
yield* write(ctx.payload.message).pipe(Effect.annotateLogs(ctx.payload.extra ?? {}))
return true
})
@@ -2,7 +2,6 @@ import { EventV2Bridge } from "@/event-v2-bridge"
import { InstanceState } from "@/effect/instance-state"
import { GlobalBus } from "@/bus/global"
import { EventV2 } from "@opencode-ai/core/event"
import * as Log from "@opencode-ai/core/util/log"
import { Effect, Queue } from "effect"
import * as Stream from "effect/Stream"
import { HttpServerResponse } from "effect/unstable/http"
@@ -10,8 +9,6 @@ import { HttpApiBuilder } from "effect/unstable/httpapi"
import * as Sse from "effect/unstable/encoding/Sse"
import { EventApi } from "../groups/event"
const log = Log.create({ service: "server" })
function eventData(data: unknown): Sse.Event {
return {
_tag: "Event",
@@ -68,14 +65,14 @@ function eventResponse(events: EventV2.Interface) {
Stream.map(() => ({ id: eventID(), type: "server.heartbeat", properties: {} })),
)
log.info("event connected")
yield* Effect.logInfo("event connected")
return HttpServerResponse.stream(
Stream.make({ id: eventID(), type: "server.connected", properties: {} }).pipe(
Stream.concat(output.pipe(Stream.merge(heartbeat, { haltStrategy: "left" }))),
Stream.map(eventData),
Stream.pipeThroughChannel(Sse.encode()),
Stream.encodeText,
Stream.ensuring(Effect.sync(() => log.info("event disconnected"))),
Stream.ensuring(Effect.logInfo("event disconnected")),
),
{
contentType: "text/event-stream",
@@ -4,15 +4,12 @@ import { LocationServiceMap } from "@opencode-ai/core/location-layer"
import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
import { Search } from "@opencode-ai/core/filesystem/search"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Log } from "@opencode-ai/core/util/log"
import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema"
import { Effect, Layer } from "effect"
import path from "path"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { InstanceHttpApi } from "../api"
const log = Log.create({ service: "server.file" })
export const fileHandlers = HttpApiBuilder.group(InstanceHttpApi, "file", (handlers) =>
Effect.gen(function* () {
const ripgrep = yield* Ripgrep.Service
@@ -42,7 +39,7 @@ export const fileHandlers = HttpApiBuilder.group(InstanceHttpApi, "file", (handl
// to the ripgrep-backed FileSystem.find when fff is unavailable.
const fff = yield* search.file({ cwd: directory, query: ctx.query.query, limit, kind }).pipe(Effect.orDie)
if (fff !== undefined) {
log.info("find file", {
yield* Effect.logInfo("find file", {
engine: "fff",
query: ctx.query.query,
kind,
@@ -62,7 +59,7 @@ export const fileHandlers = HttpApiBuilder.group(InstanceHttpApi, "file", (handl
}),
),
)).map((item) => item.path)
log.info("find file", {
yield* Effect.logInfo("find file", {
engine: "ripgrep",
query: ctx.query.query,
kind,
@@ -5,7 +5,6 @@ import { EventV2 } from "@opencode-ai/core/event"
import { Installation } from "@/installation"
import { disposeAllInstancesAndEmitGlobalDisposed } from "@/server/global-lifecycle"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import * as Log from "@opencode-ai/core/util/log"
import { Effect, Queue, Schema } from "effect"
import * as Stream from "effect/Stream"
import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
@@ -14,8 +13,6 @@ import * as Sse from "effect/unstable/encoding/Sse"
import { RootHttpApi } from "../api"
import { GlobalUpgradeInput } from "../groups/global"
const log = Log.create({ service: "server" })
function eventData(data: unknown): Sse.Event {
return {
_tag: "Event",
@@ -34,36 +31,38 @@ function parseBody(body: string) {
}
function eventResponse() {
log.info("global event connected")
const events = Stream.callback<GlobalBusEvent>((queue) => {
const handler = (event: GlobalBusEvent) => Queue.offerUnsafe(queue, event)
return Effect.acquireRelease(
Effect.sync(() => GlobalBus.on("event", handler)),
() => Effect.sync(() => GlobalBus.off("event", handler)),
return Effect.gen(function* () {
yield* Effect.logInfo("global event connected")
const events = Stream.callback<GlobalBusEvent>((queue) => {
const handler = (event: GlobalBusEvent) => Queue.offerUnsafe(queue, event)
return Effect.acquireRelease(
Effect.sync(() => GlobalBus.on("event", handler)),
() => Effect.sync(() => GlobalBus.off("event", handler)),
)
})
const heartbeat = Stream.tick("10 seconds").pipe(
Stream.drop(1),
Stream.map(() => ({ payload: { id: EventV2.ID.create(), type: "server.heartbeat", properties: {} } })),
)
return HttpServerResponse.stream(
Stream.make({ payload: { id: EventV2.ID.create(), type: "server.connected", properties: {} } }).pipe(
Stream.concat(events.pipe(Stream.merge(heartbeat, { haltStrategy: "left" }))),
Stream.map(eventData),
Stream.pipeThroughChannel(Sse.encode()),
Stream.encodeText,
Stream.ensuring(Effect.logInfo("global event disconnected")),
),
{
contentType: "text/event-stream",
headers: {
"Cache-Control": "no-cache, no-transform",
"X-Accel-Buffering": "no",
"X-Content-Type-Options": "nosniff",
},
},
)
})
const heartbeat = Stream.tick("10 seconds").pipe(
Stream.drop(1),
Stream.map(() => ({ payload: { id: EventV2.ID.create(), type: "server.heartbeat", properties: {} } })),
)
return HttpServerResponse.stream(
Stream.make({ payload: { id: EventV2.ID.create(), type: "server.connected", properties: {} } }).pipe(
Stream.concat(events.pipe(Stream.merge(heartbeat, { haltStrategy: "left" }))),
Stream.map(eventData),
Stream.pipeThroughChannel(Sse.encode()),
Stream.encodeText,
Stream.ensuring(Effect.sync(() => log.info("global event disconnected"))),
),
{
contentType: "text/event-stream",
headers: {
"Cache-Control": "no-cache, no-transform",
"X-Accel-Buffering": "no",
"X-Content-Type-Options": "nosniff",
},
},
)
}
export const globalHandlers = HttpApiBuilder.group(RootHttpApi, "global", (handlers) =>
@@ -77,7 +76,7 @@ export const globalHandlers = HttpApiBuilder.group(RootHttpApi, "global", (handl
})
const event = Effect.fn("GlobalHttpApi.event")(function* () {
return eventResponse()
return yield* eventResponse()
})
const configGet = Effect.fn("GlobalHttpApi.configGet")(function* () {
@@ -314,9 +314,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
yield* promptSvc.prompt({ ...ctx.payload, sessionID: ctx.params.sessionID }).pipe(
Effect.catchCause((cause) =>
Effect.gen(function* () {
yield* Effect.logError("prompt_async failed").pipe(
Effect.annotateLogs({ sessionID: ctx.params.sessionID, cause }),
)
yield* Effect.logError("prompt_async failed", { sessionID: ctx.params.sessionID, cause })
yield* events.publish(Session.Event.Error, {
sessionID: ctx.params.sessionID,
error: new NamedError.Unknown({ message: Cause.pretty(cause) }).toObject(),
@@ -15,9 +15,6 @@ import { Effect, Scope } from "effect"
import { HttpApiBuilder, HttpApiError } from "effect/unstable/httpapi"
import { InstanceHttpApi } from "../api"
import { HistoryPayload, ReplayPayload, SessionPayload } from "../groups/sync"
import * as Log from "@opencode-ai/core/util/log"
const log = Log.create({ service: "server.sync" })
export const syncHandlers = HttpApiBuilder.group(InstanceHttpApi, "sync", (handlers) =>
Effect.gen(function* () {
@@ -43,7 +40,7 @@ export const syncHandlers = HttpApiBuilder.group(InstanceHttpApi, "sync", (handl
data: { ...event.data },
}))
const source = payload[0].aggregateID
log.info("sync replay requested", {
yield* Effect.logInfo("sync replay requested", {
sessionID: source,
events: payload.length,
first: payload[0]?.seq,
@@ -52,7 +49,7 @@ export const syncHandlers = HttpApiBuilder.group(InstanceHttpApi, "sync", (handl
})
const ownerID = yield* InstanceState.workspaceID
yield* events.replayAll(payload, { ownerID, strictOwner: true })
log.info("sync replay complete", {
yield* Effect.logInfo("sync replay complete", {
sessionID: source,
events: payload.length,
first: payload[0]?.seq,
@@ -67,10 +64,7 @@ export const syncHandlers = HttpApiBuilder.group(InstanceHttpApi, "sync", (handl
yield* session.setWorkspace({ sessionID: ctx.payload.sessionID, workspaceID })
log.info("sync session stolen", {
sessionID: ctx.payload.sessionID,
workspaceID,
})
yield* Effect.logInfo("sync session stolen", { sessionID: ctx.payload.sessionID, workspaceID })
return { sessionID: ctx.payload.sessionID }
})
@@ -1,12 +1,9 @@
import { EffectBridge } from "@/effect/bridge"
import type { InstanceContext } from "@/project/instance-context"
import { InstanceStore } from "@/project/instance-store"
import * as Log from "@opencode-ai/core/util/log"
import { Effect } from "effect"
import { HttpEffect, HttpMiddleware, HttpServerRequest } from "effect/unstable/http"
const log = Log.create({ service: "server" })
type MarkedInstance = {
ctx: InstanceContext
store: InstanceStore.Interface
@@ -51,7 +48,7 @@ export const disposeMiddleware: HttpMiddleware.HttpMiddleware = (effect) =>
if (!marked) return response
disposeAfterResponse.delete(request.source)
yield* Effect.uninterruptible(marked.bridge.run(marked.store.dispose(marked.ctx))).pipe(
Effect.catchCause((cause) => Effect.sync(() => log.warn("instance disposal failed", { cause }))),
Effect.catchCause((cause) => Effect.logWarning("instance disposal failed", { cause })),
)
return response
})
@@ -1,10 +1,7 @@
import { NamedError } from "@opencode-ai/core/util/error"
import * as Log from "@opencode-ai/core/util/log"
import { Cause, Effect } from "effect"
import { HttpRouter, HttpServerError, HttpServerRespondable, HttpServerResponse } from "effect/unstable/http"
const log = Log.create({ service: "server" })
// Keep typed HttpApi failures on their declared error path; this boundary only replaces defect-only empty 500s.
export const errorLayer = HttpRouter.middleware<{ handles: unknown }>()((effect) =>
effect.pipe(
@@ -20,15 +17,15 @@ export const errorLayer = HttpRouter.middleware<{ handles: unknown }>()((effect)
const error = defect.defect
const ref = `err_${crypto.randomUUID().slice(0, 8)}`
log.error("failed", { ref, error, cause: Cause.pretty(cause) })
return Effect.succeed(
HttpServerResponse.jsonUnsafe(
new NamedError.Unknown({
message: "Unexpected server error. Check server logs for details.",
ref,
}).toObject(),
{ status: 500 },
return Effect.logError("failed", { ref, error, cause: Cause.pretty(cause) }).pipe(
Effect.as(
HttpServerResponse.jsonUnsafe(
new NamedError.Unknown({
message: "Unexpected server error. Check server logs for details.",
ref,
}).toObject(),
{ status: 500 },
),
),
)
}),
@@ -1,11 +1,8 @@
import { Effect } from "effect"
import { HttpServerResponse } from "effect/unstable/http"
import { HttpApiMiddleware } from "effect/unstable/httpapi"
import * as Log from "@opencode-ai/core/util/log"
import { InvalidRequestError } from "../errors"
const log = Log.create({ service: "server" })
// Effect's Issue formatter recursively dumps the rejected `actual` value with
// no truncation, so a 5KB invalid array produces a ~360KB string. Cap to keep
// 4xx responses small and avoid mirroring entire request payloads (which may
@@ -27,16 +24,18 @@ export class SchemaErrorMiddleware extends HttpApiMiddleware.Service<SchemaError
export const schemaErrorLayer = HttpApiMiddleware.layerSchemaErrorTransform(SchemaErrorMiddleware, (error, context) => {
const reason = truncateReason(error.cause.message)
log.warn("schema rejection", { kind: error.kind, reason })
if (context.endpoint.path.startsWith("/api/")) {
return Effect.fail(
new InvalidRequestError({
message: reason,
kind: error.kind,
}),
)
}
return Effect.succeed(
HttpServerResponse.jsonUnsafe({ name: "BadRequest", data: { message: reason, kind: error.kind } }, { status: 400 }),
)
const response = context.endpoint.path.startsWith("/api/")
? Effect.fail(
new InvalidRequestError({
message: reason,
kind: error.kind,
}),
)
: Effect.succeed(
HttpServerResponse.jsonUnsafe(
{ name: "BadRequest", data: { message: reason, kind: error.kind } },
{ status: 400 },
),
)
return Effect.logWarning("schema rejection", { kind: error.kind, reason }).pipe(Effect.andThen(response))
})
@@ -16,7 +16,7 @@ import { Auth } from "@/auth"
import { BackgroundJob } from "@/background/job"
import { Config } from "@/config/config"
import { Command } from "@/command"
import * as Observability from "@opencode-ai/core/effect/observability"
import * as Observability from "@opencode-ai/core/observability"
import { Ripgrep } from "@opencode-ai/core/filesystem/ripgrep"
import { Format } from "@/format"
import { RuntimeFlags } from "@/effect/runtime-flags"
+3 -4
View File
@@ -1,7 +1,6 @@
import "./init-projectors"
import { NodeHttpServer } from "@effect/platform-node"
import * as Log from "@opencode-ai/core/util/log"
import { ConfigProvider, Context, Effect, Exit, Layer, Scope } from "effect"
import { HttpRouter, HttpServer } from "effect/unstable/http"
import { OpenApi } from "effect/unstable/httpapi"
@@ -17,8 +16,6 @@ import { lazy } from "@/util/lazy"
// @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85
globalThis.AI_SDK_LOG_WARNINGS = false
const log = Log.create({ service: "server" })
export type Listener = {
hostname: string
port: number
@@ -165,7 +162,9 @@ function setupMdns(opts: ListenOptions, port: number, scope: Scope.Scope) {
yield* Scope.addFinalizer(scope, unpublish)
return unpublish
}
if (opts.mdns) log.warn("mDNS enabled but hostname is loopback; skipping mDNS publish")
if (opts.mdns) {
yield* Effect.logWarning("mDNS enabled but hostname is loopback; skipping mDNS publish")
}
return Effect.void
})
}
+2 -10
View File
@@ -3,12 +3,10 @@ import { inArray } from "drizzle-orm"
import { EventSequenceTable } from "@opencode-ai/core/event/sql"
import { Workspace } from "@/control-plane/workspace"
import type { WorkspaceV2 } from "@opencode-ai/core/workspace"
import * as Log from "@opencode-ai/core/util/log"
import { Effect } from "effect"
export const HEADER = "x-opencode-sync"
export type State = Record<string, number>
const log = Log.create({ service: "fence" })
export function load(db: Database.Interface["db"], ids?: string[]) {
return Effect.gen(function* () {
@@ -55,14 +53,8 @@ export function parse(headers: Headers): State | undefined {
export function wait(workspaceID: WorkspaceV2.ID, state: State, signal?: AbortSignal) {
return Effect.gen(function* () {
log.info("waiting for state", {
workspaceID,
state,
})
yield* Effect.logInfo("waiting for state", { workspaceID, state })
yield* Workspace.Service.use((workspace) => workspace.waitForSync(workspaceID, state, signal))
log.info("state fully synced", {
workspaceID,
state,
})
yield* Effect.logInfo("state fully synced", { workspaceID, state })
})
}