LibreChat/api/config/winston.js
Danny Avila 7f58e4c2ed
🧾 feat: Add Structured Logging Context (#13110)
* feat: add structured logging context

* fix: reduce cloudfront disabled logging

* fix: preserve strict reject logging context

* chore: format auth middleware test

* fix: omit system tenant from log context

* fix: type parser spec formatter info

* fix: normalize tenant guard before reject checks
2026-05-13 19:17:39 -04:00

210 lines
5.3 KiB
JavaScript

const path = require('path');
const fs = require('fs');
const winston = require('winston');
require('winston-daily-rotate-file');
const {
getTenantId,
getUserId,
getRequestId,
SYSTEM_TENANT_ID,
} = require('@librechat/data-schemas');
const {
redactFormat,
redactMessage,
debugTraverse,
jsonTruncateFormat,
formatConsoleMeta,
} = require('./parsers');
/**
* Determine the log directory.
* Priority:
* 1. LIBRECHAT_LOG_DIR environment variable (allows user override)
* 2. /app/logs if running in Docker (bind-mounted with correct permissions)
* 3. api/logs relative to this file (local development)
*/
const getLogDir = () => {
if (process.env.LIBRECHAT_LOG_DIR) {
return process.env.LIBRECHAT_LOG_DIR;
}
// Check if running in Docker container (cwd is /app)
if (process.cwd() === '/app') {
const dockerLogDir = '/app/logs';
// Ensure the directory exists
if (!fs.existsSync(dockerLogDir)) {
fs.mkdirSync(dockerLogDir, { recursive: true });
}
return dockerLogDir;
}
// Local development: use api/logs relative to this file
return path.join(__dirname, '..', 'logs');
};
const logDir = getLogDir();
const { NODE_ENV, DEBUG_LOGGING = true, CONSOLE_JSON = false, DEBUG_CONSOLE = false } = process.env;
const useConsoleJson =
(typeof CONSOLE_JSON === 'string' && CONSOLE_JSON?.toLowerCase() === 'true') ||
CONSOLE_JSON === true;
const useDebugConsole =
(typeof DEBUG_CONSOLE === 'string' && DEBUG_CONSOLE?.toLowerCase() === 'true') ||
DEBUG_CONSOLE === true;
const useDebugLogging =
(typeof DEBUG_LOGGING === 'string' && DEBUG_LOGGING?.toLowerCase() === 'true') ||
DEBUG_LOGGING === true;
const levels = {
error: 0,
warn: 1,
info: 2,
http: 3,
verbose: 4,
debug: 5,
activity: 6,
silly: 7,
};
const LOG_CONTEXT_KEYS = ['tenantId', 'userId', 'requestId'];
const getLogTenantId = () => {
const tenantId = getTenantId();
return tenantId === SYSTEM_TENANT_ID ? undefined : tenantId;
};
const requestContextFormat = winston.format((info) => {
if (info.tenantId === SYSTEM_TENANT_ID) {
delete info.tenantId;
}
const context = {
tenantId: getLogTenantId(),
userId: getUserId(),
requestId: getRequestId(),
};
LOG_CONTEXT_KEYS.forEach((key) => {
if (context[key] && info[key] == null) {
info[key] = context[key];
}
});
return info;
});
const formatRequestContext = (info) => {
const context = {};
LOG_CONTEXT_KEYS.forEach((key) => {
const value = info[key];
if (key === 'tenantId' && value === SYSTEM_TENANT_ID) {
return;
}
if (typeof value === 'string' && value) {
context[key] = value;
}
});
return Object.keys(context).length > 0 ? JSON.stringify(context) : '';
};
winston.addColors({
info: 'green', // fontStyle color
warn: 'italic yellow',
error: 'red',
debug: 'blue',
});
const level = () => {
const env = NODE_ENV || 'development';
const isDevelopment = env === 'development';
return isDevelopment ? 'debug' : 'warn';
};
const fileFormat = winston.format.combine(
redactFormat(),
winston.format.timestamp({ format: () => new Date().toISOString() }),
winston.format.errors({ stack: true }),
winston.format.splat(),
requestContextFormat(),
// redactErrors(),
);
const transports = [
new winston.transports.DailyRotateFile({
level: 'error',
filename: `${logDir}/error-%DATE%.log`,
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d',
format: fileFormat,
}),
];
if (useDebugLogging) {
transports.push(
new winston.transports.DailyRotateFile({
level: 'debug',
filename: `${logDir}/debug-%DATE%.log`,
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d',
format: winston.format.combine(fileFormat, debugTraverse),
}),
);
}
const consoleFormat = winston.format.combine(
redactFormat(),
requestContextFormat(),
winston.format.colorize({ all: true }),
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
// redactErrors(),
winston.format.printf((info) => {
const base = `${info.timestamp} ${info.level}: ${info.message}`;
const isErrorOrWarn = info.level.includes('error') || info.level.includes('warn');
const metaTrailer = isErrorOrWarn ? formatConsoleMeta(info) : formatRequestContext(info);
const line = metaTrailer ? `${base} ${metaTrailer}` : base;
return isErrorOrWarn ? redactMessage(line) : line;
}),
);
// Determine console log level
let consoleLogLevel = 'info';
if (useDebugConsole) {
consoleLogLevel = 'debug';
}
if (useDebugConsole) {
transports.push(
new winston.transports.Console({
level: consoleLogLevel,
format: useConsoleJson
? winston.format.combine(fileFormat, jsonTruncateFormat(), winston.format.json())
: winston.format.combine(fileFormat, debugTraverse),
}),
);
} else if (useConsoleJson) {
transports.push(
new winston.transports.Console({
level: consoleLogLevel,
format: winston.format.combine(fileFormat, jsonTruncateFormat(), winston.format.json()),
}),
);
} else {
transports.push(
new winston.transports.Console({
level: consoleLogLevel,
format: consoleFormat,
}),
);
}
const logger = winston.createLogger({
level: level(),
levels,
transports,
});
module.exports = logger;