/* * app-log.js — the persistent log this app never had. * * Every child (voice process, congregation server, translation model) writes * to stdout, and main.js forwards that to ITS stdout — which, for an app * launched from the Finder, is nowhere. So the morning a service went wrong * there was a caption transcript and not one line saying what the voice * process had decided to do. This module tees everything to * /logs/app.log, rotated so it can never fill a disk. * * Deliberately boring: synchronous appends (a crash must not lose the line * that explains it), a size cap, three generations, and every failure * swallowed — logging must never be the thing that takes the service down. */ 'use strict'; const fs = require('fs'); const path = require('path'); const MAX_BYTES = 2 * 1024 * 1024; // rotate past 2 MB const KEEP = 3; // app.log, app.log.1, app.log.2 function createAppLog(dir, name) { name = name || 'app.log'; const file = path.join(dir, name); let size = 0; try { fs.mkdirSync(dir, { recursive: true }); } catch (_) {} try { size = fs.statSync(file).size; } catch (_) { size = 0; } function rotate() { try { for (let i = KEEP - 1; i >= 1; i--) { const from = file + (i === 1 ? '' : '.' + (i - 1)); const to = file + '.' + i; try { fs.renameSync(from, to); } catch (_) {} } } catch (_) {} size = 0; } function stamp() { const d = new Date(); const p = (n, w) => String(n).padStart(w || 2, '0'); return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' + p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds()) + '.' + p(d.getMilliseconds(), 3); } /* Write one or more lines under a source tag. Multi-line chunks from a child's stdout are split so every line carries its own stamp and tag. */ function write(tag, chunk) { try { const text = String(chunk == null ? '' : chunk); if (!text.trim()) return; const lines = text.replace(/\r/g, '').split('\n').filter((l) => l.length); const out = lines.map((l) => stamp() + ' [' + tag + '] ' + l).join('\n') + '\n'; /* Bytes, not characters: accented Spanish and CJK log lines are 2-3 bytes a character, and a character count let a generation reach 6 MB. */ const bytes = Buffer.byteLength(out, 'utf8'); if (size + bytes > MAX_BYTES) rotate(); fs.appendFileSync(file, out); size += bytes; } catch (_) { /* never let logging take anything down */ } } /* Attach to a child process: tee stdout/stderr to the file. Returns the child for chaining. The caller may still forward to its own stdout. */ function tee(child, tag) { if (child && child.stdout) child.stdout.on('data', (d) => write(tag, d)); if (child && child.stderr) child.stderr.on('data', (d) => write(tag + ':err', d)); return child; } return { write, tee, file, rotate }; } module.exports = { createAppLog, MAX_BYTES, KEEP };