Masar / src /utils /logger.ts
Hussien Haider
H
8cc91a6
Raw
History Blame Contribute Delete
9.64 kB
/**
* Modern colored logger β€” structured output with ANSI styling.
* Never log: passwords, tokens, keys, user content, or raw error objects.
*/
type LogLevel = 'info' | 'warn' | 'error' | 'debug';
const C = {
reset: '\x1b[0m',
dim: '\x1b[2m',
bold: '\x1b[1m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m',
white: '\x1b[37m',
gray: '\x1b[90m',
bgRed: '\x1b[41m',
bgGreen: '\x1b[42m',
bgYellow: '\x1b[43m',
bgBlue: '\x1b[44m',
bgMagenta: '\x1b[45m',
bgCyan: '\x1b[46m',
};
const LEVEL_STYLES: Record<LogLevel, { icon: string; color: string; bg: string; label: string }> = {
info: { icon: '●', color: C.green, bg: C.bgGreen, label: ' INFO ' },
warn: { icon: 'β–²', color: C.yellow, bg: C.bgYellow, label: ' WARN ' },
error: { icon: 'βœ–', color: C.red, bg: C.bgRed, label: ' ERROR' },
debug: { icon: 'β—†', color: C.cyan, bg: C.bgCyan, label: ' DEBUG' },
};
function timestamp(): string {
return new Date().toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
}
function formatLevel(level: LogLevel): string {
const s = LEVEL_STYLES[level];
return `${s.bg}${C.bold}${C.white} ${s.icon} ${s.label} ${C.reset}`;
}
function formatContext(ctx?: string): string {
if (!ctx) return '';
return `${C.gray}[${C.cyan}${ctx}${C.gray}]${C.reset} `;
}
function formatMessage(msg: string): string {
return `${C.white}${msg}${C.reset}`;
}
function log(level: LogLevel, message: string, context?: string): void {
const ts = `${C.dim}${timestamp()}${C.reset}`;
const lvl = formatLevel(level);
const ctx = formatContext(context);
const msg = formatMessage(message);
const line = `${C.dim}β”Œβ”€${C.reset} ${ts} ${lvl} ${ctx}${msg}`;
if (level === 'error') {
console.error(line);
} else if (level === 'warn') {
console.warn(line);
} else {
console.log(line);
}
}
export function logInfo(message: string, context?: string) {
log('info', message, context);
}
export function logWarn(message: string, context?: string) {
log('warn', message, context);
}
export function logError(message: string, context?: string) {
log('error', message, context);
}
export function logDebug(message: string, context?: string) {
log('debug', message, context);
}
// ─── Request Logger Middleware ───────────────────────────────────────────────
export function requestLogger() {
return (req: any, _res: any, next: () => void) => {
const method = req.method;
const url = req.originalUrl || req.url;
const METHOD_COLORS: Record<string, string> = {
GET: C.green,
POST: C.blue,
PUT: C.yellow,
DELETE: C.red,
PATCH: C.magenta,
OPTIONS: C.gray,
};
const color = METHOD_COLORS[method] || C.white;
const ts = `${C.dim}${timestamp()}${C.reset}`;
const methodStr = `${color}${C.bold}${method.padEnd(7)}${C.reset}`;
const urlStr = `${C.white}${url}${C.reset}`;
const ip = req.ip || req.connection?.remoteAddress || '';
console.log(`${C.dim}β”Œβ”€${C.reset} ${ts} ${C.bgBlue}${C.bold}${C.white} REQ ${C.reset} ${methodStr} ${urlStr} ${C.dim}${C.gray}← ${ip}${C.reset}`);
next();
};
}
// ─── Server Banner ──────────────────────────────────────────────────────────
export function printBanner(port: string | number, env: string) {
const isProd = env === 'production';
const envColor = isProd ? C.red : C.green;
const envLabel = isProd ? 'PRODUCTION' : 'DEVELOPMENT';
const line = `${C.dim}────────────────────────────────────────────${C.reset}`;
console.log('');
console.log(`${C.blue}${C.bold} ╔══════════════════════════════════════════╗${C.reset}`);
console.log(`${C.blue}${C.bold} β•‘${C.reset} ${C.cyan}${C.bold}MASAR${C.reset} ${C.dim}Β·${C.reset} ${C.white}Backend API${C.reset} ${C.blue}${C.bold}β•‘${C.reset}`);
console.log(`${C.blue}${C.bold} ╠══════════════════════════════════════════╣${C.reset}`);
console.log(`${C.blue}${C.bold} β•‘${C.reset} ${C.blue}${C.bold}β•‘${C.reset}`);
console.log(`${C.blue}${C.bold} β•‘${C.reset} ${C.gray}Status${C.reset} ${C.green}${C.bold}● RUNNING${C.reset} ${C.blue}${C.bold}β•‘${C.reset}`);
console.log(`${C.blue}${C.bold} β•‘${C.reset} ${C.gray}Port${C.reset} ${C.white}${C.bold}${port}${C.reset} ${C.blue}${C.bold}β•‘${C.reset}`);
console.log(`${C.blue}${C.bold} β•‘${C.reset} ${C.gray}Environment${C.reset} ${envColor}${C.bold}${envLabel}${C.reset} ${C.blue}${C.bold}β•‘${C.reset}`);
console.log(`${C.blue}${C.bold} β•‘${C.reset} ${C.gray}Time${C.reset} ${C.white}${new Date().toLocaleString('en-US', { timeZone: 'Asia/Baghdad' })}${C.reset} ${C.blue}${C.bold}β•‘${C.reset}`);
console.log(`${C.blue}${C.bold} β•‘${C.reset} ${C.blue}${C.bold}β•‘${C.reset}`);
console.log(`${C.blue}${C.bold} ╠══════════════════════════════════════════╣${C.reset}`);
const routes: [string, string, string, string][] = [
['GET', '/api/health', C.green, 'Health check'],
['POST', '/api/auth/*', C.blue, 'Authentication'],
['GET', '/api/users/*', C.green, 'User profiles'],
['GET', '/api/courses/*', C.green, 'Course content'],
['POST', '/api/challenges/*', C.blue, 'Challenge submit'],
['GET', '/api/leaderboard', C.green, 'Leaderboard'],
['GET', '/api/admin/*', C.green, 'Admin panel'],
['GET', '/api/teams/*', C.green, 'Team management'],
];
routes.forEach(([method, path, color, label]) => {
const m = `${color}${C.bold}${method.padEnd(6)}${C.reset}`;
const p = `${C.white}${path.padEnd(22)}${C.reset}`;
const l = `${C.dim}${label}${C.reset}`;
console.log(`${C.blue}${C.bold} β•‘${C.reset} ${m} ${p} ${l} ${C.blue}${C.bold}β•‘${C.reset}`);
});
console.log(`${C.blue}${C.bold} β•‘${C.reset} ${C.blue}${C.bold}β•‘${C.reset}`);
console.log(`${C.blue}${C.bold} β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•${C.reset}`);
console.log('');
console.log(`${C.dim} ${line}${C.reset}`);
console.log(`${C.cyan} β†’ Ready to accept connections${C.reset}`);
console.log(`${C.dim} ${line}${C.reset}`);
console.log('');
}
// ─── Response Logger (call in middleware after res finishes) ─────────────────
export function responseLogger() {
return (req: any, res: any, next: () => void) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
const status = res.statusCode;
const method = req.method;
const url = req.originalUrl || req.url;
let statusBg = C.bgGreen;
if (status >= 500) { statusBg = C.bgRed; }
else if (status >= 400) { statusBg = C.bgYellow; }
else if (status >= 300) { statusBg = C.bgCyan; }
const durationColor = duration > 1000 ? C.red : duration > 500 ? C.yellow : C.green;
const METHOD_COLORS: Record<string, string> = {
GET: C.green,
POST: C.blue,
PUT: C.yellow,
DELETE: C.red,
PATCH: C.magenta,
};
const mColor = METHOD_COLORS[method] || C.white;
const ts = `${C.dim}${timestamp()}${C.reset}`;
const m = `${mColor}${C.bold}${method.padEnd(7)}${C.reset}`;
const s = `${statusBg}${C.bold}${C.white} ${status} ${C.reset}`;
const d = `${durationColor}${C.bold}${duration}ms${C.reset}`;
const u = `${C.white}${url}${C.reset}`;
console.log(`${C.dim}└─${C.reset} ${ts} ${C.magenta}${C.bold} RES ${C.reset} ${m} ${s} ${u} ${C.dim}${C.gray}β†’${C.reset} ${d}`);
});
next();
};
}
// ─── Security Alert ─────────────────────────────────────────────────────────
export function logSecurity(message: string, ip?: string) {
const ts = `${C.dim}${timestamp()}${C.reset}`;
const icon = `${C.bgRed}${C.bold}${C.white} ⚠ SECURITY ${C.reset}`;
const ipStr = ip ? `${C.gray}← ${ip}${C.reset}` : '';
console.log(`${C.red}${C.bold} β”Œβ”€${C.reset} ${ts} ${icon} ${C.red}${C.bold}${message}${C.reset} ${ipStr}`);
}
// ─── Activity Log (console only) ────────────────────────────────────────────
export function logActivityConsole(action: string, details?: string) {
const ts = `${C.dim}${timestamp()}${C.reset}`;
const icon = `${C.bgMagenta}${C.bold}${C.white} ♦ LOG ${C.reset}`;
const act = `${C.magenta}${C.bold}${action}${C.reset}`;
const det = details ? `${C.dim}β€” ${details}${C.reset}` : '';
console.log(`${C.dim}β”Œβ”€${C.reset} ${ts} ${icon} ${act} ${det}`);
}