diff --git a/.env.example b/.env.example index 3d4e372338..6d5195f016 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/api/config/__tests__/logToFile.spec.js b/api/config/__tests__/logToFile.spec.js new file mode 100644 index 0000000000..4b3170f95a --- /dev/null +++ b/api/config/__tests__/logToFile.spec.js @@ -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()); + }); +}); diff --git a/api/config/meiliLogger.js b/api/config/meiliLogger.js index 398672da5c..7eb6e3b986 100644 --- a/api/config/meiliLogger.js +++ b/api/config/meiliLogger.js @@ -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 }), diff --git a/api/config/winston.js b/api/config/winston.js index 163e0b4eab..983205fc70 100644 --- a/api/config/winston.js +++ b/api/config/winston.js @@ -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( diff --git a/api/utils/logger.js b/api/utils/logger.js index 542a0a5327..36f23c25cd 100644 --- a/api/utils/logger.js +++ b/api/utils/logger.js @@ -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; diff --git a/packages/data-schemas/src/config/meiliLogger.ts b/packages/data-schemas/src/config/meiliLogger.ts index 572e877071..243b3bed69 100644 --- a/packages/data-schemas/src/config/meiliLogger.ts +++ b/packages/data-schemas/src/config/meiliLogger.ts @@ -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 }), diff --git a/packages/data-schemas/src/config/winston.ts b/packages/data-schemas/src/config/winston.ts index 9bc7652415..9c4a7b7800 100644 --- a/packages/data-schemas/src/config/winston.ts +++ b/packages/data-schemas/src/config/winston.ts @@ -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(