Context
ScriptRuntime currently manages automation-script console.* output to ${DATA_PATH}/automation-logs/automation.log via a hand-rolled fs.WriteStream (logWriter), with explicit lifecycle logic: openLogWriter() (create stream + await open + destroy-on-error), assign on load(), close on stop(), destroy on create-failure, and a log() writer. This is ~15 lines of stream-lifecycle ceremony.
Separately, script output now also goes to a dedicated pino child logger named AutomationScript (added for cleaner console output).
Idea
Investigate replacing the manual WriteStream lifecycle with something simpler now that a dedicated automation logger exists.
Findings from initial investigation (to save the next person time)
- A pino
child() logger CANNOT target its own file — pino v10's child() uses Object.create(this) and inherits the parent's stdout destination; there's no per-child destination option. So "make automationScriptLogger also write to the file" requires a separate top-level pino() instance with its own pino.destination(), not a child.
- The
GET /automation/log endpoint (getLogController) returns the file verbatim as text/plain (raw message\n lines) via read-last-lines. Any replacement must keep emitting plain-text lines, or the endpoint/frontend regresses. Default pino output is JSON, so a naive pino file destination would break this.
- Today
logWriter opens with flag 'w' → truncates on every script run (log shows only current/last run). A pino destination appends by default — behavior change to decide on.
- pino transports run on a worker thread (
thread-stream), complicating the synchronous open/close/truncate guarantees load()/stop() rely on. Would also promote pino-pretty from dev to a runtime dependency if used for plain-message formatting.
onConsoleLog also feeds the WebSocket consoleLog event — independent of logWriter, must be preserved regardless of approach.
Candidate approaches
- Option A — dedicated top-level pino file instance. Removes the manual stream, but adds pino-instance config + plain-message formatting (likely
pino-pretty as a runtime dep), worker-thread transport, and an append-vs-truncate decision. Heavier; only justified if we want structured/rotated/multi-sink automation logs later.
- Option B (recommended starting point) — drop the persistent stream, use direct
fs writes. log() → fs.appendFileSync(logFilePath, data + '\n'); truncate-per-run → one fs.writeFileSync(path, '') in load(). Removes openLogWriter(), the WriteStream field, close-on-stop, destroy-on-failure — no new dependency, no worker thread, format & endpoint unchanged. Sync write per log line is negligible at observed volumes.
Open decisions
- Truncate-per-run (today) vs. append-across-runs?
- Option A (pino) vs. Option B (simple
fs)?
- Should script output continue to stdout via the
AutomationScript pino logger, or live only in the file (not interleaved into the app's ops log)?
Acceptance criteria
- Manual
WriteStream lifecycle simplified/removed.
GET /automation/log still returns plain-text lines unchanged.
- WebSocket
consoleLog stream preserved.
- Truncate/append behavior explicitly decided & documented.
Context
ScriptRuntimecurrently manages automation-scriptconsole.*output to${DATA_PATH}/automation-logs/automation.logvia a hand-rolledfs.WriteStream(logWriter), with explicit lifecycle logic:openLogWriter()(create stream + awaitopen+ destroy-on-error), assign onload(), close onstop(), destroy on create-failure, and alog()writer. This is ~15 lines of stream-lifecycle ceremony.Separately, script output now also goes to a dedicated pino child logger named
AutomationScript(added for cleaner console output).Idea
Investigate replacing the manual
WriteStreamlifecycle with something simpler now that a dedicated automation logger exists.Findings from initial investigation (to save the next person time)
child()logger CANNOT target its own file — pino v10'schild()usesObject.create(this)and inherits the parent's stdout destination; there's no per-child destination option. So "makeautomationScriptLoggeralso write to the file" requires a separate top-levelpino()instance with its ownpino.destination(), not a child.GET /automation/logendpoint (getLogController) returns the file verbatim astext/plain(rawmessage\nlines) viaread-last-lines. Any replacement must keep emitting plain-text lines, or the endpoint/frontend regresses. Default pino output is JSON, so a naive pino file destination would break this.logWriteropens with flag'w'→ truncates on every script run (log shows only current/last run). A pinodestinationappends by default — behavior change to decide on.thread-stream), complicating the synchronous open/close/truncate guaranteesload()/stop()rely on. Would also promotepino-prettyfrom dev to a runtime dependency if used for plain-message formatting.onConsoleLogalso feeds the WebSocketconsoleLogevent — independent oflogWriter, must be preserved regardless of approach.Candidate approaches
pino-prettyas a runtime dep), worker-thread transport, and an append-vs-truncate decision. Heavier; only justified if we want structured/rotated/multi-sink automation logs later.fswrites.log()→fs.appendFileSync(logFilePath, data + '\n'); truncate-per-run → onefs.writeFileSync(path, '')inload(). RemovesopenLogWriter(), theWriteStreamfield, close-on-stop, destroy-on-failure — no new dependency, no worker thread, format & endpoint unchanged. Sync write per log line is negligible at observed volumes.Open decisions
fs)?AutomationScriptpino logger, or live only in the file (not interleaved into the app's ops log)?Acceptance criteria
WriteStreamlifecycle simplified/removed.GET /automation/logstill returns plain-text lines unchanged.consoleLogstream preserved.