WindsurfAPI / src /dashboard /logger.js
github-actions[bot]
Deploy from GitHub: 7495fde758f0be655f95e6331fec2898267f790c
f6266b9
/**
* Structured logging with ring buffer, SSE, and on-disk JSONL persistence.
*
* Patches the primitive `log` object from config.js so every log call also:
* 1. lands in an in-memory ring buffer (dashboard "recent logs")
* 2. fans out to live SSE subscribers
* 3. appends a structured JSONL line to logs/app.jsonl (daily-rotated)
* 4. errors/warns also go to logs/error.jsonl
*
* Structured context: the last argument to log.*() may be a plain object.
* It is stripped from the message and attached as `ctx`, so callers can do:
* log.info('Chat request', { requestId, model, account: acct.email });
* and the dashboard can filter/group by ctx fields.
*/
import { mkdirSync, createWriteStream, existsSync } from 'fs';
import { join } from 'path';
import { randomUUID } from 'crypto';
import { log } from '../config.js';
const MAX_BUFFER = 1000;
const _buffer = [];
const _subscribers = new Set();
const LOG_DIR = join(process.cwd(), 'logs');
try { mkdirSync(LOG_DIR, { recursive: true }); } catch {}
// Rotate by UTC date. One stream per day, lazily recreated at midnight.
let _appStream = null;
let _errStream = null;
let _streamDate = '';
function today() {
const d = new Date();
return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}-${String(d.getUTCDate()).padStart(2, '0')}`;
}
function getStreams() {
const date = today();
if (date !== _streamDate) {
try { _appStream?.end(); } catch {}
try { _errStream?.end(); } catch {}
_appStream = createWriteStream(join(LOG_DIR, `app-${date}.jsonl`), { flags: 'a' });
_errStream = createWriteStream(join(LOG_DIR, `error-${date}.jsonl`), { flags: 'a' });
_streamDate = date;
}
return { app: _appStream, err: _errStream };
}
function formatArg(a) {
if (typeof a === 'string') return a;
if (a instanceof Error) return a.stack || a.message;
try { return JSON.stringify(a); } catch { return String(a); }
}
// Detect "context object": plain object, not array, not Error, reasonable size.
function isCtx(x) {
return x && typeof x === 'object' && !Array.isArray(x) && !(x instanceof Error)
&& Object.getPrototypeOf(x) === Object.prototype;
}
// Save originals before patching
const _orig = {
debug: log.debug,
info: log.info,
warn: log.warn,
error: log.error,
};
for (const level of ['debug', 'info', 'warn', 'error']) {
log[level] = (...args) => {
// Pull trailing context object out of args.
let ctx = null;
if (args.length > 1 && isCtx(args[args.length - 1])) {
ctx = args[args.length - 1];
args = args.slice(0, -1);
}
const msg = args.map(formatArg).join(' ');
const entry = { ts: Date.now(), level, msg };
if (ctx) entry.ctx = ctx;
_buffer.push(entry);
if (_buffer.length > MAX_BUFFER) _buffer.shift();
for (const fn of _subscribers) {
try { fn(entry); } catch {}
}
// Persist to disk
try {
const { app, err } = getStreams();
const line = JSON.stringify(entry) + '\n';
app.write(line);
if (level === 'error' || level === 'warn') err.write(line);
} catch {}
// Also print to console so pm2 logs still work
if (ctx) {
const ctxStr = Object.entries(ctx)
.map(([k, v]) => `${k}=${typeof v === 'string' ? v : JSON.stringify(v)}`)
.join(' ');
_orig[level](...args, ctxStr ? `{${ctxStr}}` : '');
} else {
_orig[level](...args);
}
};
}
/**
* Return a logger bound to a fixed context (e.g. { requestId }).
* Later args to .info/.warn/.error can still add more context fields.
*/
export function withCtx(baseCtx) {
const bind = (level) => (...args) => {
let extra = null;
if (args.length > 1 && isCtx(args[args.length - 1])) {
extra = args[args.length - 1];
args = args.slice(0, -1);
}
log[level](...args, { ...baseCtx, ...(extra || {}) });
};
return {
debug: bind('debug'),
info: bind('info'),
warn: bind('warn'),
error: bind('error'),
requestId: baseCtx.requestId,
};
}
/** Generate a short request id for tracing a single chat call end-to-end. */
export function newRequestId() {
return 'r_' + randomUUID().replace(/-/g, '').slice(0, 10);
}
/** Get recent logs, optionally filtered by since/level/ctx. */
export function getLogs(since = 0, level = null, ctxFilter = null) {
let result = _buffer;
if (since > 0) result = result.filter(e => e.ts > since);
if (level) result = result.filter(e => e.level === level);
if (ctxFilter && typeof ctxFilter === 'object') {
result = result.filter(e => {
if (!e.ctx) return false;
for (const [k, v] of Object.entries(ctxFilter)) {
if (e.ctx[k] !== v) return false;
}
return true;
});
}
return result;
}
export function subscribeToLogs(callback) { _subscribers.add(callback); }
export function unsubscribeFromLogs(callback) { _subscribers.delete(callback); }
/** Get current log directory (for dashboard to display). */
export function getLogDir() { return LOG_DIR; }