LibreChat/api/config/winston.js
Danny Avila 799a080479
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
🗂️ feat: Allow Disabling File Log Transports (#13215)
* fix: allow disabling file log transports

* fix: defer log directory setup when file logging disabled
2026-05-20 23:16:56 -04:00

224 lines
5.5 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 {
NODE_ENV,
DEBUG_LOGGING = true,
CONSOLE_JSON = false,
DEBUG_CONSOLE = false,
LOG_TO_FILE = true,
} = 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 useFileLogging =
(typeof LOG_TO_FILE === 'string' && LOG_TO_FILE?.toLowerCase() !== 'false') ||
LOG_TO_FILE === 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 = [];
if (useFileLogging) {
const logDir = getLogDir();
transports.push(
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;