Mega push vol 4
This commit is contained in:
207
src/lib/utils/logger.ts
Normal file
207
src/lib/utils/logger.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* Centralized Logger for Root Org
|
||||
*
|
||||
* Works on both client and server. Outputs structured logs with:
|
||||
* - Timestamp
|
||||
* - Level (debug/info/warn/error)
|
||||
* - Context (which module/function)
|
||||
* - Structured data
|
||||
*
|
||||
* On the server (dev terminal), logs are colorized and always visible.
|
||||
* On the client, logs go to console and can optionally trigger toasts.
|
||||
*/
|
||||
|
||||
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
||||
|
||||
export interface LogEntry {
|
||||
level: LogLevel;
|
||||
context: string;
|
||||
message: string;
|
||||
data?: unknown;
|
||||
error?: unknown;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
const LEVEL_PRIORITY: Record<LogLevel, number> = {
|
||||
debug: 0,
|
||||
info: 1,
|
||||
warn: 2,
|
||||
error: 3,
|
||||
};
|
||||
|
||||
const LEVEL_COLORS: Record<LogLevel, string> = {
|
||||
debug: '\x1b[36m', // cyan
|
||||
info: '\x1b[32m', // green
|
||||
warn: '\x1b[33m', // yellow
|
||||
error: '\x1b[31m', // red
|
||||
};
|
||||
|
||||
const RESET = '\x1b[0m';
|
||||
const BOLD = '\x1b[1m';
|
||||
const DIM = '\x1b[2m';
|
||||
|
||||
// Minimum level to output — can be overridden
|
||||
let minLevel: LogLevel = 'debug';
|
||||
|
||||
function shouldLog(level: LogLevel): boolean {
|
||||
return LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[minLevel];
|
||||
}
|
||||
|
||||
function isServer(): boolean {
|
||||
return typeof window === 'undefined';
|
||||
}
|
||||
|
||||
function formatError(err: unknown): string {
|
||||
if (err instanceof Error) {
|
||||
const stack = err.stack ? `\n${err.stack}` : '';
|
||||
return `${err.name}: ${err.message}${stack}`;
|
||||
}
|
||||
if (typeof err === 'object' && err !== null) {
|
||||
try {
|
||||
return JSON.stringify(err, null, 2);
|
||||
} catch {
|
||||
return String(err);
|
||||
}
|
||||
}
|
||||
return String(err);
|
||||
}
|
||||
|
||||
function formatData(data: unknown): string {
|
||||
if (data === undefined || data === null) return '';
|
||||
try {
|
||||
return JSON.stringify(data, null, 2);
|
||||
} catch {
|
||||
return String(data);
|
||||
}
|
||||
}
|
||||
|
||||
function serverLog(entry: LogEntry) {
|
||||
const color = LEVEL_COLORS[entry.level];
|
||||
const levelTag = `${color}${BOLD}[${entry.level.toUpperCase()}]${RESET}`;
|
||||
const time = `${DIM}${entry.timestamp}${RESET}`;
|
||||
const ctx = `${color}[${entry.context}]${RESET}`;
|
||||
|
||||
let line = `${levelTag} ${time} ${ctx} ${entry.message}`;
|
||||
|
||||
if (entry.data !== undefined) {
|
||||
line += `\n ${DIM}data:${RESET} ${formatData(entry.data)}`;
|
||||
}
|
||||
if (entry.error !== undefined) {
|
||||
line += `\n ${color}error:${RESET} ${formatError(entry.error)}`;
|
||||
}
|
||||
|
||||
// Use stderr for errors/warnings so they stand out in terminal
|
||||
if (entry.level === 'error') {
|
||||
console.error(line);
|
||||
} else if (entry.level === 'warn') {
|
||||
console.warn(line);
|
||||
} else {
|
||||
console.log(line);
|
||||
}
|
||||
}
|
||||
|
||||
function clientLog(entry: LogEntry) {
|
||||
const prefix = `[${entry.level.toUpperCase()}] [${entry.context}]`;
|
||||
const args: unknown[] = [prefix, entry.message];
|
||||
|
||||
if (entry.data !== undefined) args.push(entry.data);
|
||||
if (entry.error !== undefined) args.push(entry.error);
|
||||
|
||||
switch (entry.level) {
|
||||
case 'error':
|
||||
console.error(...args);
|
||||
break;
|
||||
case 'warn':
|
||||
console.warn(...args);
|
||||
break;
|
||||
case 'debug':
|
||||
console.debug(...args);
|
||||
break;
|
||||
default:
|
||||
console.log(...args);
|
||||
}
|
||||
}
|
||||
|
||||
function log(level: LogLevel, context: string, message: string, extra?: { data?: unknown; error?: unknown }) {
|
||||
if (!shouldLog(level)) return;
|
||||
|
||||
const entry: LogEntry = {
|
||||
level,
|
||||
context,
|
||||
message,
|
||||
data: extra?.data,
|
||||
error: extra?.error,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
if (isServer()) {
|
||||
serverLog(entry);
|
||||
} else {
|
||||
clientLog(entry);
|
||||
}
|
||||
|
||||
// Store in recent logs buffer for debugging
|
||||
recentLogs.push(entry);
|
||||
if (recentLogs.length > MAX_RECENT_LOGS) {
|
||||
recentLogs.shift();
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
// Ring buffer of recent logs — useful for dumping context on crash
|
||||
const MAX_RECENT_LOGS = 100;
|
||||
const recentLogs: LogEntry[] = [];
|
||||
|
||||
/**
|
||||
* Create a scoped logger for a specific module/context.
|
||||
*
|
||||
* Usage:
|
||||
* ```ts
|
||||
* const log = createLogger('kanban.api');
|
||||
* log.info('Loading board', { data: { boardId } });
|
||||
* log.error('Failed to load board', { error: err, data: { boardId } });
|
||||
* ```
|
||||
*/
|
||||
export function createLogger(context: string) {
|
||||
return {
|
||||
debug: (message: string, extra?: { data?: unknown; error?: unknown }) =>
|
||||
log('debug', context, message, extra),
|
||||
info: (message: string, extra?: { data?: unknown; error?: unknown }) =>
|
||||
log('info', context, message, extra),
|
||||
warn: (message: string, extra?: { data?: unknown; error?: unknown }) =>
|
||||
log('warn', context, message, extra),
|
||||
error: (message: string, extra?: { data?: unknown; error?: unknown }) =>
|
||||
log('error', context, message, extra),
|
||||
};
|
||||
}
|
||||
|
||||
/** Set the minimum log level */
|
||||
export function setLogLevel(level: LogLevel) {
|
||||
minLevel = level;
|
||||
}
|
||||
|
||||
/** Get recent log entries (for error reports / debugging) */
|
||||
export function getRecentLogs(): LogEntry[] {
|
||||
return [...recentLogs];
|
||||
}
|
||||
|
||||
/** Clear recent logs */
|
||||
export function clearRecentLogs() {
|
||||
recentLogs.length = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format recent logs as a copyable string for bug reports.
|
||||
* User can paste this to you for debugging.
|
||||
*/
|
||||
export function dumpLogs(): string {
|
||||
return recentLogs
|
||||
.map((e) => {
|
||||
let line = `[${e.level.toUpperCase()}] ${e.timestamp} [${e.context}] ${e.message}`;
|
||||
if (e.data !== undefined) line += ` | data: ${formatData(e.data)}`;
|
||||
if (e.error !== undefined) line += ` | error: ${formatError(e.error)}`;
|
||||
return line;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
Reference in New Issue
Block a user