Logger - The debugging companion you actually want to use

After years of wrestling with logging libraries that seemed designed to make debugging harder, not easier, we built something different. This logger does exactly what you need and nothing you don't.

You know the drill: Your app crashes in production, you check the logs, and get a wall of JSON that tells you everything except what you actually need to know. Or worse, a stack trace that looks like this:

Error: Something broke
at file:///Users/dev/project/node_modules/@framework/router/dist/index.js:234:12
at file:///Users/dev/project/node_modules/@framework/core/dist/handler.js:89:5

Good luck figuring out where YOUR code went wrong.

Development Mode: Beautiful, scannable terminal output that actually helps

22:14:29 [500] (⚡ 341 µs) GET /api/users/123
[Error] ValidationError: User validation failed
Stack trace:
at Route.handler (./src/handlers/users.js line:42)
at Router.handleRequest (./packages/wings/core/router.js line:747)
Additional properties:
code: VALIDATION_ERROR
field: email

Production Mode: Structured JSON that compliance auditors love

{
"timestamp": "2024-01-15T22:14:29.123Z",
"level": "error",
"method": "GET",
"path": "/api/users/123",
"statusCode": 500,
"duration": 341,
"success": false,
"errorCount": 1,
"errors": [{"name": "ValidationError", "message": "User validation failed"}]
}

This logger has exactly zero external dependencies. No security vulnerabilities from packages you never asked for. No breaking changes from upstream maintainers who decided to rewrite everything. Just reliable logging that works.

Unlike most loggers that crash your request when an error occurs, this one collects errors throughout the request lifecycle and logs them properly at the end. Your users get proper responses, you get proper debugging info.

Production logs automatically include everything needed for SOC2, ISO 27001, and GDPR compliance:

  • Timestamp (UTC/ISO format)
  • User identity (from Authorization header)
  • Action performed (HTTP method + path)
  • Source IP address
  • Success/failure indicator
  • Request duration for performance monitoring
import { Logger } from '@raven-js/wings/server/logger.js';
import { Router } from '@raven-js/wings/core/router.js';

const router = new Router();

// Development: Beautiful terminal output
const logger = new Logger({ production: false });
router.useEarly(logger);

// Production: Structured JSON logging
const prodLogger = new Logger({
production: true,
includeHeaders: false // Skip headers for cleaner logs
});
router.useEarly(prodLogger);
router.get('/api/data', (ctx) => {
// This error gets collected, not thrown immediately
throw new ValidationError('Invalid input');
// Request continues, after callbacks run, logger sees the error
});

// Logger output:
// 22:14:29 [500] (⚡ 234 µs) GET /api/data
// [Error] ValidationError: Invalid input
// Stack trace:
// → at Route.handler (./src/routes/api.js line:15)
const error = new Error('Database connection failed');
error.code = 'DB_CONN_ERROR';
error.retryAfter = 5000;
error.severity = 'high';
throw error;

// Logger automatically includes custom properties:
// Additional properties:
// code: DB_CONN_ERROR
// retryAfter: 5000
// severity: high

We believe good defaults beat extensive configuration. This logger works perfectly out of the box for 90% of use cases. The few options we provide are there because they actually matter in real applications.

  • production: Switch between beautiful development output and structured JSON
  • includeHeaders: Control header logging (useful for privacy/compliance)
  • includeBody: Debug request bodies in development (never in production)

No configuration files, no environment variables, no magic strings. Just import, instantiate, and start shipping.

Hierarchy

  • Middleware
    • Logger

Constructors

  • Create Logger middleware with error collection architecture

    Integration Trap: Must register early via router.useEarly() to capture all request lifecycle errors. Critical Behavior: Errors are collected during request processing, not thrown immediately. Performance Alert: Console.log is synchronous and blocks the event loop - avoid massive log volumes.

    Parameters

    • Optionaloptions: {
          identifier: string;
          includeBody: boolean;
          includeHeaders: boolean;
          production: boolean;
      } = {}

      Logger configuration

      • identifier: string

        Middleware identifier

      • includeBody: boolean

        Log request bodies (development only, never production)

      • includeHeaders: boolean

        Log request headers (consider privacy for production)

      • production: boolean

        JSON logs (true) vs colored terminal output (false)

    Returns Logger

    // Just works - beautiful colored output, includes headers for debugging
    const logger = new Logger();
    router.useEarly(logger);
    // Clean JSON logs, no headers for privacy/performance
    const logger = new Logger({
    production: true,
    includeHeaders: false
    });
    router.useEarly(logger);
    // Include request bodies for deep debugging
    const logger = new Logger({
    production: false,
    includeBody: true // Only works in development
    });
    router.useEarly(logger);

Properties

