99 lines
3.2 KiB
TypeScript
99 lines
3.2 KiB
TypeScript
/**
|
||
* Structured logging for email worker with daily rotation
|
||
*
|
||
* Uses pino for high-performance JSON logging.
|
||
* Console output is human-readable via pino-pretty in dev,
|
||
* and JSON in production (for Docker json-file driver).
|
||
*
|
||
* File logging uses a simple daily rotation approach.
|
||
*/
|
||
|
||
import pino from 'pino';
|
||
import { existsSync, mkdirSync, createWriteStream, type WriteStream } from 'node:fs';
|
||
import { join } from 'node:path';
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Configuration
|
||
// ---------------------------------------------------------------------------
|
||
const LOG_DIR = '/var/log/email-worker';
|
||
const LOG_FILE_PREFIX = 'worker';
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// File stream (best-effort, never crashes the worker)
|
||
// ---------------------------------------------------------------------------
|
||
let fileStream: WriteStream | null = null;
|
||
let currentDateStr = '';
|
||
|
||
function getDateStr(): string {
|
||
return new Date().toISOString().slice(0, 10); // YYYY-MM-DD
|
||
}
|
||
|
||
function ensureFileStream(): WriteStream | null {
|
||
const today = getDateStr();
|
||
if (fileStream && currentDateStr === today) return fileStream;
|
||
|
||
try {
|
||
if (!existsSync(LOG_DIR)) mkdirSync(LOG_DIR, { recursive: true });
|
||
const filePath = join(LOG_DIR, `${LOG_FILE_PREFIX}.${today}.log`);
|
||
fileStream = createWriteStream(filePath, { flags: 'a' });
|
||
currentDateStr = today;
|
||
return fileStream;
|
||
} catch {
|
||
// Silently continue without file logging (e.g. permission issue)
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Pino logger
|
||
// ---------------------------------------------------------------------------
|
||
const logger = pino({
|
||
level: 'info',
|
||
formatters: {
|
||
level(label) {
|
||
return { level: label };
|
||
},
|
||
},
|
||
timestamp: pino.stdTimeFunctions.isoTime,
|
||
// In production Docker we write plain JSON to stdout;
|
||
// pino-pretty can be used during dev via `pino-pretty` pipe.
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Log level mapping (matches Python worker levels)
|
||
// ---------------------------------------------------------------------------
|
||
type LogLevel = 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR' | 'CRITICAL' | 'SUCCESS';
|
||
|
||
const LEVEL_MAP: Record<LogLevel, keyof pino.Logger> = {
|
||
DEBUG: 'debug',
|
||
INFO: 'info',
|
||
WARNING: 'warn',
|
||
ERROR: 'error',
|
||
CRITICAL: 'fatal',
|
||
SUCCESS: 'info',
|
||
};
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Public API – mirrors Python's log(message, level, worker_name)
|
||
// ---------------------------------------------------------------------------
|
||
export function log(
|
||
message: string,
|
||
level: LogLevel = 'INFO',
|
||
workerName = 'unified-worker',
|
||
): void {
|
||
const prefix = level === 'SUCCESS' ? '[SUCCESS] ' : '';
|
||
const formatted = `[${workerName}] ${prefix}${message}`;
|
||
|
||
// Pino
|
||
const method = LEVEL_MAP[level] ?? 'info';
|
||
(logger as any)[method](formatted);
|
||
|
||
// File (best-effort)
|
||
const stream = ensureFileStream();
|
||
if (stream) {
|
||
const ts = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
||
const line = `[${ts}] [${level}] [${workerName}] ${prefix}${message}\n`;
|
||
stream.write(line);
|
||
}
|
||
}
|