npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@analog-tools/logger

v0.0.18

Published

Logging utility for AnalogJS applications

Readme

@analog-tools/logger

Early Development Stage -- Breaking changes may happen frequently as the APIs evolve.

Structured logging for AnalogJS, Nitro, and H3-based server applications. Provides context-based child loggers, log deduplication, error serialization with circular reference handling, and pluggable output formatters (console with ANSI colors, JSON for log aggregation, or custom).

npm version License: MIT

Table of Contents

Installation

npm install @analog-tools/logger

Peer dependency (optional -- only required for Nitro integration and DI patterns):

npm install @analog-tools/inject

Quick Start

import { LoggerService } from '@analog-tools/logger';

const logger = new LoggerService({
  level: 'info',
  name: 'my-app',
});

logger.info('Server started on port 3000');
logger.warn('Cache miss for key user:123');
logger.error('Failed to connect to database', new Error('ECONNREFUSED'));

// Create a child logger with context
const dbLogger = logger.forContext('database');
dbLogger.debug('Query executed in 42ms');

Log Levels

Seven levels, ordered by severity:

| Level | Numeric | Console Method | Use Case | |----------|---------|----------------|-----------------------------------------| | trace | 0 | console.trace | Granular diagnostics, function entry/exit | | debug | 1 | console.debug | Development-time debugging | | info | 2 | console.info | Normal operational events | | warn | 3 | console.warn | Recoverable issues, deprecation notices | | error | 4 | console.error | Failures requiring attention | | fatal | 5 | console.error | Unrecoverable errors, imminent shutdown | | silent | 6 | (none) | Suppress all output |

Messages at levels below the configured threshold are discarded without evaluation:

const logger = new LoggerService({ level: 'warn' });

logger.debug('skipped'); // not logged
logger.info('skipped');  // not logged
logger.warn('logged');   // logged
logger.error('logged');  // logged

The LogLevel type provides compile-time checking:

import { LogLevel, isValidLogLevel } from '@analog-tools/logger';

const level: LogLevel = 'debug'; // IntelliSense autocomplete

// Runtime validation for external input
const envLevel = process.env['LOG_LEVEL'] || 'info';
if (isValidLogLevel(envLevel)) {
  new LoggerService({ level: envLevel });
}

Invalid levels passed to the constructor throw a LoggerError:

import { LoggerError } from '@analog-tools/logger';

try {
  new LoggerService({ level: 'verbose' as LogLevel }); // throws LoggerError
} catch (e) {
  // "Invalid log level: verbose. Valid levels: trace, debug, info, warn, error, fatal, silent."
}

Context-Based Logging

forContext() creates child loggers that share configuration with the parent but include a context label in output. Child loggers are cached -- calling forContext('db') twice returns the same instance.

const logger = new LoggerService({ level: 'debug', name: 'my-app' });

const authLogger = logger.forContext('auth');
const dbLogger = logger.forContext('database');

authLogger.info('User authenticated');
// Output: [my-app:auth] User authenticated

dbLogger.error('Connection pool exhausted');
// Output: [my-app:database] Connection pool exhausted

Disabling Contexts

Suppress output from specific contexts without changing log levels:

const logger = new LoggerService({
  level: 'debug',
  name: 'my-app',
  disabledContexts: ['verbose-polling', 'health-check'],
});

const polling = logger.forContext('verbose-polling');
polling.info('tick'); // suppressed

// Modify at runtime
logger.setDisabledContexts(['health-check']);
polling.info('tick'); // now logged

Formatters

The logger supports pluggable output formatters. The default is ConsoleFormatter (ANSI-colored terminal output). A JsonFormatter is included for structured log aggregation, and you can provide a custom formatter function.

Console Formatter (default)

const logger = new LoggerService({
  name: 'my-app',
  level: 'info',
  useColors: true, // default; set false to strip ANSI codes
});

JSON Formatter

Outputs one JSON object per log line -- useful for piping to log aggregation tools (Datadog, ELK, CloudWatch):

import { FormatterFactory } from '@analog-tools/logger';

const logger = new LoggerService({
  name: 'my-app',
  level: 'info',
  formatter: FormatterFactory.createJson(),
});

logger.info('Request handled', { userId: '42', duration: 150 });
// Output: {"timestamp":"2025-01-15T10:30:00.000Z","level":"info","logger":"my-app","message":"Request handled","metadata":{"userId":"42","duration":150}}

// Pretty-print for development
const devLogger = new LoggerService({
  formatter: FormatterFactory.createJson({ prettyPrint: true }),
});

