Spaces:
Sleeping
Sleeping
File size: 5,026 Bytes
f6266b9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | /**
* 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; }
|