Files
dotcom-arcade/backend/src/utils/logger.ts
T

193 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import winston from 'winston';
import path from 'path';
import fs from 'fs';
import { config } from '../config';
// Try to import OpenTelemetry API for trace context
let trace: any = null;
let context: any = null;
try {
const otelApi = require('@opentelemetry/api');
trace = otelApi.trace;
context = otelApi.context;
} catch {
// OpenTelemetry not available - trace context will not be included
}
function getTraceContext(): { trace_id?: string; span_id?: string } {
if (!trace || !context) {
return {};
}
try {
const activeSpan = trace.getSpan(context.active());
if (activeSpan) {
const spanContext = activeSpan.spanContext();
return {
trace_id: spanContext.traceId,
span_id: spanContext.spanId,
};
}
} catch {
// Ignore errors getting trace context
}
return {};
}
/**
* Map Winston log levels to OTEL severity numbers
* https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber
*/
function getSeverityNumber(level: string): number {
const severityMap: Record<string, number> = {
error: 17, // ERROR
warn: 13, // WARN
info: 9, // INFO
http: 9, // INFO
verbose: 5, // DEBUG
debug: 5, // DEBUG
silly: 1, // TRACE
};
return severityMap[level] || 9;
}
const otelFormat = winston.format((info) => {
const traceContext = getTraceContext();
if (traceContext.trace_id) {
info.trace_id = traceContext.trace_id;
info.span_id = traceContext.span_id;
}
info.severity = info.level.toUpperCase();
info.severityNumber = getSeverityNumber(info.level);
return info;
});
const transports: winston.transport[] = [
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.printf(({ level, message, timestamp, ...meta }) => {
let msg = `${timestamp} [${level}]: ${message}`;
const traceId = meta.trace_id as string | undefined;
if (traceId) {
msg = `${timestamp} [${level}] [trace:${traceId.substring(0, 8)}]: ${message}`;
}
const displayMeta = { ...meta };
delete displayMeta.service;
delete displayMeta.trace_id;
delete displayMeta.span_id;
delete displayMeta.severity;
delete displayMeta.severityNumber;
if (Object.keys(displayMeta).length > 0) {
msg += ` ${JSON.stringify(displayMeta)}`;
}
return msg;
})
),
}),
];
let fileTransport: winston.transports.FileTransportInstance | null = null;
let fileLoggingError: Error | null = null;
if (config.logFile) {
try {
const logDir = path.dirname(config.logFile);
console.log(`[Logger] Attempting to setup file logging...`);
console.log(`[Logger] LOG_FILE: ${config.logFile}`);
console.log(`[Logger] Log directory: ${logDir}`);
if (!fs.existsSync(logDir)) {
console.log(`[Logger] Creating log directory...`);
fs.mkdirSync(logDir, { recursive: true });
}
try {
const testFile = path.join(logDir, '.write-test');
fs.writeFileSync(testFile, 'test');
fs.unlinkSync(testFile);
console.log(`[Logger] Directory is writable`);
} catch (writeError) {
throw new Error(
`Log directory is not writable: ${writeError instanceof Error ? writeError.message : writeError}`
);
}
fileTransport = new winston.transports.File({
filename: config.logFile,
format: winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DDTHH:mm:ss.SSSZ' }),
winston.format.errors({ stack: true }),
otelFormat(),
winston.format.json()
),
maxsize: 10485760, // 10MB
maxFiles: 5,
tailable: true,
});
fileTransport.on('error', (error) => {
console.error(`[Logger] File transport error: ${error.message}`);
fileLoggingError = error;
});
transports.push(fileTransport);
console.log(`[Logger] ✅ File logging enabled: ${config.logFile}`);
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
console.error(`[Logger] ❌ Failed to setup file logging: ${errorMsg}`);
fileLoggingError = error instanceof Error ? error : new Error(errorMsg);
}
} else {
console.log(`[Logger] ℹ️ File logging disabled (LOG_FILE not set)`);
}
export const logger = winston.createLogger({
level: config.logLevel,
format: winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DDTHH:mm:ss.SSSZ' }), // ISO 8601
winston.format.errors({ stack: true }),
winston.format.splat(),
otelFormat(),
winston.format.json()
),
defaultMeta: { service: 'dotcom-arcade-backend' },
transports,
});
export function getLoggingStatus(): {
consoleEnabled: boolean;
fileEnabled: boolean;
filePath: string | null;
fileError: string | null;
logLevel: string;
otelIntegration: boolean;
} {
return {
consoleEnabled: true,
fileEnabled: fileTransport !== null && fileLoggingError === null,
filePath: config.logFile || null,
fileError: fileLoggingError?.message || null,
logLevel: config.logLevel,
otelIntegration: trace !== null,
};
}
export function verifyFileLogging(): boolean {
if (!config.logFile || !fileTransport) {
return false;
}
try {
logger.info('Log file verification - logging system initialized');
// Sync check only - actual write is async, so this is best-effort
return fs.existsSync(config.logFile);
} catch (error) {
console.error(`[Logger] Verification failed: ${error}`);
return false;
}
}