Custom Formatter

import { FormatterFactory, LogEntry } from '@analog-tools/logger';

const logger = new LoggerService({
  name: 'my-app',
  formatter: FormatterFactory.createCustom((entry: LogEntry) => {
    return `${entry.timestamp.toISOString()} [${entry.level}] ${entry.message}`;
  }),
});

The LogEntry interface passed to custom formatters contains:

interface LogEntry {
  readonly level: LogLevelEnum;
  readonly message: string;
  readonly logger: string;
  readonly timestamp: Date;
  readonly context?: string;
  readonly metadata?: Record<string, unknown>;
  readonly error?: Error;
  readonly styling?: LogStyling;
  readonly correlationId?: string;
}

Error Handling

The error() and fatal() methods accept multiple call signatures:

// Message only
logger.error('Connection refused');

// Error object only (message extracted automatically)
logger.error(new Error('ECONNREFUSED'));

// Message + Error
logger.error('Database query failed', new Error('timeout'));

// Message + structured context
logger.error('Validation failed', { field: 'email', value: 'not-an-email' });

// Message + Error + context
logger.error('Payment failed', paymentError, {
  orderId: 'ord-123',
  amount: 99.99,
});

ErrorSerializer

Errors are serialized using ErrorSerializer, which handles:

  • Circular references (replaced with [Circular Reference])
  • Deep object graphs (configurable maxDepth, default 10)
  • Error.cause chains (Node.js 16+)
  • Non-enumerable properties (opt-in)

Direct usage for custom serialization needs:

import { ErrorSerializer } from '@analog-tools/logger';

const serialized = ErrorSerializer.serialize(error, {
  includeStack: false,        // omit stack traces in production logs
  maxDepth: 5,                // limit object traversal depth
  includeNonEnumerable: true, // include non-enumerable properties
});

The serializer caches results (up to 100 entries) based on error identity to avoid repeated serialization of the same error.

Log Deduplication

LogDeduplicator batches identical messages within a time window and outputs them with a repeat count. Only simple messages (no metadata or extra data) are deduplicated.

const logger = new LoggerService({
  level: 'info',
  name: 'my-app',
  deduplication: {
    enabled: true,
    windowMs: 5000,        // batch window (default: 5000ms)
    flushOnCritical: true, // flush pending messages on error/fatal (default: true)
  },
});

// These three calls produce one output line after the window expires:
logger.info('Retrying connection...');
logger.info('Retrying connection...');
logger.info('Retrying connection...');
// Output after 5s: [my-app] Retrying connection... (x3)

Behavior details:

  • Error and fatal messages bypass deduplication entirely and log immediately.
  • Messages with metadata, styling, or extra data arguments are never batched.
  • Different contexts are tracked separately (a child logger's messages do not batch with the parent's).
  • Different log levels are tracked separately even for the same message text.

Log Sanitization

The logger sanitizes sensitive data by default to prevent accidental exposure in logs. Sanitization is enabled out of the box with secure defaults.

Default Behavior

Sensitive data patterns are automatically redacted:

const logger = new LoggerService({ level: 'info' });

logger.info('User login', {
  password: 'mySecretPassword123',  // → [REDACTED]
  token: 'abc123def456ghi789jkl',   // → [TOKEN] (base64-like)
  email: '[email protected]',        // → [EMAIL]
  creditCard: '4532-1234-5678-9012', // → [CARD]
  ip: '192.168.1.1',                // → [IP]
});

Sensitive Key Detection

Object properties with sensitive names are fully redacted regardless of value:

  • password, token, secret, apiKey, authorization, credential, private

Log Injection Protection

Control characters (newlines, tabs, etc.) are escaped to prevent log injection attacks:

logger.info('Malicious\ninjection\tattempt');
// Output: Malicious\ninjection\tattempt
// (displayed with literal backslash-n and backslash-t, not actual newline/tab)

Configuration

Opt-Out for Development

const logger = new LoggerService({
  level: 'debug',
  sanitization: { enabled: false }, // Disable for local debugging
});

Custom Rules (Append to Defaults)

const logger = new LoggerService({
  sanitization: {
    customRules: [
      { pattern: /userId:\s*\d+/gi, replacement: 'userId: [USER_ID]' },
    ],
  },
});

Replace Default Rules

const logger = new LoggerService({
  sanitization: {
    rules: [
      { pattern: /secret/gi, replacement: '***' },
    ],
  },
});

Sanitization Strategies

