diff --git a/.gitignore b/.gitignore index e067b72..0cc0212 100644 --- a/.gitignore +++ b/.gitignore @@ -192,4 +192,5 @@ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # #----------------------------------------------------------------------------- examples/ -context/ \ No newline at end of file +context/ +manual-test.cjs diff --git a/package.json b/package.json index 50008c5..afe6ba7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@wgtechlabs/log-engine", - "version": "2.3.0", + "version": "2.3.1", "description": "A lightweight, security-first logging utility with automatic data redaction for Node.js applications - the first logging library with built-in PII protection.", "type": "module", "keywords": [ diff --git a/src/__tests__/emoji-override.test.ts b/src/__tests__/emoji-override.test.ts new file mode 100644 index 0000000..5b5f6b9 --- /dev/null +++ b/src/__tests__/emoji-override.test.ts @@ -0,0 +1,458 @@ +/** + * Tests for per-call emoji override functionality + * Verifies that emoji can be overridden on individual log calls + */ + +import { LogFormatter } from '../formatter'; +import { LogLevel, LogMode } from '../types'; +import { EmojiSelector } from '../formatter/emoji-selector'; +import { Logger } from '../logger/core'; + +describe('Per-Call Emoji Override', () => { + beforeEach(() => { + // Reset emoji selector before each test + EmojiSelector.reset(); + }); + + describe('LogFormatter emoji override', () => { + it('should use override emoji when provided via options', () => { + const formatted = LogFormatter.format( + LogLevel.INFO, + 'Database initialized', + undefined, + undefined, + { emoji: '✅' } + ); + const cleanFormatted = formatted.replace(/\x1b\[[0-9;]*m/g, ''); + + // Should contain override emoji, not auto-detected emoji + expect(cleanFormatted).toContain('[✅]'); + expect(cleanFormatted).toMatch(/\[INFO\]\[✅\]: Database initialized$/); + }); + + it('should use override emoji even when message has keyword match', () => { + const formatted = LogFormatter.format( + LogLevel.ERROR, + 'Database connection failed', + undefined, + undefined, + { emoji: '🔴' } + ); + const cleanFormatted = formatted.replace(/\x1b\[[0-9;]*m/g, ''); + + // Should contain override emoji, not database emoji (đŸ—ƒī¸) + expect(cleanFormatted).toContain('[🔴]'); + expect(cleanFormatted).not.toContain('[đŸ—ƒī¸]'); + expect(cleanFormatted).toMatch(/\[ERROR\]\[🔴\]: Database connection failed$/); + }); + + it('should use auto-detected emoji when no override provided', () => { + const formatted = LogFormatter.format( + LogLevel.ERROR, + 'Database connection failed', + undefined, + undefined + ); + const cleanFormatted = formatted.replace(/\x1b\[[0-9;]*m/g, ''); + + // Should contain auto-detected database emoji + expect(cleanFormatted).toContain('[đŸ—ƒī¸]'); + expect(cleanFormatted).toMatch(/\[ERROR\]\[đŸ—ƒī¸\]: Database connection failed$/); + }); + + it('should suppress emoji when override is empty string', () => { + const formatted = LogFormatter.format( + LogLevel.INFO, + 'Database initialized', + undefined, + undefined, + { emoji: '' } + ); + const cleanFormatted = formatted.replace(/\x1b\[[0-9;]*m/g, ''); + + // Should not contain any emoji brackets + expect(cleanFormatted).not.toMatch(/\[đŸ—ƒī¸\]/); + expect(cleanFormatted).not.toMatch(/\[✅\]/); + expect(cleanFormatted).not.toMatch(/\[â„šī¸\]/); + expect(cleanFormatted).toMatch(/\[INFO\]: Database initialized$/); + }); + + it('should respect includeEmoji: false even with override emoji', () => { + const formatted = LogFormatter.format( + LogLevel.INFO, + 'Test message', + undefined, + { includeEmoji: false }, + { emoji: '✅' } + ); + const cleanFormatted = formatted.replace(/\x1b\[[0-9;]*m/g, ''); + + // Should not include emoji when globally disabled + expect(cleanFormatted).not.toContain('[✅]'); + expect(cleanFormatted).toMatch(/\[INFO\]: Test message$/); + }); + }); + + describe('Logger method emoji override', () => { + let logger: Logger; + let capturedOutput: { level: string; message: string }[]; + + beforeEach(() => { + logger = new Logger(); + capturedOutput = []; + + // Configure logger to capture output with INFO mode + logger.configure({ + mode: LogMode.INFO, + outputs: [(level: string, message: string) => { + capturedOutput.push({ level, message }); + }] + }); + }); + + it('should override emoji in logger.info() with options', () => { + logger.info('Config engine initialized', undefined, { emoji: '✅' }); + + expect(capturedOutput).toHaveLength(1); + const cleanMessage = capturedOutput[0].message.replace(/\x1b\[[0-9;]*m/g, ''); + expect(cleanMessage).toContain('[✅]'); + expect(cleanMessage).toMatch(/\[INFO\]\[✅\]: Config engine initialized$/); + }); + + it('should override emoji in logger.error() with options', () => { + logger.error('Critical failure', undefined, { emoji: 'đŸ’Ĩ' }); + + expect(capturedOutput).toHaveLength(1); + const cleanMessage = capturedOutput[0].message.replace(/\x1b\[[0-9;]*m/g, ''); + expect(cleanMessage).toContain('[đŸ’Ĩ]'); + expect(cleanMessage).toMatch(/\[ERROR\]\[đŸ’Ĩ\]: Critical failure$/); + }); + + it('should override emoji in logger.debug() with options', () => { + logger.configure({ mode: LogMode.DEBUG }); // Enable DEBUG mode + logger.debug('Debugging info', undefined, { emoji: '🔍' }); + + expect(capturedOutput).toHaveLength(1); + const cleanMessage = capturedOutput[0].message.replace(/\x1b\[[0-9;]*m/g, ''); + expect(cleanMessage).toContain('[🔍]'); + expect(cleanMessage).toMatch(/\[DEBUG\]\[🔍\]: Debugging info$/); + }); + + it('should override emoji in logger.warn() with options', () => { + logger.warn('Low disk space', undefined, { emoji: '💾' }); + + expect(capturedOutput).toHaveLength(1); + const cleanMessage = capturedOutput[0].message.replace(/\x1b\[[0-9;]*m/g, ''); + expect(cleanMessage).toContain('[💾]'); + expect(cleanMessage).toMatch(/\[WARN\]\[💾\]: Low disk space$/); + }); + + it('should override emoji in logger.log() with options', () => { + logger.log('System ready', undefined, { emoji: '🚀' }); + + expect(capturedOutput).toHaveLength(1); + const cleanMessage = capturedOutput[0].message.replace(/\x1b\[[0-9;]*m/g, ''); + expect(cleanMessage).toContain('[🚀]'); + expect(cleanMessage).toMatch(/\[LOG\]\[🚀\]: System ready$/); + }); + + it('should work with data parameter and emoji override', () => { + logger.info('User logged in', { userId: 123 }, { emoji: '👤' }); + + expect(capturedOutput).toHaveLength(1); + const cleanMessage = capturedOutput[0].message.replace(/\x1b\[[0-9;]*m/g, ''); + expect(cleanMessage).toContain('[👤]'); + expect(cleanMessage).toMatch(/\[INFO\]\[👤\]: User logged in/); + expect(cleanMessage).toContain('userId'); + }); + + it('should maintain backward compatibility when options not provided', () => { + logger.info('Database initialized'); + + expect(capturedOutput).toHaveLength(1); + const cleanMessage = capturedOutput[0].message.replace(/\x1b\[[0-9;]*m/g, ''); + // Should use auto-detected or fallback emoji + expect(cleanMessage).toMatch(/\[INFO\]\[.+\]: Database initialized$/); + }); + }); + + describe('Raw logging methods with emoji override', () => { + let logger: Logger; + let capturedOutput: { level: string; message: string }[]; + + beforeEach(() => { + logger = new Logger(); + capturedOutput = []; + + logger.configure({ + mode: LogMode.INFO, + outputs: [(level: string, message: string) => { + capturedOutput.push({ level, message }); + }] + }); + }); + + it('should override emoji in debugRaw() with options', () => { + logger.configure({ mode: LogMode.DEBUG }); // Enable DEBUG mode + logger.debugRaw('Debug output', { secret: 'password123' }, { emoji: '🔐' }); + + expect(capturedOutput).toHaveLength(1); + const cleanMessage = capturedOutput[0].message.replace(/\x1b\[[0-9;]*m/g, ''); + expect(cleanMessage).toContain('[🔐]'); + expect(cleanMessage).toMatch(/\[DEBUG\]\[🔐\]: Debug output/); + // Raw methods should not redact + expect(cleanMessage).toContain('password123'); + }); + + it('should override emoji in infoRaw() with options', () => { + logger.infoRaw('Info output', undefined, { emoji: '📝' }); + + expect(capturedOutput).toHaveLength(1); + const cleanMessage = capturedOutput[0].message.replace(/\x1b\[[0-9;]*m/g, ''); + expect(cleanMessage).toContain('[📝]'); + expect(cleanMessage).toMatch(/\[INFO\]\[📝\]: Info output$/); + }); + + it('should override emoji in warnRaw() with options', () => { + logger.warnRaw('Warning output', undefined, { emoji: '🚨' }); + + expect(capturedOutput).toHaveLength(1); + const cleanMessage = capturedOutput[0].message.replace(/\x1b\[[0-9;]*m/g, ''); + expect(cleanMessage).toContain('[🚨]'); + expect(cleanMessage).toMatch(/\[WARN\]\[🚨\]: Warning output$/); + }); + + it('should override emoji in errorRaw() with options', () => { + logger.errorRaw('Error output', undefined, { emoji: 'â˜ ī¸' }); + + expect(capturedOutput).toHaveLength(1); + const cleanMessage = capturedOutput[0].message.replace(/\x1b\[[0-9;]*m/g, ''); + expect(cleanMessage).toContain('[â˜ ī¸]'); + expect(cleanMessage).toMatch(/\[ERROR\]\[â˜ ī¸\]: Error output$/); + }); + + it('should override emoji in logRaw() with options', () => { + logger.logRaw('Log output', undefined, { emoji: '📋' }); + + expect(capturedOutput).toHaveLength(1); + const cleanMessage = capturedOutput[0].message.replace(/\x1b\[[0-9;]*m/g, ''); + expect(cleanMessage).toContain('[📋]'); + expect(cleanMessage).toMatch(/\[LOG\]\[📋\]: Log output$/); + }); + }); + + describe('Real-world use cases', () => { + let logger: Logger; + let capturedOutput: { level: string; message: string }[]; + + beforeEach(() => { + logger = new Logger(); + capturedOutput = []; + + logger.configure({ + mode: LogMode.INFO, + outputs: [(level: string, message: string) => { + capturedOutput.push({ level, message }); + }] + }); + }); + + it('should prevent duplicate emoji when caller wants specific emoji', () => { + // Before: logger.info('✅ Config engine initialized') would result in [â„šī¸]: ✅ Config engine... + // After: logger.info('Config engine initialized', undefined, { emoji: '✅' }) + logger.info('Config engine initialized', undefined, { emoji: '✅' }); + + expect(capturedOutput).toHaveLength(1); + const cleanMessage = capturedOutput[0].message.replace(/\x1b\[[0-9;]*m/g, ''); + + // Should have emoji only in the element, not in message + expect(cleanMessage).toContain('[✅]'); + expect(cleanMessage).toMatch(/\[INFO\]\[✅\]: Config engine initialized$/); + // Should not have duplicate emoji in message + expect(cleanMessage).not.toMatch(/\[INFO\]\[.+\]: ✅/); + }); + + it('should allow custom robot emoji for AI/bot messages', () => { + logger.info('Heartware initialized', undefined, { emoji: '🤖' }); + + expect(capturedOutput).toHaveLength(1); + const cleanMessage = capturedOutput[0].message.replace(/\x1b\[[0-9;]*m/g, ''); + expect(cleanMessage).toContain('[🤖]'); + expect(cleanMessage).toMatch(/\[INFO\]\[🤖\]: Heartware initialized$/); + }); + + it('should allow mix of override and auto-detected emoji in same session', () => { + logger.info('Database initialized', undefined, { emoji: '✅' }); + logger.info('Starting API server'); // Auto-detected + logger.error('Connection failed', undefined, { emoji: '🔴' }); + + expect(capturedOutput).toHaveLength(3); + + const clean1 = capturedOutput[0].message.replace(/\x1b\[[0-9;]*m/g, ''); + expect(clean1).toContain('[✅]'); + + const clean2 = capturedOutput[1].message.replace(/\x1b\[[0-9;]*m/g, ''); + expect(clean2).toMatch(/\[INFO\]\[.+\]: Starting API server$/); + + const clean3 = capturedOutput[2].message.replace(/\x1b\[[0-9;]*m/g, ''); + expect(clean3).toContain('[🔴]'); + }); + }); + + describe('LogEngine wrapper emoji override', () => { + let capturedOutput: { level: string; message: string }[]; + + beforeEach(() => { + capturedOutput = []; + }); + + it('should pass emoji override through LogEngine.info()', async () => { + const { LogEngine, LogMode } = await import('../index'); + + LogEngine.configure({ + mode: LogMode.INFO, + outputs: [(level: string, message: string) => { + capturedOutput.push({ level, message }); + }] + }); + + LogEngine.info('Database initialized', undefined, { emoji: '✅' }); + + expect(capturedOutput).toHaveLength(1); + const cleanMessage = capturedOutput[0].message.replace(/\x1b\[[0-9;]*m/g, ''); + expect(cleanMessage).toContain('[✅]'); + expect(cleanMessage).toMatch(/\[INFO\]\[✅\]: Database initialized$/); + }); + + it('should pass emoji override through LogEngine.error()', async () => { + const { LogEngine, LogMode } = await import('../index'); + + LogEngine.configure({ + mode: LogMode.INFO, + outputs: [(level: string, message: string) => { + capturedOutput.push({ level, message }); + }] + }); + + LogEngine.error('Critical failure', undefined, { emoji: 'đŸ’Ĩ' }); + + expect(capturedOutput).toHaveLength(1); + const cleanMessage = capturedOutput[0].message.replace(/\x1b\[[0-9;]*m/g, ''); + expect(cleanMessage).toContain('[đŸ’Ĩ]'); + expect(cleanMessage).toMatch(/\[ERROR\]\[đŸ’Ĩ\]: Critical failure$/); + }); + + it('should pass emoji override through LogEngine.debug()', async () => { + const { LogEngine, LogMode } = await import('../index'); + + LogEngine.configure({ + mode: LogMode.DEBUG, + outputs: [(level: string, message: string) => { + capturedOutput.push({ level, message }); + }] + }); + + LogEngine.debug('Debug info', undefined, { emoji: '🔍' }); + + expect(capturedOutput).toHaveLength(1); + const cleanMessage = capturedOutput[0].message.replace(/\x1b\[[0-9;]*m/g, ''); + expect(cleanMessage).toContain('[🔍]'); + expect(cleanMessage).toMatch(/\[DEBUG\]\[🔍\]: Debug info$/); + }); + + it('should pass emoji override through LogEngine.warn()', async () => { + const { LogEngine, LogMode } = await import('../index'); + + LogEngine.configure({ + mode: LogMode.INFO, + outputs: [(level: string, message: string) => { + capturedOutput.push({ level, message }); + }] + }); + + LogEngine.warn('Low memory', undefined, { emoji: '💾' }); + + expect(capturedOutput).toHaveLength(1); + const cleanMessage = capturedOutput[0].message.replace(/\x1b\[[0-9;]*m/g, ''); + expect(cleanMessage).toContain('[💾]'); + expect(cleanMessage).toMatch(/\[WARN\]\[💾\]: Low memory$/); + }); + + it('should pass emoji override through LogEngine.log()', async () => { + const { LogEngine, LogMode } = await import('../index'); + + LogEngine.configure({ + mode: LogMode.INFO, + outputs: [(level: string, message: string) => { + capturedOutput.push({ level, message }); + }] + }); + + LogEngine.log('System started', undefined, { emoji: '🚀' }); + + expect(capturedOutput).toHaveLength(1); + const cleanMessage = capturedOutput[0].message.replace(/\x1b\[[0-9;]*m/g, ''); + expect(cleanMessage).toContain('[🚀]'); + expect(cleanMessage).toMatch(/\[LOG\]\[🚀\]: System started$/); + }); + + it('should pass emoji override through LogEngine.infoRaw()', async () => { + const { LogEngine, LogMode } = await import('../index'); + + LogEngine.configure({ + mode: LogMode.INFO, + outputs: [(level: string, message: string) => { + capturedOutput.push({ level, message }); + }] + }); + + LogEngine.infoRaw('Raw info', { secret: 'data' }, { emoji: '📝' }); + + expect(capturedOutput).toHaveLength(1); + const cleanMessage = capturedOutput[0].message.replace(/\x1b\[[0-9;]*m/g, ''); + expect(cleanMessage).toContain('[📝]'); + expect(cleanMessage).toMatch(/\[INFO\]\[📝\]: Raw info/); + // Raw methods should not redact + expect(cleanMessage).toContain('secret'); + }); + + it('should pass emoji override through LogEngine.withoutRedaction()', async () => { + const { LogEngine, LogMode } = await import('../index'); + + LogEngine.configure({ + mode: LogMode.INFO, + outputs: [(level: string, message: string) => { + capturedOutput.push({ level, message }); + }] + }); + + LogEngine.withoutRedaction().info('Unredacted', { password: 'secret' }, { emoji: '🔓' }); + + expect(capturedOutput).toHaveLength(1); + const cleanMessage = capturedOutput[0].message.replace(/\x1b\[[0-9;]*m/g, ''); + expect(cleanMessage).toContain('[🔓]'); + expect(cleanMessage).toMatch(/\[INFO\]\[🔓\]: Unredacted/); + // withoutRedaction should not redact + expect(cleanMessage).toContain('password'); + }); + + it('should allow suppressing emoji via empty string through LogEngine', async () => { + const { LogEngine, LogMode } = await import('../index'); + + LogEngine.configure({ + mode: LogMode.INFO, + outputs: [(level: string, message: string) => { + capturedOutput.push({ level, message }); + }] + }); + + LogEngine.info('Plain message', undefined, { emoji: '' }); + + expect(capturedOutput).toHaveLength(1); + const cleanMessage = capturedOutput[0].message.replace(/\x1b\[[0-9;]*m/g, ''); + // Should not have emoji brackets (like [✅]) between [INFO] and the colon + expect(cleanMessage).not.toMatch(/\[INFO\]\[.+\]:/); + expect(cleanMessage).toMatch(/\[INFO\]: Plain message$/); + }); + }); +}); diff --git a/src/formatter/message-formatter.ts b/src/formatter/message-formatter.ts index 4689a76..a69633b 100644 --- a/src/formatter/message-formatter.ts +++ b/src/formatter/message-formatter.ts @@ -3,7 +3,7 @@ * Handles the main log message formatting with colors, timestamps, and levels */ -import { LogLevel, LogData, LogFormatConfig } from '../types'; +import { LogLevel, LogData, LogFormatConfig, LogCallOptions } from '../types'; import { colors, colorScheme } from './colors'; import { getTimestampComponents, formatTimestamp } from './timestamp'; import { formatData, styleData } from './data-formatter'; @@ -30,9 +30,10 @@ export class MessageFormatter { * @param message - The message content to format * @param data - Optional data object to include in the log output * @param formatConfig - Optional format configuration to control element inclusion + * @param options - Optional per-call options (e.g., emoji override) * @returns Formatted string with ANSI colors and timestamps */ - static format(level: LogLevel, message: string, data?: LogData, formatConfig?: LogFormatConfig): string { + static format(level: LogLevel, message: string, data?: LogData, formatConfig?: LogFormatConfig, options?: LogCallOptions): string { // Merge provided format configuration with the default configuration const config: LogFormatConfig = { ...MessageFormatter.DEFAULT_FORMAT_CONFIG, @@ -65,7 +66,17 @@ export class MessageFormatter { const coloredLevel = `${levelColor}[${levelName}]${colors.reset}`; // Select emoji based on context if includeEmoji is true (default) - const emoji = config.includeEmoji !== false ? EmojiSelector.selectEmoji(level, message, data) : ''; + // Use override emoji from options if provided (including empty string to suppress), otherwise use EmojiSelector + let emoji = ''; + if (config.includeEmoji !== false) { + if (options?.emoji !== undefined) { + // Use override emoji (even if empty string) + emoji = options.emoji; + } else { + // Auto-select emoji + emoji = EmojiSelector.selectEmoji(level, message, data); + } + } const emojiPart = emoji ? `[${emoji}]` : ''; // Format the base message (level is always included as per requirements) diff --git a/src/index.ts b/src/index.ts index 3dc90b3..0fe6f08 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,7 +21,7 @@ */ import { Logger } from './logger'; -import type { LoggerConfig, RedactionConfig, ILogEngineWithoutRedaction, LogData } from './types'; +import type { LoggerConfig, RedactionConfig, ILogEngineWithoutRedaction, LogData, LogCallOptions } from './types'; import { DataRedactor, defaultRedactionConfig } from './redaction'; // Create a singleton logger instance @@ -48,60 +48,70 @@ export const LogEngine = { * Only shown in DEVELOPMENT mode * @param message - The debug message to log * @param data - Optional data object to log (sensitive data will be redacted) + * @param options - Optional per-call options (e.g., emoji override) * @example * ```typescript * LogEngine.debug('Processing user data', { userId: 123, email: 'user@example.com' }); + * LogEngine.debug('Starting process', undefined, { emoji: '🔍' }); * ``` */ - debug: (message: string, data?: LogData): void => logger.debug(message, data), + debug: (message: string, data?: LogData, options?: LogCallOptions): void => logger.debug(message, data, options), /** * Log an info message with automatic data redaction * Shown in DEVELOPMENT and PRODUCTION modes * @param message - The info message to log * @param data - Optional data object to log (sensitive data will be redacted) + * @param options - Optional per-call options (e.g., emoji override) * @example * ```typescript * LogEngine.info('User login successful', { username: 'john' }); + * LogEngine.info('Database initialized', undefined, { emoji: '✅' }); * ``` */ - info: (message: string, data?: LogData): void => logger.info(message, data), + info: (message: string, data?: LogData, options?: LogCallOptions): void => logger.info(message, data, options), /** * Log a warning message with automatic data redaction * Shown in DEVELOPMENT and PRODUCTION modes * @param message - The warning message to log * @param data - Optional data object to log (sensitive data will be redacted) + * @param options - Optional per-call options (e.g., emoji override) * @example * ```typescript * LogEngine.warn('API rate limit approaching', { requestsRemaining: 10 }); + * LogEngine.warn('Low disk space', undefined, { emoji: '💾' }); * ``` */ - warn: (message: string, data?: LogData): void => logger.warn(message, data), + warn: (message: string, data?: LogData, options?: LogCallOptions): void => logger.warn(message, data, options), /** * Log an error message with automatic data redaction * Shown in DEVELOPMENT and PRODUCTION modes * @param message - The error message to log * @param data - Optional data object to log (sensitive data will be redacted) + * @param options - Optional per-call options (e.g., emoji override) * @example * ```typescript * LogEngine.error('Database connection failed', { host: 'localhost', port: 5432 }); + * LogEngine.error('Critical failure', undefined, { emoji: 'đŸ’Ĩ' }); * ``` */ - error: (message: string, data?: LogData): void => logger.error(message, data), + error: (message: string, data?: LogData, options?: LogCallOptions): void => logger.error(message, data, options), /** * Log a critical message with automatic data redaction * Always shown regardless of mode (except OFF) * @param message - The critical log message to log * @param data - Optional data object to log (sensitive data will be redacted) + * @param options - Optional per-call options (e.g., emoji override) * @example * ```typescript * LogEngine.log('Application starting', { version: '1.0.0' }); + * LogEngine.log('System ready', undefined, { emoji: '🚀' }); * ``` */ - log: (message: string, data?: LogData): void => logger.log(message, data), + log: (message: string, data?: LogData, options?: LogCallOptions): void => logger.log(message, data, options), // Raw methods that bypass redaction (use with caution) /** @@ -109,40 +119,45 @@ export const LogEngine = { * Bypasses automatic data redaction for debugging purposes * @param message - The debug message to log * @param data - Optional data object to log (no redaction applied) + * @param options - Optional per-call options (e.g., emoji override) */ - debugRaw: (message: string, data?: LogData): void => logger.debugRaw(message, data), + debugRaw: (message: string, data?: LogData, options?: LogCallOptions): void => logger.debugRaw(message, data, options), /** * Log an info message without redaction (use with caution) * Bypasses automatic data redaction for debugging purposes * @param message - The info message to log * @param data - Optional data object to log (no redaction applied) + * @param options - Optional per-call options (e.g., emoji override) */ - infoRaw: (message: string, data?: LogData): void => logger.infoRaw(message, data), + infoRaw: (message: string, data?: LogData, options?: LogCallOptions): void => logger.infoRaw(message, data, options), /** * Log a warning message without redaction (use with caution) * Bypasses automatic data redaction for debugging purposes * @param message - The warning message to log * @param data - Optional data object to log (no redaction applied) + * @param options - Optional per-call options (e.g., emoji override) */ - warnRaw: (message: string, data?: LogData): void => logger.warnRaw(message, data), + warnRaw: (message: string, data?: LogData, options?: LogCallOptions): void => logger.warnRaw(message, data, options), /** * Log an error message without redaction (use with caution) * Bypasses automatic data redaction for debugging purposes * @param message - The error message to log * @param data - Optional data object to log (no redaction applied) + * @param options - Optional per-call options (e.g., emoji override) */ - errorRaw: (message: string, data?: LogData): void => logger.errorRaw(message, data), + errorRaw: (message: string, data?: LogData, options?: LogCallOptions): void => logger.errorRaw(message, data, options), /** * Log a critical message without redaction (use with caution) * Bypasses automatic data redaction for debugging purposes * @param message - The critical log message to log * @param data - Optional data object to log (no redaction applied) + * @param options - Optional per-call options (e.g., emoji override) */ - logRaw: (message: string, data?: LogData): void => logger.logRaw(message, data), + logRaw: (message: string, data?: LogData, options?: LogCallOptions): void => logger.logRaw(message, data, options), // Redaction configuration methods /** @@ -199,14 +214,15 @@ export const LogEngine = { * @example * ```typescript * LogEngine.withoutRedaction().info('Debug data', sensitiveObject); + * LogEngine.withoutRedaction().info('Custom emoji', undefined, { emoji: '🔍' }); * ``` */ withoutRedaction: (): ILogEngineWithoutRedaction => ({ - debug: (message: string, data?: LogData): void => logger.debugRaw(message, data), - info: (message: string, data?: LogData): void => logger.infoRaw(message, data), - warn: (message: string, data?: LogData): void => logger.warnRaw(message, data), - error: (message: string, data?: LogData): void => logger.errorRaw(message, data), - log: (message: string, data?: LogData): void => logger.logRaw(message, data) + debug: (message: string, data?: LogData, options?: LogCallOptions): void => logger.debugRaw(message, data, options), + info: (message: string, data?: LogData, options?: LogCallOptions): void => logger.infoRaw(message, data, options), + warn: (message: string, data?: LogData, options?: LogCallOptions): void => logger.warnRaw(message, data, options), + error: (message: string, data?: LogData, options?: LogCallOptions): void => logger.errorRaw(message, data, options), + log: (message: string, data?: LogData, options?: LogCallOptions): void => logger.logRaw(message, data, options) }) }; @@ -219,6 +235,7 @@ export type { LogOutputHandler, BuiltInOutputHandler, OutputTarget, + LogCallOptions, // Advanced types FileOutputConfig, HttpOutputConfig, diff --git a/src/logger/core.ts b/src/logger/core.ts index d57300e..c170b1e 100644 --- a/src/logger/core.ts +++ b/src/logger/core.ts @@ -6,7 +6,7 @@ * Includes automatic data redaction for sensitive information */ -import { LogLevel, LogMode, LoggerConfig, LogOutputHandler, OutputTarget, EnhancedOutputTarget, LogData } from '../types'; +import { LogLevel, LogMode, LoggerConfig, LogOutputHandler, OutputTarget, EnhancedOutputTarget, LogData, LogCallOptions } from '../types'; import { LogFormatter, EmojiSelector } from '../formatter'; import { DataRedactor, RedactionController, defaultRedactionConfig } from '../redaction'; import { LoggerConfigManager } from './config'; @@ -52,11 +52,12 @@ export class Logger { * @param level - The log level to format for * @param message - The message content to format * @param data - Optional data object to include in the log output + * @param options - Optional per-call options (e.g., emoji override) * @returns Formatted string with appropriate configuration applied */ - private formatMessage(level: LogLevel, message: string, data?: LogData): string { + private formatMessage(level: LogLevel, message: string, data?: LogData, options?: LogCallOptions): string { const cachedConfig = this.getCachedConfig(); - return LogFormatter.format(level, message, data, cachedConfig.format); + return LogFormatter.format(level, message, data, cachedConfig.format, options); } /** @@ -297,11 +298,12 @@ export class Logger { * Automatically redacts sensitive data when provided * @param message - The debug message to log * @param data - Optional data object to log (will be redacted) + * @param options - Optional per-call options (e.g., emoji override) */ - debug(message: string, data?: LogData): void { + debug(message: string, data?: LogData, options?: LogCallOptions): void { if (this.shouldLog(LogLevel.DEBUG)) { const processedData = DataRedactor.redactData(data); - const formatted = this.formatMessage(LogLevel.DEBUG, message, processedData); + const formatted = this.formatMessage(LogLevel.DEBUG, message, processedData, options); this.writeToOutput('debug', message, formatted, processedData); } } @@ -312,11 +314,12 @@ export class Logger { * Automatically redacts sensitive data when provided * @param message - The info message to log * @param data - Optional data object to log (will be redacted) + * @param options - Optional per-call options (e.g., emoji override) */ - info(message: string, data?: LogData): void { + info(message: string, data?: LogData, options?: LogCallOptions): void { if (this.shouldLog(LogLevel.INFO)) { const processedData = DataRedactor.redactData(data); - const formatted = this.formatMessage(LogLevel.INFO, message, processedData); + const formatted = this.formatMessage(LogLevel.INFO, message, processedData, options); this.writeToOutput('info', message, formatted, processedData); } } @@ -327,11 +330,12 @@ export class Logger { * Automatically redacts sensitive data when provided * @param message - The warning message to log * @param data - Optional data object to log (will be redacted) + * @param options - Optional per-call options (e.g., emoji override) */ - warn(message: string, data?: LogData): void { + warn(message: string, data?: LogData, options?: LogCallOptions): void { if (this.shouldLog(LogLevel.WARN)) { const processedData = DataRedactor.redactData(data); - const formatted = this.formatMessage(LogLevel.WARN, message, processedData); + const formatted = this.formatMessage(LogLevel.WARN, message, processedData, options); this.writeToOutput('warn', message, formatted, processedData, false, true); } } @@ -342,11 +346,12 @@ export class Logger { * Automatically redacts sensitive data when provided * @param message - The error message to log * @param data - Optional data object to log (will be redacted) + * @param options - Optional per-call options (e.g., emoji override) */ - error(message: string, data?: LogData): void { + error(message: string, data?: LogData, options?: LogCallOptions): void { if (this.shouldLog(LogLevel.ERROR)) { const processedData = DataRedactor.redactData(data); - const formatted = this.formatMessage(LogLevel.ERROR, message, processedData); + const formatted = this.formatMessage(LogLevel.ERROR, message, processedData, options); this.writeToOutput('error', message, formatted, processedData, true, false); } } @@ -358,11 +363,12 @@ export class Logger { * Automatically redacts sensitive data when provided * @param message - The log message to output * @param data - Optional data object to log (will be redacted) + * @param options - Optional per-call options (e.g., emoji override) */ - log(message: string, data?: LogData): void { + log(message: string, data?: LogData, options?: LogCallOptions): void { if (this.shouldLog(LogLevel.LOG)) { const processedData = DataRedactor.redactData(data); - const formatted = this.formatMessage(LogLevel.LOG, message, processedData); + const formatted = this.formatMessage(LogLevel.LOG, message, processedData, options); this.writeToOutput('log', message, formatted, processedData); } } @@ -372,10 +378,11 @@ export class Logger { * Log a debug message without data redaction * @param message - The debug message to log * @param data - Optional data object to log (no redaction applied) + * @param options - Optional per-call options (e.g., emoji override) */ - debugRaw(message: string, data?: LogData): void { + debugRaw(message: string, data?: LogData, options?: LogCallOptions): void { if (this.shouldLog(LogLevel.DEBUG)) { - const formatted = this.formatMessage(LogLevel.DEBUG, message, data); + const formatted = this.formatMessage(LogLevel.DEBUG, message, data, options); this.writeToOutput('debug', message, formatted, data); } } @@ -384,10 +391,11 @@ export class Logger { * Log an info message without data redaction * @param message - The info message to log * @param data - Optional data object to log (no redaction applied) + * @param options - Optional per-call options (e.g., emoji override) */ - infoRaw(message: string, data?: LogData): void { + infoRaw(message: string, data?: LogData, options?: LogCallOptions): void { if (this.shouldLog(LogLevel.INFO)) { - const formatted = this.formatMessage(LogLevel.INFO, message, data); + const formatted = this.formatMessage(LogLevel.INFO, message, data, options); this.writeToOutput('info', message, formatted, data); } } @@ -396,10 +404,11 @@ export class Logger { * Log a warning message without data redaction * @param message - The warning message to log * @param data - Optional data object to log (no redaction applied) + * @param options - Optional per-call options (e.g., emoji override) */ - warnRaw(message: string, data?: LogData): void { + warnRaw(message: string, data?: LogData, options?: LogCallOptions): void { if (this.shouldLog(LogLevel.WARN)) { - const formatted = this.formatMessage(LogLevel.WARN, message, data); + const formatted = this.formatMessage(LogLevel.WARN, message, data, options); this.writeToOutput('warn', message, formatted, data, false, true); } } @@ -408,10 +417,11 @@ export class Logger { * Log an error message without data redaction * @param message - The error message to log * @param data - Optional data object to log (no redaction applied) + * @param options - Optional per-call options (e.g., emoji override) */ - errorRaw(message: string, data?: LogData): void { + errorRaw(message: string, data?: LogData, options?: LogCallOptions): void { if (this.shouldLog(LogLevel.ERROR)) { - const formatted = this.formatMessage(LogLevel.ERROR, message, data); + const formatted = this.formatMessage(LogLevel.ERROR, message, data, options); this.writeToOutput('error', message, formatted, data, true, false); } } @@ -420,10 +430,11 @@ export class Logger { * Log a message without data redaction (always outputs unless mode is OFF) * @param message - The log message to output * @param data - Optional data object to log (no redaction applied) + * @param options - Optional per-call options (e.g., emoji override) */ - logRaw(message: string, data?: LogData): void { + logRaw(message: string, data?: LogData, options?: LogCallOptions): void { if (this.shouldLog(LogLevel.LOG)) { - const formatted = this.formatMessage(LogLevel.LOG, message, data); + const formatted = this.formatMessage(LogLevel.LOG, message, data, options); this.writeToOutput('log', message, formatted, data); } } diff --git a/src/types/index.ts b/src/types/index.ts index a0571b8..e5c97dc 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -10,6 +10,22 @@ // eslint-disable-next-line @typescript-eslint/no-explicit-any export type LogData = any; +/** + * Options for individual log method calls + * Allows per-call customization of log behavior + */ +export interface LogCallOptions { + /** + * Override the auto-detected emoji for this specific log call + * - Set to a specific emoji string to use that emoji + * - Set to empty string ('') to suppress emoji for this call + * - Leave undefined (or omit) to use auto-detection + * @example { emoji: '✅' } // Use check mark emoji for this call + * @example { emoji: '' } // Suppress emoji for this call + */ + emoji?: string; +} + /** * Log levels representing message severity (lowest to highest) * Used for filtering messages based on importance @@ -230,27 +246,27 @@ export interface ILogEngine { // Standard logging methods with automatic redaction /** Log a debug message with automatic data redaction */ - debug(message: string, data?: LogData): void; + debug(message: string, data?: LogData, options?: LogCallOptions): void; /** Log an info message with automatic data redaction */ - info(message: string, data?: LogData): void; + info(message: string, data?: LogData, options?: LogCallOptions): void; /** Log a warn message with automatic data redaction */ - warn(message: string, data?: LogData): void; + warn(message: string, data?: LogData, options?: LogCallOptions): void; /** Log an error message with automatic data redaction */ - error(message: string, data?: LogData): void; + error(message: string, data?: LogData, options?: LogCallOptions): void; /** Log a message with automatic data redaction */ - log(message: string, data?: LogData): void; + log(message: string, data?: LogData, options?: LogCallOptions): void; // Raw logging methods (bypass redaction) /** Log a debug message without redaction */ - debugRaw(message: string, data?: LogData): void; + debugRaw(message: string, data?: LogData, options?: LogCallOptions): void; /** Log an info message without redaction */ - infoRaw(message: string, data?: LogData): void; + infoRaw(message: string, data?: LogData, options?: LogCallOptions): void; /** Log a warn message without redaction */ - warnRaw(message: string, data?: LogData): void; + warnRaw(message: string, data?: LogData, options?: LogCallOptions): void; /** Log an error message without redaction */ - errorRaw(message: string, data?: LogData): void; + errorRaw(message: string, data?: LogData, options?: LogCallOptions): void; /** Log a message without redaction */ - logRaw(message: string, data?: LogData): void; + logRaw(message: string, data?: LogData, options?: LogCallOptions): void; // Redaction configuration methods /** Configure redaction settings */ @@ -283,15 +299,15 @@ export interface ILogEngine { */ export interface ILogEngineWithoutRedaction { /** Log a debug message without redaction */ - debug(message: string, data?: LogData): void; + debug(message: string, data?: LogData, options?: LogCallOptions): void; /** Log an info message without redaction */ - info(message: string, data?: LogData): void; + info(message: string, data?: LogData, options?: LogCallOptions): void; /** Log a warn message without redaction */ - warn(message: string, data?: LogData): void; + warn(message: string, data?: LogData, options?: LogCallOptions): void; /** Log an error message without redaction */ - error(message: string, data?: LogData): void; + error(message: string, data?: LogData, options?: LogCallOptions): void; /** Log a message without redaction */ - log(message: string, data?: LogData): void; + log(message: string, data?: LogData, options?: LogCallOptions): void; } /**