UNPKG

@analog-tools/logger

Version:

Logging utility for AnalogJS applications

1,632 lines (1,267 loc) 48.7 kB
# @analog-tools/logger > **⚠️ IMPORTANT: Early Development Stage** ⚠️ > This project is in its early development stage. Breaking changes may happen frequently as the APIs evolve. Use with caution in production environments. A minimal, type-safe logging utility for server-side applications in AnalogJS, Nitro and H3-based environments. Works standalone or with optional @analog-tools/inject integration for dependency injection patterns. [![npm version](https://img.shields.io/npm/v/@analog-tools/logger.svg)](https://www.npmjs.com/package/@analog-tools/logger) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![npm downloads](https://img.shields.io/npm/dm/@analog-tools/logger.svg)](https://www.npmjs.com/package/@analog-tools/logger) [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/) ## Table of Contents - [Features](#features) - [Prerequisites](#prerequisites) - [Installation](#installation) - [30-Second Quickstart](#30-second-quickstart) - [Quick Start](#quick-start) - [Type Safety](#type-safety) - [Usage with @analog-tools/inject (Optional)](#usage-with-analog-toolsinject-optional) - [Usage in AnalogJS API Routes](#usage-in-analogjs-api-routes) - [Enhanced Error Handling](#enhanced-error-handling) - [Context-Based Logging](#context-based-logging) - [Metadata-Based Styling and Icons](#metadata-based-styling-and-icons) - [Smart Log Deduplication](#smart-log-deduplication) - [Log Grouping](#log-grouping) - [API Reference](#api-reference) - [Environment Variables](#environment-variables) - [Testing with MockLoggerService](#testing-with-mockloggerservice) - [Colored Console Output](#colored-console-output) - [Log Grouping](#log-grouping) - [Performance Considerations](#performance-considerations) - [Troubleshooting](#troubleshooting) - [Migration Guide](#migration-guide) - [Best Practices](#best-practices) - [Security Considerations](#security-considerations) - [Contributing](#contributing) - [Future Plans](#future-plans) - [License](#license) ## Features - 📝 **Type-safe log levels** with `LogLevel` union type: `trace`, `debug`, `info`, `warn`, `error`, `fatal`, `silent` - 🛡️ **Compile-time type checking** for log level configuration - ⚠️ **Runtime validation** with graceful fallback for invalid log levels - 🎯 **IntelliSense support** for log level auto-completion - 🎨 **Metadata-based styling** with colors, formatting, and emoji icons - 🌈 **Curated color palette** with rich ANSI color support via ColorEnum - ✨ **Semantic styling** with global and per-call configuration - 🧩 Seamless integration with @analog-tools/inject for dependency injection - 🌳 Context-based logging with child loggers - 🔧 Configurable via environment variables - 🚫 Context filtering with disabledContexts - 🌐 Nitro middleware and request handler integration - 🧪 Mock logger implementation for testing - 🚀 Lightweight implementation using standard console - 🔥 **Enhanced Error Handling** with multiple overloads and type safety - 🛡️ **Structured Error Serialization** with circular reference protection - 📊 **LogContext Support** for structured logging - 🔄 **Backwards Compatibility** with existing error logging patterns - 🔁 **Smart Log Deduplication** with aggregation to reduce noise from repeated messages ## Prerequisites - Node.js 18.13.0 or later - AnalogJS project or Nitro/H3-based application - TypeScript 4.8 or later (for full type safety) - @analog-tools/inject ^0.0.5 (peer dependency) ### Compatibility Matrix | @analog-tools/logger | AnalogJS | Node.js | TypeScript | |---------------------|----------|---------|------------| | 0.0.5 | ≥ 1.0.0 | ≥ 18.13 | ≥ 4.8 | ## Installation ```bash # Using npm npm install @analog-tools/logger # Using pnpm pnpm add @analog-tools/logger # Using yarn yarn add @analog-tools/logger ``` ## 30-Second Quickstart Get up and running with @analog-tools/logger in under a minute: ```typescript import { LoggerService } from '@analog-tools/logger'; // Create a logger instance const logger = new LoggerService({ level: 'info', name: 'my-app' }); // Start logging immediately logger.info('Hello from @analog-tools/logger!'); logger.warn('This is a warning message'); logger.error('Error occurred', new Error('Something went wrong')); // Add styling and icons with metadata logger.info('Success!', { style: 'success', icon: '✅' }); logger.warn('Be careful', { icon: '⚠️' }); logger.error('Critical error', { style: { color: ColorEnum.FireRed, bold: true }, icon: '🔥' }); // Create context-specific loggers const dbLogger = logger.forContext('database'); dbLogger.info('Database operation completed', { icon: '🗄️' }); ``` ## Quick Start Here's a basic example of using the logger: ```typescript import { LoggerService } from '@analog-tools/logger'; // Create a logger instance const logger = new LoggerService({ level: 'info', name: 'my-app', }); // Log at different levels logger.debug('Debug message'); logger.info('Info message', { userId: 123 }); logger.warn('Warning message'); logger.error('Error occurred', new Error('Something went wrong')); logger.fatal('Fatal error', new Error('Critical failure')); // Create a context-specific logger const authLogger = logger.forContext('auth'); authLogger.info('User authenticated'); // Group related log messages logger.group('API Request'); logger.info('Processing request to /users'); logger.debug('Validating request body'); logger.info('Request processed successfully'); logger.groupEnd('API Request'); // Nested groups logger.group('Database Operations'); logger.info('Starting transaction'); logger.group('Query Execution'); logger.debug('Executing SQL: SELECT * FROM users'); logger.debug('Query completed in 15ms'); logger.groupEnd('Query Execution'); logger.info('Transaction committed'); logger.groupEnd('Database Operations'); ``` ## Type Safety @analog-tools/logger provides comprehensive type safety through TypeScript, ensuring compile-time validation and excellent developer experience. ### LogLevelString Type The logger uses a string union type `LogLevel` for log levels, providing compile-time validation and IntelliSense support: ```typescript import { LoggerConfig, LogLevel, isValidLogLevel } from '@analog-tools/logger'; // ✅ Valid log levels with IntelliSense support const validLevels: LogLevel[] = [ 'trace', 'debug', 'info', 'warn', 'error', 'fatal', 'silent' ]; // ✅ Type-safe configuration const config: LoggerConfig = { level: 'debug', // ← IntelliSense shows valid options name: 'my-app' }; // ❌ TypeScript error for invalid levels const invalidConfig: LoggerConfig = { level: 'verbose', // ← TypeScript error: Type '"verbose"' is not assignable name: 'my-app' }; ``` ### Runtime Validation The logger provides graceful runtime validation for scenarios where log levels come from external sources: ```typescript import { LoggerService, isValidLogLevel } from '@analog-tools/logger'; // Runtime validation function if (isValidLogLevel(externalLevel)) { // Safe to use const logger = new LoggerService({ level: externalLevel }); } else { // Handle invalid level console.warn(`Invalid log level: ${externalLevel}`); } // Automatic fallback with warning const logger = new LoggerService({ level: 'INVALID' as LogLevel, // Runtime error name: 'my-app' }); // Console output: [LoggerService] Invalid log level "INVALID". Falling back to "info"... ``` ### LogLevel Enum For scenarios where you need numeric log level values: ```typescript import { LogLevelEnum } from '@analog-tools/logger'; console.log(LogLevelEnum.trace); // 0 console.log(LogLevelEnum.debug); // 1 console.log(LogLevelEnum.info); // 2 console.log(LogLevelEnum.warn); // 3 console.log(LogLevelEnum.error); // 4 console.log(LogLevelEnum.fatal); // 5 console.log(LogLevelEnum.silent); // 6 // Useful for custom log level comparisons if (currentLogLevel >= LogLevelEnum.warn) { // Log warning and above } ``` ### Type-Safe Nitro Integration The Nitro integration also supports type-safe log levels: ```typescript import { withLogging, LogLevel } from '@analog-tools/logger'; // ✅ Type-safe Nitro middleware export default withLogging(myHandler, { namespace: 'api', level: 'debug' as LogLevel, // ← IntelliSense support logResponse: true }); // ❌ TypeScript error for invalid levels export default withLogging(myHandler, { level: 'verbose' // ← TypeScript error }); ``` ### Migration from String Types If you're migrating from a version that used generic `string` types: ```typescript // Before (generic string) interface OldConfig { level?: string; // ❌ No type safety } // After (type-safe) interface NewConfig { level?: LogLevel; // ✅ Compile-time validation } // Migration strategy const config: LoggerConfig = { level: process.env.LOG_LEVEL as LogLevel, // Runtime validation will handle invalid values name: 'my-app' }; ``` ## Usage with @analog-tools/inject (Optional) The logger works perfectly standalone, but integrates seamlessly with @analog-tools/inject for dependency injection patterns. **Note: @analog-tools/inject is a peer dependency and completely optional.** ### Standalone Usage (No Injection Framework) ```typescript import { LoggerService } from '@analog-tools/logger'; // Direct instantiation - works without @analog-tools/inject const logger = new LoggerService({ level: 'debug', name: 'my-app', disabledContexts: ['verbose-module'] }); // Use directly in your services class UserService { private logger = new LoggerService({ name: 'user-service' }); private userLogger = this.logger.forContext('users'); getUser(id: string) { this.userLogger.info(`Getting user with id ${id}`, { userId: id }); // Implementation... } } // Or create a singleton pattern class LoggerFactory { private static instance: LoggerService; static getInstance(): LoggerService { if (!this.instance) { this.instance = new LoggerService({ level: process.env['LOG_LEVEL'] || 'info', name: 'my-app' }); } return this.instance; } } // Usage const logger = LoggerFactory.getInstance(); const apiLogger = logger.forContext('api'); ``` ### With @analog-tools/inject Integration When you want centralized dependency injection and configuration: ```typescript import { inject, registerService } from '@analog-tools/inject'; import { LoggerService } from '@analog-tools/logger'; // Register with custom configuration (optional) registerService(LoggerService, { level: 'debug', name: 'my-app', disabledContexts: ['verbose-module'] // Disable specific contexts }); // Inject in your services or API routes class UserService { private logger = inject(LoggerService); private userLogger = this.logger.forContext('users'); getUser(id: string) { this.userLogger.info(`Getting user with id ${id}`, { userId: id }); // Implementation... } } // Auto-registration: If not manually registered, LoggerService will be auto-registered with defaults class ProductService { // This works even without explicit registerService() call private logger = inject(LoggerService).forContext('products'); getProduct(id: string) { this.logger.info(`Fetching product ${id}`); // Implementation... } } ``` ### Choosing Between Approaches **Use Standalone When:** - You prefer direct control over logger instances - You're not using dependency injection patterns - You want minimal dependencies - You're integrating into existing applications without DI **Use with @analog-tools/inject When:** - You want centralized configuration management - You're building applications with dependency injection patterns - You want consistent logger configuration across services - You're using other @analog-tools packages that leverage injection ### Migration Between Approaches You can easily switch between standalone and injection patterns: - **To Standalone:** Replace `inject(LoggerService)` with `new LoggerService()` - **To Injection:** replace direct instantiation with `inject(LoggerService)` - **Mixed Approach:** Use injection for services and standalone for utilities as needed ## Usage in AnalogJS API Routes ### Basic Usage (With @analog-tools/inject) ```typescript // src/server/routes/api/users/[id].ts import { defineEventHandler, getRouterParam } from 'h3'; import { inject } from '@analog-tools/inject'; import { LoggerService } from '@analog-tools/logger'; export default defineEventHandler((event) => { const logger = inject(LoggerService).forContext('users-api'); const userId = getRouterParam(event, 'id'); logger.info(`User endpoint called for ID: ${userId}`, { userId, path: event.node.req.url, }); // Route implementation... return { id: userId, name: 'Example User' }; }); ``` ### Basic Usage (Standalone) ```typescript // src/server/routes/api/users/[id].ts import { defineEventHandler, getRouterParam } from 'h3'; import { LoggerService } from '@analog-tools/logger'; // Create a shared logger instance (you might want to create this in a separate module) const apiLogger = new LoggerService({ level: 'info', name: 'api' }); export default defineEventHandler((event) => { const logger = apiLogger.forContext('users-api'); const userId = getRouterParam(event, 'id'); logger.info(`User endpoint called for ID: ${userId}`, { userId, path: event.node.req.url, }); // Route implementation... return { id: userId, name: 'Example User' }; }); ``` ### Shared Logger Configuration for API Routes For standalone usage, create a shared logger module: ```typescript // src/server/utils/logger.ts import { LoggerService } from '@analog-tools/logger'; export const apiLogger = new LoggerService({ level: process.env['LOG_LEVEL'] || 'info', name: 'api', disabledContexts: process.env['LOG_DISABLED_CONTEXTS']?.split(',') || [] }); // Export commonly used context loggers export const usersApiLogger = apiLogger.forContext('users'); export const productsApiLogger = apiLogger.forContext('products'); export const authApiLogger = apiLogger.forContext('auth'); ``` ```typescript // src/server/routes/api/users/[id].ts import { defineEventHandler, getRouterParam } from 'h3'; import { usersApiLogger } from '../../utils/logger'; export default defineEventHandler((event) => { const userId = getRouterParam(event, 'id'); usersApiLogger.info(`User endpoint called for ID: ${userId}`, { userId, path: event.node.req.url, }); // Route implementation... return { id: userId, name: 'Example User' }; }); ``` ### With Nitro Middleware ```typescript // src/server/middleware/logging.ts import { createLoggerMiddleware } from '@analog-tools/logger'; export default createLoggerMiddleware('api-requests'); ``` ### With Request Handler Wrapper ```typescript // src/server/routes/api/products/index.ts import { defineEventHandler } from 'h3'; import { withLogging } from '@analog-tools/logger'; export default withLogging( defineEventHandler(() => { // Handler implementation return { products: [] }; }), { namespace: 'products-api', level: 'info', logResponse: true // Log response body (use with caution) } ); ``` ## Context-Based Logging Create child loggers for specific contexts: ```typescript // Main logger const logger = new LoggerService(); // Create context-specific loggers const authLogger = logger.forContext('auth'); const dbLogger = logger.forContext('database'); // Usage authLogger.info('User logged in'); // Logs with context: 'auth' dbLogger.error('Database connection failed'); // Logs with context: 'database' ``` ### Disabling Specific Contexts You can disable logging for specific contexts: ```typescript // Option 1: Via constructor configuration const logger = new LoggerService({ disabledContexts: ['verbose-module', 'debug-info'] }); // Option 2: Via setter method logger.setDisabledContexts(['verbose-module', 'debug-info']); // Option 3: Via environment variable // Set LOGGER_DISABLED_CONTEXTS=verbose-module,debug-info ``` ## API Reference ### `LoggerService` The main logger class: ```typescript class LoggerService implements ILogger { // Mark as injectable for @analog-tools/inject static INJECTABLE = true; constructor(config?: LoggerConfig); // Create a child logger with context forContext(context: string): ILogger; // Get/set configuration getLogLevel(): LogLevel; getDisabledContexts(): string[]; setDisabledContexts(contexts: string[]): void; setUseColors(enabled: boolean): void; getUseColors(): boolean; // Logging methods trace(message: string, ...data: unknown[]): void; debug(message: string, ...data: unknown[]): void; info(message: string, ...data: unknown[]): void; warn(message: string, ...data: unknown[]): void; error(message: string, error?: Error | unknown, ...data: unknown[]): void; fatal(message: string, error?: Error | unknown, ...data: unknown[]): void; } ``` ### `LoggerConfig` Configuration options: ```typescript interface LoggerConfig { // Log level (default: 'info' or from LOG_LEVEL env variable) level?: string; // Logger name prefix (default: 'analog-tools') name?: string; // Contexts to disable logging for disabledContexts?: string[]; // Whether to use colored output (default: true in non-test environments) useColors?: boolean; } ``` ### Nitro Integration ```typescript // Create middleware that adds logger to event context createLoggerMiddleware(namespace: string = 'api'): EventHandler; // Wrap an event handler with automatic request logging withLogging<T extends EventHandlerRequest>( handler: EventHandler<T>, options?: { namespace?: string; // Context for the logger (default: 'api') level?: 'debug' | 'info'; // Log level (default: 'debug') logResponse?: boolean; // Whether to log response bodies (default: false) } ): EventHandler<T>; ``` ### Mock Logger for Testing ```typescript class MockLoggerService implements ILogger { static INJECTABLE = true; // All standard logger methods that can be spied on in tests forContext(context: string): ILogger; trace(message: string, data?: Record<string, unknown>): void; debug(message: string, data?: Record<string, unknown>): void; info(message: string, data?: Record<string, unknown>): void; warn(message: string, data?: Record<string, unknown>): void; error(message: string, error?: Error | unknown, data?: Record<string, unknown>): void; fatal(message: string, error?: Error | unknown, data?: Record<string, unknown>): void; } ``` ## Environment Variables The logger supports the following environment variables: ``` LOG_LEVEL=info # Set the default log level LOG_DISABLED_CONTEXTS=verbose-module,debug-info # Comma-separated list of contexts to disable ``` ## Testing with MockLoggerService For unit tests, you can use the provided MockLoggerService: ```typescript import { MockLoggerService } from '@analog-tools/logger'; import { registerService } from '@analog-tools/inject'; import { beforeEach, describe, expect, it, vi } from 'vitest'; describe('MyService', () => { let mockLogger: MockLoggerService; beforeEach(() => { mockLogger = new MockLoggerService(); registerService(LoggerService, mockLogger); // Spy on logger methods vi.spyOn(mockLogger, 'info'); vi.spyOn(mockLogger, 'error'); }); it('should log correctly', () => { const myService = new MyService(); myService.doSomething(); expect(mockLogger.info).toHaveBeenCalledWith( 'Operation completed', expect.objectContaining({ status: 'success' }) ); }); }); ``` ## Metadata-Based Styling and Icons @analog-tools/logger now supports a powerful metadata-based styling and icon system for all log methods. This enables semantic and custom styles, emoji icons, and per-call or global configuration for beautiful, expressive logs. ### Usage Example ```typescript import { LoggerService, ColorEnum } from '@analog-tools/logger'; const logger = new LoggerService({ level: 'info', name: 'my-app', // Optional: global style/icon config styles: { highlight: { color: ColorEnum.LemonYellow, bold: true } }, icons: { success: '✅', info: 'ℹ️' } }); // Per-call metadata for style and icon logger.info('Success!', { style: 'success', icon: '✅' }); logger.warn('Be careful', { icon: '⚠️' }); logger.info('Custom dreamy color', { style: { color: ColorEnum.DeepPurple, underline: true } }); logger.info('With emoji', { icon: '🚀' }); ``` #### Supported Features - **Semantic styles**: Use names like `'success'`, `'warning'`, `'highlight'`, etc. - **Custom styles**: Use `ColorEnum` and style config for color, bold, underline, background, etc. - **Emoji icons**: Use any emoji or semantic icon name (e.g., `'success'`, `'info'`). - **Global config**: Set default styles/icons in `LoggerConfig`. - **Per-call override**: Pass metadata as the last argument to any log method. - **Fallback/warning**: Unknown styles/icons trigger a warning and fallback to defaults. #### Example: Highlighted Info (replaces `info2()`) ```typescript // Old: // logger.info2('Important!'); // New: logger.info('Important!', { style: 'highlight', icon: '⭐️' }); ``` See the [Migration Guide](./OPTIMIZATION.md) for upgrade instructions and more examples. --- Use predefined semantic styles for common scenarios: ```typescript // Configure semantic styles globally const logger = new LoggerService({ level: 'info', name: 'my-app', useColors: true, styles: { highlight: { color: ColorEnum.LemonYellow, bold: true }, success: { color: ColorEnum.ForestGreen }, error: { color: ColorEnum.FireRed }, warning: { color: ColorEnum.TangerineOrange }, info: { color: ColorEnum.OceanBlue }, debug: { color: ColorEnum.SlateGray } }, icons: { success: '✅', warning: '⚠️', error: '❌', info: 'ℹ️', debug: '🐞' } }); // Use semantic styles logger.info('Important message', { style: 'highlight' }); logger.info('Operation successful', { style: 'success', icon: 'success' }); logger.info('Debug information', { style: 'debug', icon: 'debug' }); ``` ### ColorEnum Options The logger includes a comprehensive set of dreamy colors with ANSI codes: ```typescript // Blue shades ColorEnum.SkyBlue // Bright blue ColorEnum.OceanBlue // Standard blue ColorEnum.MidnightBlue // Deep blue // Green shades ColorEnum.MintGreen // Bright green ColorEnum.ForestGreen // Standard green ColorEnum.EmeraldGreen // Deep green // Yellow shades ColorEnum.LemonYellow // Bright yellow ColorEnum.SunflowerYellow // Standard yellow ColorEnum.GoldYellow // Gold // Red shades ColorEnum.RoseRed // Bright red ColorEnum.FireRed // Standard red ColorEnum.BurgundyRed // Deep red // Purple shades ColorEnum.LavenderPurple // Bright purple ColorEnum.RoyalPurple // Medium purple ColorEnum.DeepPurple // Deep purple // Orange shades ColorEnum.PeachOrange // Light orange ColorEnum.TangerineOrange // Standard orange ColorEnum.AmberOrange // Deep orange // Gray shades ColorEnum.SilverGray // Light gray ColorEnum.SlateGray // Medium gray ColorEnum.CharcoalGray // Dark gray // Background colors (add "Bg" suffix) ColorEnum.SkyBlueBg // Blue background ColorEnum.ForestGreenBg // Green background // ... and many more ``` ### Advanced Styling Options ```typescript // Custom styling with multiple format options logger.info('Formatted message', { style: { color: ColorEnum.RoyalPurple, bold: true, underline: true }, icon: '🎨' }); // Using with regular data logger.info('User logged in', { userId: '123', timestamp: Date.now() }, { style: 'success', icon: '👤' } ); // Multiple data objects with metadata logger.info('Complex operation', { step: 1, status: 'processing' }, { duration: 150, memory: '2.1MB' }, { style: 'highlight', icon: '⚡️' } ); ``` ### Error and Fatal Logging with Metadata The enhanced error and fatal methods support metadata styling: ```typescript // Error with metadata styling logger.error('Database error', new Error('Connection timeout'), { userId: '123', query: 'SELECT * FROM users' }, { style: 'error', icon: '🔥' } ); // Fatal error with styling logger.fatal('Critical system failure', { style: { color: ColorEnum.FireRed, bold: true }, icon: '💀' }); ``` ### Icon System The logger supports a comprehensive set of emoji icons: ```typescript // Common icons logger.info('Success', { icon: '✅' }); logger.warn('Warning', { icon: '⚠️' }); logger.error('Error', { icon: '❌' }); logger.info('Information', { icon: 'ℹ️' }); logger.debug('Debug', { icon: '🐞' }); // Process icons logger.info('Loading', { icon: '⏳' }); logger.info('Rocket launch', { icon: '🚀' }); logger.info('Fire alert', { icon: '🔥' }); logger.info('Star rating', { icon: '⭐️' }); // Status icons logger.info('Locked', { icon: '🔒' }); logger.info('Unlocked', { icon: '🔓' }); logger.info('Up arrow', { icon: '⬆️' }); logger.info('Down arrow', { icon: '⬇️' }); // And many more emoji options available... ``` ### Fallback and Warning Behavior The logger gracefully handles unknown styles and icons: ```typescript // Unknown semantic style - logs warning and uses default logger.info('Message', { style: 'unknown-style' }); // Console output: [my-app] Unknown semantic style: unknown-style. Falling back to default. // Invalid icon - logs warning and uses the string as-is logger.info('Message', { icon: 'not-an-emoji' }); // Console output: [my-app] Invalid icon: not-an-emoji. Expected a valid emoji or semantic icon name. ``` ### Child Logger Inheritance Child loggers inherit global styling configuration: ```typescript const logger = new LoggerService({ styles: { highlight: { color: ColorEnum.LemonYellow, bold: true } }, icons: { success: '✅' } }); const childLogger = logger.forContext('child'); childLogger.info('Child message', { style: 'highlight', icon: 'success' }); // Uses parent's configuration ``` ### Color Control Control color output globally or per-call: ```typescript // Disable colors globally const logger = new LoggerService({ level: 'info', name: 'my-app', useColors: false // Icons still work, colors are disabled }); // Enable/disable colors at runtime logger.setUseColors(false); logger.info('No colors', { style: 'highlight', icon: '🎨' }); // Output: 🎨 [my-app] No colors (no color codes) logger.setUseColors(true); logger.info('With colors', { style: 'highlight', icon: '🎨' }); // Output: 🎨 [my-app] With colors (with color codes) ``` ### Migration from Legacy Styling If you were using custom color libraries or formatting, you can now use the built-in metadata system: ```typescript // Before (custom color library) logger.info(chalk.yellow.bold('Important message')); // After (metadata-based) logger.info('Important message', { style: { color: ColorEnum.LemonYellow, bold: true } }); // Before (manual emoji concatenation) logger.info('✅ Task completed'); // After (metadata-based) logger.info('Task completed', { icon: '' }); ``` ## Smart Log Deduplication The logger includes smart deduplication to reduce noise from repeated log messages. When enabled, identical messages within a time window are batched together and displayed with repeat counts. ### Basic Configuration ```typescript const logger = new LoggerService({ level: 'info', deduplication: { enabled: true, windowMs: 5000, // 5-second batching window (default: 1000ms) flushOnCritical: true // Flush batched messages when error/fatal occurs (default: true) } }); // These repeated messages will be batched logger.info('Processing file...'); logger.info('Processing file...'); logger.info('Processing file...'); // After 5 seconds, you'll see: // [my-app] Processing file... (×3) ``` ### How It Works 1. **Message Batching**: Identical messages at the same log level are grouped together within a time window 2. **Automatic Flushing**: Batched messages are automatically flushed when the time window expires 3. **Critical Message Priority**: Error and fatal messages always log immediately and optionally flush any pending batches 4. **Context Awareness**: Messages from different contexts are tracked separately 5. **Metadata Bypass**: Messages with styling metadata or additional data are never batched (logged immediately) ### Key Features #### Simple Messages Only Only simple messages without metadata or additional data are deduplicated: ```typescript // These will be batched logger.info('Connection retry'); logger.info('Connection retry'); logger.info('Connection retry'); // These will NOT be batched (logged immediately) logger.info('Connection retry', { style: 'warning' }); logger.info('Connection retry', { userId: 123 }); logger.info('Connection retry', additionalData); ``` #### Critical Message Handling Error and fatal messages bypass deduplication entirely: ```typescript // Setup with batching logger.info('Processing...'); logger.info('Processing...'); // This error logs immediately and flushes batched messages logger.error('Processing failed!'); // Output: // [my-app] Processing... (×2) // [my-app] Processing failed! ``` #### Level-Specific Batching Different log levels are batched separately: ```typescript logger.debug('Checking status'); logger.info('Checking status'); // Different level, won't batch with debug logger.debug('Checking status'); // Will batch with first debug message // Results in separate batches for debug and info levels ``` #### Context Separation Child loggers maintain separate deduplication tracking: ```typescript const rootLogger = new LoggerService({ deduplication: { enabled: true } }); const userLogger = rootLogger.forContext('user'); const adminLogger = rootLogger.forContext('admin'); // These are tracked separately rootLogger.info('System status'); userLogger.info('System status'); adminLogger.info('System status'); // Results in three separate log entries ``` ### Configuration Options ```typescript interface DeduplicationConfig { enabled: boolean; // Enable/disable deduplication windowMs?: number; // Time window in milliseconds (default: 1000) flushOnCritical?: boolean; // Flush batches on error/fatal (default: true) } ``` ### Environment Variable Support You can configure deduplication via environment variables: ```bash # Enable deduplication with 2-second window LOG_DEDUP_ENABLED=true LOG_DEDUP_WINDOW=2000 LOG_DEDUP_FLUSH_CRITICAL=true ``` ### Best Practices 1. **Use for High-Volume Scenarios**: Enable deduplication when you expect repeated messages (polling, retry loops, etc.) 2. **Keep Short Windows**: Use shorter time windows (1-5 seconds) to maintain responsiveness 3. **Critical Messages**: Always keep `flushOnCritical: true` to ensure important errors are seen immediately 4. **Disable for Development**: Consider disabling during development for immediate feedback 5. **Monitor Performance**: While lightweight, consider the memory footprint in extremely high-volume scenarios ### Performance Impact - **Memory**: Minimal overhead - uses in-memory Map with automatic cleanup - **CPU**: Negligible processing overhead per message - **Timing**: Uses Node.js timers with automatic cleanup on flush ## Log Grouping The logger provides methods to visually group related log messages in the console output: ```typescript // Start a group logger.group('Process Name'); // All logs after this will be visually grouped until groupEnd is called logger.info('First step'); logger.debug('Details about first step'); logger.info('Second step'); // End the group logger.groupEnd('Process Name'); // Nested groups logger.group('Parent Process'); logger.info('Parent process started'); logger.group('Child Process'); logger.debug('Child process details'); logger.groupEnd('Child Process'); logger.info('Parent process continued'); logger.groupEnd('Parent Process'); // End a group without specifying name (ends most recent group) logger.group('Another Group'); logger.info('Some info'); logger.groupEnd(); // Ends 'Another Group' ``` Groups are particularly useful for: - Organizing related log messages (e.g., HTTP request/response cycles) - Tracking multi-step processes - Debugging complex operations with multiple sub-operations - Improving readability of logs with many entries ## Enhanced Error Handling The logger provides robust and flexible error handling with multiple overloads, structured serialization, and full backwards compatibility. ### Error Method Overloads The `error()` and `fatal()` methods support multiple call signatures for maximum flexibility: ```typescript import { LoggerService } from '@analog-tools/logger'; const logger = new LoggerService({ name: 'my-app' }); // 1. Simple message logger.error('Something went wrong'); // 2. Error object only const dbError = new Error('Connection failed'); logger.error(dbError); // 3. Message with Error object logger.error('Database operation failed', dbError); // 4. Message with structured metadata logger.error('Validation failed', { userId: '12345', field: 'email', value: 'invalid-email', timestamp: Date.now() }); // 5. Message with Error and metadata logger.error('Payment processing failed', paymentError, { orderId: 'order-123', amount: 99.99, currency: 'USD' }); // 6. Backwards compatible: message with additional data logger.error('Operation failed', { context: 'user-service' }, { operation: 'createUser' }); ``` ### Structured Error Serialization The logger includes a powerful error serializer that handles complex scenarios: ```typescript import { ErrorSerializer } from '@analog-tools/logger'; // Circular reference handling const obj = { name: 'test' }; obj.self = obj; // Creates circular reference logger.error('Circular reference detected', obj); // Safely serialized // Deep object traversal with limits const deepObj = { level1: { level2: { level3: { level4: 'deep' } } } }; logger.error('Deep object logging', deepObj); // Respects max depth // Custom error serialization const customError = ErrorSerializer.serialize(error, { includeStack: false, // Exclude stack traces maxDepth: 5, // Limit object depth includeNonEnumerable: true // Include hidden properties }); ``` ### LogContext Interface Use the structured `LogContext` interface for consistent logging: ```typescript import { LogContext } from '@analog-tools/logger'; const context: LogContext = { correlationId: 'req-123', userId: 'user-456', requestId: 'api-789', context: { service: 'auth', operation: 'login', duration: 150 } }; logger.error('Authentication failed', authError, context); ``` ### Error Handling Patterns #### Service Layer Error Handling ```typescript class UserService { static INJECTABLE = true; private logger = inject(LoggerService).forContext('UserService'); async createUser(userData: CreateUserRequest): Promise<User> { const context: LogContext = { operation: 'createUser', correlationId: userData.correlationId }; try { const user = await this.userRepository.create(userData); this.logger.info('User created successfully', { ...context, userId: user.id }); return user; } catch (error) { // Enhanced error logging with Error object and context this.logger.error('User creation failed', error, { ...context, email: userData.email, provider: userData.provider }); throw error; } } } ``` ### API Route Error Handling ```typescript // src/server/routes/api/users/[id].ts import { defineEventHandler, getRouterParam, createError } from 'h3'; import { withLogging, LogMetadata } from '@analog-tools/logger'; export default withLogging( defineEventHandler(async (event) => { const logger = inject(LoggerService).forContext('users-api'); const userId = getRouterParam(event, 'id'); const context: LogContext = { requestId: event.context.requestId, method: event.node.req.method, path: event.node.req.url, userId }; try { if (!userId) { logger.warn('User ID missing from request', context); throw createError({ statusCode: 400, statusMessage: 'User ID is required' }); } const user = await getUserById(userId); logger.info('User API request successful', context); return user; } catch (error) { // Log with Error object and structured context logger.error('User API request failed', error, context); throw error; } }), { namespace: 'users-api', level: 'info' } ); ``` ## Performance Considerations Optimize your logging for production environments: ### Log Level Configuration ```typescript // Development const devLogger = new LoggerService({ level: 'debug', name: 'my-app-dev' }); // Production const prodLogger = new LoggerService({ level: 'info', // Avoid trace/debug in production name: 'my-app-prod', disabledContexts: ['verbose-module', 'debug-info'] }); ``` ### Efficient Context Usage ```typescript // Good: Create context loggers once and reuse class DatabaseService { private logger = inject(LoggerService).forContext('database'); async query(sql: string) { this.logger.debug('Executing query', { sql }); // Implementation... } } // Avoid: Creating new context loggers repeatedly async function badExample() { for (let i = 0; i < 1000; i++) { const logger = inject(LoggerService).forContext('loop'); // Inefficient logger.debug(`Iteration ${i}`); } } ``` ### Environment-Based Configuration ```typescript // Use environment variables for production tuning const logger = new LoggerService({ level: process.env['LOG_LEVEL'] || 'info', name: process.env['APP_NAME'] || 'my-app', disabledContexts: process.env['LOG_DISABLED_CONTEXTS']?.split(',') || [] }); ``` ## Troubleshooting ### Common Issues #### Missing @analog-tools/inject dependency **Problem:** TypeScript errors or runtime errors related to `inject()` function. **Solution:** Install the peer dependency or use standalone approach: ```bash # Option 1: Install the peer dependency npm install @analog-tools/inject # Option 2: Remove injection usage and use standalone # Replace inject(LoggerService) with new LoggerService() ``` #### Colors not showing in output **Problem:** Log messages appear without colors in development. **Solution:** Colors are automatically disabled in test environments. To force enable: ```typescript const logger = new LoggerService({ useColors: true }); // Or check if you're in a test environment const isTest = process.env['NODE_ENV'] === 'test' || process.env['VITEST'] === 'true'; const logger = new LoggerService({ useColors: !isTest }); ``` #### Context logging not working **Problem:** Child logger contexts don't appear in output. **Solution:** Check if the context is in your disabled contexts list: ```typescript // Clear disabled contexts logger.setDisabledContexts([]); // Or check current disabled contexts console.log(logger.getDisabledContexts()); // Remove specific context from disabled list const current = logger.getDisabledContexts(); const filtered = current.filter(ctx => ctx !== 'my-context'); logger.setDisabledContexts(filtered); ``` #### Log levels not working as expected **Problem:** Debug messages appear in production or info messages don't show. **Solution:** Check your log level configuration: ```typescript // Check current log level console.log('Current log level:', logger.getLogLevel()); // Set log level explicitly const logger = new LoggerService({ level: 'info' }); // Or use environment variable // LOG_LEVEL=debug npm start ``` ### Performance Issues #### High memory usage with extensive logging **Problem:** Application memory usage grows with heavy logging. **Solutions:** - Use appropriate log levels for production (`info` and above) - Disable verbose contexts: `LOG_DISABLED_CONTEXTS=debug-module,verbose-api` - Avoid logging large objects frequently - Use structured logging instead of string concatenation #### Slow application startup **Problem:** Application takes long to start with logger configuration. **Solution:** Optimize logger initialization: ```typescript // Lazy initialization let _logger: LoggerService; function getLogger() { if (!_logger) { _logger = new LoggerService({ level: 'info', name: 'my-app' }); } return _logger; } ``` ## Best Practices ### Logging Best Practices 1. **Use structured logging**: Pass structured data as additional parameters instead of string concatenation 2. **Create context-specific loggers**: Use `forContext()` to create loggers for different parts of your application 3. **Configure log levels appropriately**: Use `trace` and `debug` for development, `info` and above for production 4. **Log meaningful errors**: Include the actual Error object in error logs, not just messages 5. **Disable noisy contexts**: Use `disabledContexts` to selectively disable logging for specific contexts 6. **Use dependency injection**: Leverage @analog-tools/inject for cleaner code organization 7. **Use Nitro integration**: Use the provided middleware and handlers in AnalogJS API routes 8. **Configure via environment**: Use environment variables for production configuration ### Enhanced Error Handling Best Practices 9. **Use appropriate error overloads**: Choose the right method signature for your use case: ```typescript // ✅ For simple errors logger.error('Operation failed'); // ✅ For errors with Error objects logger.error('Database error', dbError); // ✅ For structured metadata logger.error('Validation failed', { field: 'email', value: 'invalid' }); // ✅ For comprehensive error logging logger.error('Payment failed', paymentError, { orderId: '123', amount: 99.99 }); ``` 10. **Use LogContext interface**: Structure your metadata consistently: ```typescript const context: LogContext = { correlationId: 'req-123', userId: 'user-456', context: { service: 'payments', operation: 'charge' } }; logger.error('Payment processing failed', paymentError, context); ``` 11. **Handle circular references**: The logger automatically handles circular references, but be mindful of object complexity: ```typescript // ✅ Safe - circular references are detected and handled const user = { id: '123', profile: {} }; user.profile.user = user; logger.error('User error', user); ``` 12. **Configure serialization options**: Customize error serialization when needed: ```typescript import { ErrorSerializer } from '@analog-tools/logger'; // For sensitive environments - exclude stack traces const serialized = ErrorSerializer.serialize(error, { includeStack: false, maxDepth: 3, includeNonEnumerable: true }); ``` ## Security Considerations ### Avoiding Sensitive Data in Logs Never log sensitive information such as passwords, API keys, or personal data: ```typescript // ❌ DON'T: Log sensitive data logger.info('User login attempt', { username: 'john.doe', password: 'secret123', // Never log passwords! apiKey: 'sk-1234567890' // Never log API keys! }); // ✅ DO: Log safely with sanitized data logger.info('User login attempt', { username: 'john.doe', hasPassword: true, // Boolean indicator instead apiKeyPrefix: 'sk-***', // Partial information only timestamp: new Date().toISOString() }); ``` ### Sanitizing User Input Always sanitize user input before logging: ```typescript import { LoggerService } from '@analog-tools/logger'; function sanitizeForLogging(input: unknown): unknown { if (typeof input === 'string') { // Remove potential sensitive patterns return input .replace(/password[=:]\s*\S+/gi, 'password=***') .replace(/token[=:]\s*\S+/gi, 'token=***') .replace(/key[=:]\s*\S+/gi, 'key=***'); } return input; } // Usage in your application const logger = new LoggerService({ name: 'api' }); function handleUserInput(userInput: string) { logger.info('Processing user input', { input: sanitizeForLogging(userInput), length: userInput.length }); } ``` ### Production Log Security ```typescript // Configure different log levels for different environments const logger = new LoggerService({ level: process.env['NODE_ENV'] === 'production' ? 'warn' : 'debug', name: 'secure-app', // Disable detailed contexts in production disabledContexts: process.env['NODE_ENV'] === 'production' ? ['debug-details', 'user-data', 'internal'] : [] }); ``` ### Compliance Considerations For applications handling sensitive data (GDPR, HIPAA, etc.): ```typescript // Create a compliance-aware logger class ComplianceLogger extends LoggerService { static INJECTABLE = true; // Override methods to add compliance checks info(message: string, ...data: unknown[]): void { const sanitizedData = data.map(item => this.sanitizeCompliance(item)); super.info(message, ...sanitizedData); } private sanitizeCompliance(data: unknown): unknown { if (typeof data === 'object' && data !== null) { const sanitized = { ...data as Record<string, unknown> }; // Remove common PII fields delete sanitized.ssn; delete sanitized.creditCard; delete sanitized.email; // or hash it delete sanitized.phoneNumber; return sanitized; } return data; } } ``` ## Contributing We welcome contributions! Please see our [Contributing Guide](../../CONTRIBUTING.md) for details. ### Development Setup 1. Clone the repository 2. Install dependencies: `npm install` 3. Run tests: `nx test` 4. Serve Demo App: `nx serve` ### Reporting Issues - Use [GitHub Issues](https://github.com/MrBacony/analog-tools/issues) for bug reports and feature requests - Include code examples and error messages - Specify your environment (Node.js version, AnalogJS version, etc.) ### Support Channels - 🐛 **Bug Reports:** [GitHub Issues](https://github.com/MrBacony/analog-tools/issues) - 💡 **Feature Requests:** [GitHub Discussions](https://github.com/MrBacony/analog-tools/discussions) - 📖 **Documentation:** Contribute improvements via Pull Requests ## Future Plans - Support for custom transports (file, HTTP, etc.) - Support for log rotation and compression - Structured JSON logging format option - Performance optimizations for high-volume logging ## License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. ## Metadata-Based Styling and Icon System @analog-tools/logger now supports a powerful metadata-based styling and icon system for all log methods. This enables semantic and custom styles, emoji icons, and per-call or global configuration for beautiful, expressive logs. ### Usage Example ```typescript import { LoggerService, ColorEnum } from '@analog-tools/logger'; const logger = new LoggerService({ level: 'info', name: 'my-app', // Optional: global style/icon config styles: { highlight: { color: ColorEnum.LemonYellow, bold: true } }, icons: { success: '', info: 'ℹ️' } }); // Per-call metadata for style and icon logger.info('Success!', { style: 'success', icon: '' }); logger.warn('Be careful', { icon: '⚠️' }); logger.info('Custom dreamy color', { style: { color: ColorEnum.DeepPurple, underline: true } }); logger.info('With emoji', { icon: '🚀' }); ``` #### Supported Features - **Semantic styles**: Use names like `'success'`, `'warning'`, `'highlight'`, etc. - **Custom styles**: Use `ColorEnum` and style config for color, bold, underline, background, etc. - **Emoji icons**: Use any emoji or semantic icon name (e.g., `'success'`, `'info'`). - **Global config**: Set default styles/icons in `LoggerConfig`. - **Per-call override**: Pass metadata as the last argument to any log method. - **Fallback/warning**: Unknown styles/icons trigger a warning and fallback to defaults. #### Example: Highlighted Info (replaces `info2()`) ```typescript // Old: // logger.info2('Important!'); // New: logger.info('Important!', { style: 'highlight', icon: '⭐️' }); ``` See the [Migration Guide](./OPTIMIZATION.md) for upgrade instructions and more examples. ---