| Strategy | Description | Example | |----------|-------------|---------| | mask | Replace with string (default) | secret[REDACTED] | | remove | Remove matched text | my secretmy | | hash | Replace with truncated hash | secret[HASH:a3f2b1c8] | | custom | Custom handler function | 1234-5678****-5678 |

const logger = new LoggerService({
  sanitization: {
    customRules: [
      // Hash emails for correlation without exposing PII
      { pattern: /\b[\w.]+@[\w.]+\.\w+\b/g, strategy: 'hash', hashLength: 8 },
      
      // Partial mask for credit cards
      {
        pattern: /\b(\d{4})-?\d{4}-?\d{4}-?(\d{4})\b/g,
        strategy: 'custom',
        customHandler: (match: string) => `****-****-****-${match.slice(-4)}`,
      },
    ],
  },
});

Performance Notes

  • Rules are compiled once at logger construction
  • Sanitization skipped entirely when enabled: false
  • Object traversal respects maxDepth (default: 10) to prevent stack overflow
  • Circular references handled safely

Nitro/H3 Integration

Two utilities for adding logging to Nitro event handlers. Both require @analog-tools/inject as they use inject(LoggerService) internally.

Middleware

Attaches a logger to the event context and logs incoming requests at debug level:

// src/server/middleware/logging.ts
import { createLoggerMiddleware } from '@analog-tools/logger';

export default createLoggerMiddleware('api');
// Adds event.context['logger'] as a LoggerService instance

Handler Wrapper

Wraps a handler with request timing and error logging:

// src/server/routes/api/products/index.ts
import { defineEventHandler } from 'h3';
import { withLogging } from '@analog-tools/logger';

export default withLogging(
  defineEventHandler(() => {
    return { products: [] };
  }),
  {
    namespace: 'products-api', // context name (default: 'api')
    level: 'info',             // log level for success (default: 'debug')
    logResponse: true,         // include response body in log (default: false)
  }
);
// On success: "[my-app:products-api] Request completed in 12ms"
// On error: "[my-app:products-api] Request failed after 45ms" + serialized error

Usage with @analog-tools/inject

The logger works standalone (direct instantiation) or with the @analog-tools/inject DI system. LoggerService has static INJECTABLE = true set, so it can be registered and injected:

import { inject, registerService } from '@analog-tools/inject';
import { LoggerService } from '@analog-tools/logger';

// Register with configuration
registerService(LoggerService, {
  level: 'debug',
  name: 'my-app',
  disabledContexts: ['health-check'],
});

// Inject in services
class OrderService {
  private logger = inject(LoggerService).forContext('orders');

  async createOrder(data: OrderData) {
    this.logger.info('Creating order', { customerId: data.customerId });
    // ...
  }
}

For projects not using DI, create and share instances directly:

// src/server/utils/logger.ts
import { LoggerService } from '@analog-tools/logger';

export const logger = new LoggerService({
  level: process.env['LOG_LEVEL'] || 'info',
  name: 'my-api',
});

Lazy Message Evaluation

Pass a function instead of a string to defer expensive computations. The function is only called if the message will actually be logged:

// Always evaluates JSON.stringify, even when debug is disabled
logger.debug(JSON.stringify(largeObject)); // wasteful

// Only evaluates when debug level is active
logger.debug(() => JSON.stringify(largeObject));

// Complex computation
logger.trace(() => {
  const metrics = gatherSystemMetrics();
  return `System: ${JSON.stringify(metrics)}`;
});

If the function throws, the logger catches the error and outputs a placeholder message instead of crashing:

logger.info(() => {
  throw new Error('oops');
});
// Output: [my-app] [Message evaluation failed: Error: oops]

Use lazy evaluation only for genuinely expensive operations. For plain strings, pass them directly -- the overhead of a function call is not worth it.

Styling and Icons

Log messages can include ANSI styling and emoji icons via the metadata parameter. This works with the console formatter only (JSON formatter ignores styling).

import { LoggerService, ColorEnum } from '@analog-tools/logger';

const logger = new LoggerService({
  level: 'info',
  name: 'my-app',
  styles: {
    highlight: { color: ColorEnum.LemonYellow, bold: true },
    success: { color: ColorEnum.ForestGreen },
  },
  icons: {
    success: '✅',
    warning: '⚠️',
  },
});

// Semantic style name
logger.info('Deployment complete', { style: 'success', icon: '✅' });

// Inline style object
logger.warn('Disk usage at 90%', {
  style: { color: ColorEnum.TangerineOrange, bold: true },
  icon: '⚠️',
});