includeBody: boolean
includeHeaders: boolean
production: boolean

Accessors

  • get handler(): Handler
  • Gets the middleware handler function.

    Returns the function that will be executed when the middleware runs. This getter provides read-only access to the handler function.

    Returns Handler

    The handler function

    const middleware = new Middleware((ctx) => {
    console.log('Processing request');
    });

    const handler = middleware.handler;
    console.log(typeof handler); // 'function'

    // Can be used for testing or introspection
    if (handler.toString().includes('console.log')) {
    console.log('This middleware includes logging');
    }
  • get identifier(): string
  • Gets the middleware identifier.

    Returns the identifier string or null if no identifier was set. This getter provides read-only access to the identifier.

    Returns string

    The identifier or null if not set

    // Middleware with identifier
    const authMiddleware = new Middleware((ctx) => {}, 'auth');
    console.log(authMiddleware.identifier); // 'auth'

    // Middleware without identifier
    const loggingMiddleware = new Middleware((ctx) => {});
    console.log(loggingMiddleware.identifier); // null

    // Use identifier for conditional logic
    if (middleware.identifier === 'authentication') {
    console.log('This is an authentication middleware');
    }

Methods

  • Executes the middleware handler with the given context.

    This method invokes the middleware handler function, passing the context object as the first argument. The method supports both synchronous and asynchronous handlers, always returning a Promise.

    Execution Flow:

    1. Validates that ctx is a Context instance
    2. Calls the handler function with the context
    3. Returns a Promise that resolves when the handler completes
    4. Propagates any errors thrown by the handler

    Error Handling: Any errors thrown by the handler (synchronous or asynchronous) are propagated to the caller. This allows middleware errors to be handled by the calling code.

    Parameters

    • ctx: Context

      The request/response context

    Returns Promise<void>

    Promise that resolves when the handler completes

    When ctx is not a Context instance

    Any error thrown by the middleware handler

    const middleware = new Middleware((ctx) => {
    console.log(`Processing ${ctx.method} request to ${ctx.path}`);
    ctx.data.processedAt = new Date().toISOString();
    });

    const url = new URL('http://localhost/api/users');
    const ctx = new Context('GET', url, new Headers());

    // Execute middleware
    await middleware.execute(ctx);
    console.log(ctx.data.processedAt); // ISO timestamp

    // Async middleware
    const asyncMiddleware = new Middleware(async (ctx) => {
    await new Promise(resolve => setTimeout(resolve, 100));
    ctx.data.asyncProcessed = true;
    });

    await asyncMiddleware.execute(ctx);
    console.log(ctx.data.asyncProcessed); // true

    // Error handling
    const errorMiddleware = new Middleware((ctx) => {
    throw new Error('Middleware error');
    });

    try {
    await errorMiddleware.execute(ctx);
    } catch (error) {
    console.error('Middleware failed:', error.message);
    }

    // Invalid context
    try {
    await middleware.execute(null);
    } catch (error) {
    console.error(error.message); // "Context must be a Context instance"
    }
  • Checks if this middleware has the same identifier as another middleware.

    This method is used for duplicate detection and middleware comparison. Two middleware are considered to have the same identifier if:

    • Both have non-null identifiers
    • Both identifiers are exactly equal (string comparison)

    Note: Middleware with null identifiers are never considered equal, even if both have null identifiers.

    Parameters

    • other: Middleware

      The other middleware to compare with

    Returns boolean

    True if both middlewares have the same non-null identifier

    const auth1 = new Middleware((ctx) => {}, 'authentication');
    const auth2 = new Middleware((ctx) => {}, 'authentication');
    const logging = new Middleware((ctx) => {}, 'logging');
    const generic = new Middleware((ctx) => {});

    // Same identifier
    auth1.hasSameIdentifier(auth2); // true
    auth2.hasSameIdentifier(auth1); // true

    // Different identifiers
    auth1.hasSameIdentifier(logging); // false
    logging.hasSameIdentifier(auth1); // false

    // Null identifiers (never equal)
    auth1.hasSameIdentifier(generic); // false
    generic.hasSameIdentifier(auth1); // false
    generic.hasSameIdentifier(new Middleware((ctx) => {})); // false

    // Invalid parameters
    auth1.hasSameIdentifier(null); // false
    auth1.hasSameIdentifier({}); // false
    auth1.hasSameIdentifier('string'); // false

    // Use for duplicate prevention
    function addMiddlewareIfNotExists(middlewareList, newMiddleware) {
    const hasDuplicate = middlewareList.some(existing =>
    existing.hasSameIdentifier(newMiddleware)
    );

    if (!hasDuplicate) {
    middlewareList.push(newMiddleware);
    }
    }