Skip to content

Commit 985812e

Browse files
committed
feat: Improve logging
- Implement background log collection to prevent logs from disrupting TUI display - Add `setBackgroundMode()` and `flushBackgroundLogs()` to buffer logs during TUI operation - Introduce `opt()` method to mark logs as important (always print to stderr) - Refactor log writing to support both file and stderr output with importance levels - Mark state disposal warnings as important to ensure visibility - Mark session prompt errors as non-important to reduce noise
1 parent f971679 commit 985812e

File tree

4 files changed

+78
-23
lines changed

4 files changed

+78
-23
lines changed

packages/opencode/src/cli/cmd/tui.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,8 @@ export const TuiCommand = cmd({
133133
Log.Default.info("tui", {
134134
cmd,
135135
})
136+
// Collect logs in the background to prevent them from messing up the TUI
137+
Log.setBackgroundMode(true)
136138
const proc = Bun.spawn({
137139
cmd: [
138140
...cmd,
@@ -178,6 +180,8 @@ export const TuiCommand = cmd({
178180
})()
179181

180182
await proc.exited
183+
Log.setBackgroundMode(false)
184+
Log.flushBackgroundLogs()
181185
server.stop()
182186

183187
return "done"

packages/opencode/src/project/state.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,13 @@ export namespace State {
3838

3939
setTimeout(() => {
4040
if (!disposalFinished) {
41-
Log.Default.warn("waiting for state disposal to complete... (this is usually a saving operation or subprocess shutdown)")
41+
Log.Default.opt({ important: true }).warn("waiting for state disposal to complete... (this is usually a saving operation or subprocess shutdown)")
4242
}
4343
}, 1000).unref()
4444

4545
setTimeout(() => {
4646
if (!disposalFinished) {
47-
Log.Default.warn("state disposal is taking an unusually long time - if it does not complete in a reasonable time, please report this as a bug")
47+
Log.Default.opt({ important: true }).warn("state disposal is taking an unusually long time - if it does not complete in a reasonable time, please report this as a bug")
4848
}
4949
}, 10000).unref()
5050

packages/opencode/src/session/prompt.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1124,7 +1124,7 @@ export namespace SessionPrompt {
11241124
}
11251125
}
11261126
} catch (e) {
1127-
log.error("process", {
1127+
log.opt({ important: false }).error("process", {
11281128
error: e,
11291129
})
11301130
switch (true) {

packages/opencode/src/util/log.ts

Lines changed: 71 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export namespace Log {
2121
}
2222

2323
export type Logger = {
24+
log(level: Level, message?: any, extra?: Record<string, any>): void,
2425
debug(message?: any, extra?: Record<string, any>): void
2526
info(message?: any, extra?: Record<string, any>): void
2627
error(message?: any, extra?: Record<string, any>): void
@@ -34,6 +35,16 @@ export namespace Log {
3435
stop(): void
3536
[Symbol.dispose](): void
3637
}
38+
/** Clone the logger with the specified options. */
39+
opt(options: LoggerOptions): Logger
40+
}
41+
42+
type LoggerOptions = {
43+
/**
44+
* If true, the logger will print to stderr even if printing to stderr was not explicitly enabled.
45+
* When undefined, error messages will be printed to stderr by default.
46+
*/
47+
important?: boolean
3748
}
3849

3950
const loggers = new Map<string, Logger>()
@@ -51,22 +62,23 @@ export namespace Log {
5162
return logpath
5263
}
5364

65+
let printToStderr = false
66+
let logFileWriter: Bun.FileSink | null = null
67+
5468
export async function init(options: Options) {
5569
if (options.level) level = options.level
5670
cleanup(Global.Path.log)
57-
if (options.print) return
71+
if (options.print) {
72+
printToStderr = true
73+
return
74+
}
5875
logpath = path.join(
5976
Global.Path.log,
6077
options.dev ? "dev.log" : new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log",
6178
)
6279
const logfile = Bun.file(logpath)
80+
logFileWriter = logfile.writer()
6381
await fs.truncate(logpath).catch(() => {})
64-
const writer = logfile.writer()
65-
process.stderr.write = (msg) => {
66-
writer.write(msg)
67-
writer.flush()
68-
return true
69-
}
7082
}
7183

7284
async function cleanup(dir: string) {
@@ -120,26 +132,26 @@ export namespace Log {
120132
last = next.getTime()
121133
return [next.toISOString().split(".")[0], "+" + diff + "ms", prefix, message].filter(Boolean).join(" ") + "\n"
122134
}
123-
const result: Logger = {
124-
debug(message?: any, extra?: Record<string, any>) {
125-
if (shouldLog("DEBUG")) {
126-
process.stderr.write("DEBUG " + build(message, extra))
135+
const result: Logger & { _options: LoggerOptions } = {
136+
_options: {
137+
important: undefined,
138+
},
139+
log(level: Level, message?: any, extra?: Record<string, any>) {
140+
if (shouldLog(level)) {
141+
write(level, level + " " + build(message, extra), this._options)
127142
}
128143
},
144+
debug(message?: any, extra?: Record<string, any>) {
145+
this.log("DEBUG", message, extra)
146+
},
129147
info(message?: any, extra?: Record<string, any>) {
130-
if (shouldLog("INFO")) {
131-
process.stderr.write("INFO " + build(message, extra))
132-
}
148+
this.log("INFO", message, extra)
133149
},
134150
error(message?: any, extra?: Record<string, any>) {
135-
if (shouldLog("ERROR")) {
136-
process.stderr.write("ERROR " + build(message, extra))
137-
}
151+
this.log("ERROR", message, extra)
138152
},
139153
warn(message?: any, extra?: Record<string, any>) {
140-
if (shouldLog("WARN")) {
141-
process.stderr.write("WARN " + build(message, extra))
142-
}
154+
this.log("WARN", message, extra)
143155
},
144156
tag(key: string, value: string) {
145157
if (tags) tags[key] = value
@@ -165,6 +177,11 @@ export namespace Log {
165177
},
166178
}
167179
},
180+
opt(options: LoggerOptions) {
181+
const logger = this.clone() as Logger & { _options: LoggerOptions }
182+
logger._options = options
183+
return logger
184+
}
168185
}
169186

170187
if (service && typeof service === "string") {
@@ -173,4 +190,38 @@ export namespace Log {
173190

174191
return result
175192
}
193+
194+
let messageQueue: { level: Level, message: string }[] = []
195+
let backgroundMode = false
196+
197+
function write(level: Level, message: string, options?: { ignoreFile?: boolean, important?: boolean }) {
198+
const shouldWriteToFile = !options?.ignoreFile && !printToStderr && !!logFileWriter
199+
const isImportant = options?.important ?? (levelPriority[level] >= levelPriority["ERROR"])
200+
const shouldWriteToStderr = printToStderr || isImportant
201+
if (shouldWriteToFile) {
202+
logFileWriter!.write(message)
203+
logFileWriter!.flush()
204+
}
205+
if (shouldWriteToStderr) {
206+
if (backgroundMode) messageQueue.push({ level, message })
207+
else process.stderr.write(message)
208+
}
209+
}
210+
211+
/**
212+
* Collect log messages in the background, to be flushed to stderr on-demand later.
213+
*/
214+
export function setBackgroundMode(value: boolean) {
215+
backgroundMode = value
216+
}
217+
218+
/**
219+
* Flush collected background log messages to stderr.
220+
*/
221+
export function flushBackgroundLogs() {
222+
for (const entry of messageQueue) {
223+
write(entry.level, entry.message, { ignoreFile: true })
224+
}
225+
messageQueue = []
226+
}
176227
}

0 commit comments

Comments
 (0)