Available semantic styles: highlight, accent, attention, success, warning, error, info, debug.

Color control:

// Disable ANSI colors (icons still appear)
logger.setUseColors(false);

// Or via constructor
new LoggerService({ useColors: false });

Correlation ID Tracking

Attach a correlation ID to a logger instance for request-scoped tracing. The ID appears in formatted output (both console and JSON formatters support it):

const logger = new LoggerService({ name: 'my-app' });
logger.setCorrelationId('req-abc-123');

logger.info('Processing request');
// JSON output includes: "correlationId": "req-abc-123"

logger.clearCorrelationId();

You can also set it at construction time:

const logger = new LoggerService({
  name: 'my-app',
  correlationId: 'req-abc-123',
});

Log Grouping

Visual grouping uses console.group / console.groupEnd under the hood:

logger.group('Database Migration');
logger.info('Running migration 001_create_users');
logger.info('Running migration 002_add_indexes');
logger.groupEnd('Database Migration');

// Nested groups
logger.group('Request Processing');
logger.info('Validating input');
logger.group('Database Queries');
logger.debug('SELECT * FROM users WHERE id = $1');
logger.groupEnd('Database Queries');
logger.groupEnd('Request Processing');

// End most recent group without specifying name
logger.group('Batch Job');
logger.info('Processing...');
logger.groupEnd();

Environment Variables

LOG_LEVEL=debug                              # Default log level (default: info)
LOG_DISABLED_CONTEXTS=health-check,polling   # Comma-separated contexts to suppress

These are read at construction time if the corresponding config option is not explicitly provided.

API Reference

LoggerService

class LoggerService {
  static INJECTABLE = true;

  constructor(config?: LoggerConfig);

  // Child loggers
  forContext(context: string): LoggerService;

  // Log methods (all accept string | () => string)
  trace(message: LogMessage, ...data: unknown[]): void;
  debug(message: LogMessage, ...data: unknown[]): void;
  info(message: LogMessage, ...data: unknown[]): void;
  warn(message: LogMessage, ...data: unknown[]): void;
  error(messageOrError: string | Error, ...args: unknown[]): void;
  fatal(messageOrError: string | Error, ...args: unknown[]): void;

  // Grouping
  group(groupName: string): void;
  groupEnd(groupName?: string): void;

  // Configuration
  getLogLevel(): LogLevelEnum;
  getDisabledContexts(): string[];
  setDisabledContexts(contexts: string[]): void;
  setUseColors(enabled: boolean): void;
  getUseColors(): boolean;
  setCorrelationId(id: string): void;
  getCorrelationId(): string | undefined;
  clearCorrelationId(): void;
}

LoggerConfig

interface LoggerConfig {
  level?: LogLevel;             // default: 'info' (or LOG_LEVEL env var)
  name?: string;                // default: 'analog-tools'
  disabledContexts?: string[];  // contexts to suppress
  useColors?: boolean;          // default: true
  formatter?: ILogFormatter;    // default: ConsoleFormatter
  correlationId?: string;       // request-scoped tracing ID
  styles?: Partial<Record<SemanticStyleName, StyleConfig>>;
  icons?: Partial<Record<string, Icon>>;
  deduplication?: {
    enabled: boolean;
    windowMs?: number;          // default: 5000
    flushOnCritical?: boolean;  // default: true
  };
}

FormatterFactory

class FormatterFactory {
  static createConsole(config?: { useColors?: boolean }): ILogFormatter;
  static createJson(config?: { prettyPrint?: boolean }): ILogFormatter;
  static createCustom(fn: (entry: LogEntry) => string): ILogFormatter;
}

ErrorSerializer

class ErrorSerializer {
  static serialize(
    error: unknown,
    options?: {
      includeStack?: boolean;        // default: true
      maxDepth?: number;             // default: 10
      includeNonEnumerable?: boolean; // default: false
    }
  ): StructuredError | string;
}

LogDeduplicator

class LogDeduplicator {
  constructor(config: DeduplicationConfig, formatter: ILogFormatter, loggerName: string);
  addMessage(level: LogLevelEnum, message: string, context?: string): boolean;
  flush(): void;
  destroy(): void;
}

Nitro Utilities

function createLoggerMiddleware(namespace?: string): EventHandler;

function withLogging<T extends EventHandlerRequest>(
  handler: EventHandler<T>,
  options?: {
    namespace?: string;
    level?: LogLevel;
    logResponse?: boolean;
  }
): EventHandler<T>;

License

MIT