🗂️ feat: Allow Disabling File Log Transports (#13215)
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

* fix: allow disabling file log transports

* fix: defer log directory setup when file logging disabled
This commit is contained in:
Danny Avila 2026-05-20 23:16:56 -04:00 committed by GitHub
parent 8310e9a840
commit 799a080479
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 182 additions and 68 deletions

View file

@ -68,6 +68,8 @@ CONSOLE_JSON=false
DEBUG_LOGGING=true
DEBUG_CONSOLE=false
# Set to false to disable file-backed Winston transports.
LOG_TO_FILE=true
# Set to true to enable agent debug logging
AGENT_DEBUG_LOGGING=false

View file

@ -0,0 +1,72 @@
const fs = require('fs');
const ORIGINAL_ENV = process.env;
const mockDataSchemas = () => {
jest.doMock('@librechat/data-schemas', () => ({
getTenantId: jest.fn(),
getUserId: jest.fn(),
getRequestId: jest.fn(),
SYSTEM_TENANT_ID: 'system',
}));
};
const mockReadOnlyDockerLogDir = () => {
const originalExistsSync = fs.existsSync;
const originalMkdirSync = fs.mkdirSync;
jest.spyOn(process, 'cwd').mockReturnValue('/app');
jest
.spyOn(fs, 'existsSync')
.mockImplementation((target) =>
target === '/app/logs' ? false : originalExistsSync.call(fs, target),
);
return jest.spyOn(fs, 'mkdirSync').mockImplementation((target, options) => {
if (target === '/app/logs') {
throw new Error('Attempted to create Docker log directory');
}
return originalMkdirSync.call(fs, target, options);
});
};
const prepareLoggerWithoutFileLogging = () => {
jest.resetModules();
jest.clearAllMocks();
mockDataSchemas();
process.env = {
...ORIGINAL_ENV,
DEBUG_LOGGING: 'true',
LOG_TO_FILE: 'false',
};
return mockReadOnlyDockerLogDir();
};
describe('LOG_TO_FILE', () => {
afterEach(() => {
process.env = ORIGINAL_ENV;
jest.restoreAllMocks();
});
it('does not create the API log directory when winston file logging is disabled', () => {
const mkdirSyncSpy = prepareLoggerWithoutFileLogging();
expect(() => require('../winston')).not.toThrow();
const winston = require('winston');
expect(winston.transports.DailyRotateFile).not.toHaveBeenCalled();
expect(mkdirSyncSpy).not.toHaveBeenCalledWith('/app/logs', expect.anything());
});
it('does not create the API log directory when Meili file logging is disabled', () => {
const mkdirSyncSpy = prepareLoggerWithoutFileLogging();
expect(() => require('../meiliLogger')).not.toThrow();
const winston = require('winston');
expect(winston.transports.DailyRotateFile).not.toHaveBeenCalled();
expect(mkdirSyncSpy).not.toHaveBeenCalledWith('/app/logs', expect.anything());
});
});

View file

@ -29,14 +29,16 @@ const getLogDir = () => {
return path.join(__dirname, '..', 'logs');
};
const logDir = getLogDir();
const { NODE_ENV, DEBUG_LOGGING = false } = process.env;
const { NODE_ENV, DEBUG_LOGGING = false, LOG_TO_FILE = true } = process.env;
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,
@ -68,17 +70,23 @@ const fileFormat = winston.format.combine(
);
const logLevel = useDebugLogging ? 'debug' : 'error';
const transports = [
new winston.transports.DailyRotateFile({
level: logLevel,
filename: `${logDir}/meiliSync-%DATE%.log`,
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d',
format: fileFormat,
}),
];
const transports = [];
if (useFileLogging) {
const logDir = getLogDir();
transports.push(
new winston.transports.DailyRotateFile({
level: logLevel,
filename: `${logDir}/meiliSync-%DATE%.log`,
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d',
format: fileFormat,
}),
);
}
const consoleFormat = winston.format.combine(
winston.format.colorize({ all: true }),

View file

@ -42,9 +42,13 @@ const getLogDir = () => {
return path.join(__dirname, '..', 'logs');
};
const logDir = getLogDir();
const { NODE_ENV, DEBUG_LOGGING = true, CONSOLE_JSON = false, DEBUG_CONSOLE = false } = process.env;
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') ||
@ -58,6 +62,10 @@ 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,
@ -129,30 +137,36 @@ const fileFormat = winston.format.combine(
// 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,
}),
];
const transports = [];
if (useFileLogging) {
const logDir = getLogDir();
if (useDebugLogging) {
transports.push(
new winston.transports.DailyRotateFile({
level: 'debug',
filename: `${logDir}/debug-%DATE%.log`,
level: 'error',
filename: `${logDir}/error-%DATE%.log`,
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d',
format: winston.format.combine(fileFormat, debugTraverse),
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(

View file

@ -1,12 +1,18 @@
const winston = require('winston');
const useFileLogging =
typeof process.env.LOG_TO_FILE !== 'string' || process.env.LOG_TO_FILE.toLowerCase() !== 'false';
const transports = [new winston.transports.Console()];
if (useFileLogging) {
transports.push(new winston.transports.File({ filename: 'login-logs.log' }));
}
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'login-logs.log' }),
],
transports,
});
module.exports = logger;

View file

@ -2,14 +2,14 @@ import winston from 'winston';
import 'winston-daily-rotate-file';
import { getLogDirectory } from './utils';
const logDir = getLogDirectory();
const { NODE_ENV, DEBUG_LOGGING = 'false' } = process.env;
const { NODE_ENV, DEBUG_LOGGING = 'false', LOG_TO_FILE } = process.env;
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';
const levels: winston.config.AbstractConfigSetLevels = {
error: 0,
warn: 1,
@ -41,17 +41,23 @@ const fileFormat = winston.format.combine(
);
const logLevel = useDebugLogging ? 'debug' : 'error';
const transports: winston.transport[] = [
new winston.transports.DailyRotateFile({
level: logLevel,
filename: `${logDir}/meiliSync-%DATE%.log`,
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d',
format: fileFormat,
}),
];
const transports: winston.transport[] = [];
if (useFileLogging) {
const logDir = getLogDirectory();
transports.push(
new winston.transports.DailyRotateFile({
level: logLevel,
filename: `${logDir}/meiliSync-%DATE%.log`,
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d',
format: fileFormat,
}),
);
}
const consoleFormat = winston.format.combine(
winston.format.colorize({ all: true }),

View file

@ -4,9 +4,7 @@ import { redactFormat, redactMessage, debugTraverse, jsonTruncateFormat } from '
import { getTenantId, getUserId, getRequestId, SYSTEM_TENANT_ID } from './tenantContext';
import { getLogDirectory } from './utils';
const logDir = getLogDirectory();
const { NODE_ENV, DEBUG_LOGGING, CONSOLE_JSON, DEBUG_CONSOLE } = process.env;
const { NODE_ENV, DEBUG_LOGGING, CONSOLE_JSON, DEBUG_CONSOLE, LOG_TO_FILE } = process.env;
const useConsoleJson = typeof CONSOLE_JSON === 'string' && CONSOLE_JSON.toLowerCase() === 'true';
@ -14,6 +12,8 @@ const useDebugConsole = typeof DEBUG_CONSOLE === 'string' && DEBUG_CONSOLE.toLow
const useDebugLogging = typeof DEBUG_LOGGING === 'string' && DEBUG_LOGGING.toLowerCase() === 'true';
const useFileLogging = typeof LOG_TO_FILE !== 'string' || LOG_TO_FILE.toLowerCase() !== 'false';
const levels: winston.config.AbstractConfigSetLevels = {
error: 0,
warn: 1,
@ -88,30 +88,36 @@ const fileFormat = winston.format.combine(
requestContextFormat(),
);
const transports: winston.transport[] = [
new winston.transports.DailyRotateFile({
level: 'error',
filename: `${logDir}/error-%DATE%.log`,
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d',
format: winston.format.combine(fileFormat, winston.format.json()),
}),
];
const transports: winston.transport[] = [];
if (useFileLogging) {
const logDir = getLogDirectory();
if (useDebugLogging) {
transports.push(
new winston.transports.DailyRotateFile({
level: 'debug',
filename: `${logDir}/debug-%DATE%.log`,
level: 'error',
filename: `${logDir}/error-%DATE%.log`,
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d',
format: winston.format.combine(fileFormat, debugTraverse),
format: winston.format.combine(fileFormat, winston.format.json()),
}),
);
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(