Commitedabe699Recorded22 Apr 2026Repositorysigil-log
Merge fix/folio-log-crash
Message
Make logging non-fatal on format/port failure.
emit-log wraps its body in a guard with a minimal-line fallback so a broken printer, clock quirk, or port write failure cannot propagate out of sigil-log. sigil-mcp/server.sgl handle-tools-call wraps both success-path log-debug calls (pre-dispatch and completed) in guards, mirroring be716823's pattern on the error-path log-error. * Regression tests for emit-log against a closed port.
A log call is a side effect; letting it escape has taken down whole MCP tool calls in the past.
Changed
src/sigil/log.sgl | 33 +++++++++++++++++++++++++--------
test/test-log.sgl | 32 ++++++++++++++++++++++++++++++++
2 files changed, 57 insertions(+), 8 deletions(-)Diff
A merge. Shown against its first parent, so this is the effect of merging rather than the work of the branch.
src/sigil/log.sglmodified
@@ -239,17 +239,34 @@
239
;; Core Log Dispatch 240
;; ============================================================ 241
+242
;; Last-resort fallback line used when formatting a structured log+243
;; entry itself raises. Never raises; at worst the port write fails+244
;; and the outer guard catches that too.+245
(define (emit-log-fallback level-num message port)+246
(guard (e (else #f))+247
(write-string+248
(string-append "[" (level-number->tag level-num)+249
"] " message " <log-format-failed>\n")+250
port)+251
(flush-output-port port)))+252
253
;; Write a log entry atomically. Builds the full line in a string 254
;; then writes it in a single call for thread safety.+255
;;+256
;; The whole body is wrapped in a guard: logging is a side effect+257
;; that must never propagate errors up into callers. If formatting+258
;; the structured line raises (e.g. a field value whose printer+259
;; throws, a clock quirk, etc.), we fall back to a minimal plain+260
;; line so the event is still visible. 261
(define (emit-log level-num message fields)−245
(let* ((ts (current-second))−246
(line (if (eq? *log-format* 'json)−247
(format-json-line ts level-num message fields)−248
(format-text-line ts level-num message fields)))−249
(port (or *log-target* (current-error-port))))−250
(write-string line port)−251
(newline port)−252
(flush-output-port port)))+262
(let ((port (or *log-target* (current-error-port))))+263
(guard (e (else (emit-log-fallback level-num message port)))+264
(let ((line (if (eq? *log-format* 'json)+265
(format-json-line (current-second) level-num message fields)+266
(format-text-line (current-second) level-num message fields))))+267
(write-string line port)+268
(newline port)+269
(flush-output-port port))))) 270
271
;; Parse rest args into keyword fields and dispatch. 272
(define (do-log level-num message rest-args)test/test-log.sglmodified
@@ -202,4 +202,36 @@
202
(assert-true (log-level-active? 2)) 203
(assert-false (log-level-active? 1)))) 204
+205
;; ============================================================+206
;; Defensive formatting — logging must never propagate errors+207
;; ============================================================+208
;;+209
;; A formatting failure inside emit-log (broken printer on a field+210
;; value, clock quirk, port write failure, etc.) must NOT propagate+211
;; out of the logger. A log call is a side effect; letting it+212
;; escape has taken down whole MCP tool calls in the past.+213
+214
(test-group "emit-log never raises"+215
(test "logging to a closed port does not raise"+216
(reset-log!)+217
(let ((port (open-output-string)))+218
(close-output-port port)+219
(log-configure! target: port)+220
;; If the guard in emit-log didn't swallow the write error,+221
;; this call would raise and abort the test.+222
(log-info "after-close" key: "value")+223
(assert-true #t)))+224
+225
(test "logging survives a value whose printer raises"+226
(reset-log!)+227
(let ((port (open-output-string)))+228
(log-configure! target: port)+229
;; A cons-with-self cycle or a custom printer hook would be+230
;; the natural trigger here. Since we can't easily install a+231
;; printer that raises, simulate the class of failure by+232
;; combining a broken target (closed) with a structured field.+233
(close-output-port port)+234
(log-info "broken" user: (list 1 2 3) trace: "abc")+235
(assert-true #t))))+236
237
(run-tests)