Skip to main content

Overview

HTTP Ledger allows you to customize how logs are categorized and formatted through two powerful options: customLogLevel and customFormatter. These enable integration with your existing logging infrastructure and custom log requirements.

customLogLevel

customLogLevel
(logData: LogData) => LogLevel
Custom function to determine the log level based on log data. Must return 'info', 'warn', or 'error'.

Default Behavior

Without a custom log level function, HTTP Ledger uses this logic:
// From src/utils/advancedFeatures.ts:50-61
function getDefaultLogLevel(logData: LogData): LogLevel {
  if (logData.error) {
    return 'error';
  }
  if (logData.statusCode >= 400) {
    return 'warn';
  }
  return 'info';
}

Basic Usage

app.use(logger({
  customLogLevel: (logData) => {
    if (logData.statusCode >= 500) return 'error';
    if (logData.statusCode >= 400) return 'warn';
    return 'info';
  }
}));

Advanced Examples

Log Level Based on Response Time

app.use(logger({
  customLogLevel: (logData) => {
    // Slow requests are warnings
    if (logData.timeTaken > 1000) return 'warn';
    
    // Errors
    if (logData.statusCode >= 400) return 'error';
    
    return 'info';
  }
}));

Log Level Based on Endpoint

app.use(logger({
  customLogLevel: (logData) => {
    // Admin endpoints always warn level
    if (logData.url.startsWith('/admin')) return 'warn';
    
    // Critical endpoints errors
    if (logData.url.startsWith('/api/payment') && logData.statusCode >= 400) {
      return 'error';
    }
    
    return 'info';
  }
}));

Combining Multiple Criteria

import logger, { LogData, LogLevel } from 'http-ledger';

app.use(logger({
  customLogLevel: (logData: LogData): LogLevel => {
    // Server errors
    if (logData.statusCode >= 500) return 'error';
    
    // Client errors on critical paths
    if (logData.statusCode >= 400 && logData.url.includes('/checkout')) {
      return 'error';
    }
    
    // Slow responses
    if (logData.timeTaken > 2000) return 'warn';
    
    // Large responses
    if (logData.responseSize > 1000000) return 'warn';
    
    // Client errors on non-critical paths
    if (logData.statusCode >= 400) return 'warn';
    
    return 'info';
  }
}));

customFormatter

customFormatter
(logData: LogData) => unknown
Custom function to transform log data before it’s logged. Receives the complete LogData object and should return the formatted data.

Basic Usage

app.use(logger({
  customFormatter: (logData) => ({
    ...logData,
    environment: process.env.NODE_ENV,
    service: 'user-api',
    version: '1.0.0'
  })
}));

Adding Metadata

app.use(logger({
  customFormatter: (logData) => ({
    // Original log data
    ...logData,
    
    // Additional metadata
    timestamp: new Date().toISOString(),
    environment: process.env.NODE_ENV,
    service: process.env.SERVICE_NAME,
    version: process.env.APP_VERSION,
    region: process.env.AWS_REGION,
    
    // Computed fields
    isSlowRequest: logData.timeTaken > 1000,
    isLargePayload: logData.requestSize > 100000,
    requestType: logData.method === 'GET' ? 'read' : 'write'
  })
}));

Restructuring Logs

app.use(logger({
  customFormatter: (logData) => ({
    // Flatten structure for log aggregation systems
    '@timestamp': logData.timestamp.request,
    'http.method': logData.method,
    'http.url': logData.url,
    'http.status_code': logData.statusCode,
    'http.response_time_ms': logData.timeTaken,
    'http.request_size_bytes': logData.requestSize,
    'http.response_size_bytes': logData.responseSize,
    'user_agent': logData.userAgent,
    'client_ip': logData.ipInfo?.ip
  })
}));

ELK Stack Format

app.use(logger({
  customFormatter: (logData) => ({
    '@timestamp': logData.timestamp.request,
    '@version': '1',
    message: `${logData.method} ${logData.url} ${logData.statusCode}`,
    logger_name: 'http-ledger',
    level: logData.logLevel?.toUpperCase() || 'INFO',
    http: {
      method: logData.method,
      url: logData.url,
      status_code: logData.statusCode,
      response_time: logData.timeTaken,
      request_size: logData.requestSize,
      response_size: logData.responseSize,
      version: logData.httpVersion
    },
    user_agent: logData.userAgent,
    geo: logData.ipInfo,
    error: logData.error
  })
}));

Datadog Format

app.use(logger({
  customFormatter: (logData) => ({
    ddsource: 'nodejs',
    service: 'my-api',
    hostname: logData.hostname,
    message: `${logData.method} ${logData.url} completed with status ${logData.statusCode}`,
    status: logData.logLevel || 'info',
    
    http: {
      method: logData.method,
      url: logData.url,
      status_code: logData.statusCode,
      useragent: logData.userAgent
    },
    
    network: {
      client: {
        ip: logData.ipInfo?.ip
      }
    },
    
    duration: logData.timeTaken * 1000000, // Convert to nanoseconds
    
    error: logData.error ? {
      message: logData.error.message,
      stack: logData.error.stack
    } : undefined
  })
}));

Combining Both Options

You can use both customLogLevel and customFormatter together:
const express = require('express');
const logger = require('http-ledger');

const app = express();

app.use(logger({
  customLogLevel: (logData) => {
    if (logData.statusCode >= 500) return 'error';
    if (logData.statusCode >= 400) return 'warn';
    if (logData.timeTaken > 1000) return 'warn';
    return 'info';
  },
  
  customFormatter: (logData) => ({
    ...logData,
    environment: process.env.NODE_ENV,
    service: 'my-api',
    
    // Add performance indicators
    performance: {
      isSlow: logData.timeTaken > 1000,
      isVeryLarge: logData.responseSize > 1000000
    },
    
    // Categorize the request
    category: (() => {
      if (logData.url.startsWith('/admin')) return 'admin';
      if (logData.url.startsWith('/api')) return 'api';
      return 'web';
    })()
  })
}));

Integration Examples

Winston Logger Integration

const winston = require('winston');
const winstonLogger = winston.createLogger({
  transports: [new winston.transports.Console()]
});

app.use(logger({
  customLogLevel: (logData) => {
    // Map to Winston levels
    if (logData.statusCode >= 500) return 'error';
    if (logData.statusCode >= 400) return 'warn';
    return 'info';
  },
  
  onLog: (logData) => {
    winstonLogger.log(logData.logLevel || 'info', 'HTTP Request', logData);
  }
}));

Pino Logger Integration

const pino = require('pino');
const pinoLogger = pino();

app.use(logger({
  customFormatter: (logData) => ({
    req: {
      method: logData.method,
      url: logData.url,
      headers: logData.headers,
      remoteAddress: logData.ipInfo?.ip
    },
    res: {
      statusCode: logData.statusCode,
      responseTime: logData.timeTaken
    }
  }),
  
  onLog: (logData) => {
    pinoLogger[logData.logLevel || 'info'](logData);
  }
}));

Error Handling

If customLogLevel or customFormatter throw an error, the middleware continues with default behavior. Errors are logged but don’t break request handling.
From src/index.ts:244-246:
// Apply custom log level if provided
if (customLogLevel) {
  log.logLevel = customLogLevel(log);
}

// Apply custom formatter if provided
if (customFormatter) {
  return customFormatter(log) as LogData;
}

Best Practices

Keep It Simple

Custom functions should be fast and not throw errors. Avoid async operations.

Test Thoroughly

Test custom functions with various log data scenarios, including errors.

Document Format

If using custom formatters, document your log format for team members.

Monitor Performance

Complex formatters can add overhead. Monitor impact on request timing.

External Integrations

Use onLog callback to send formatted logs to external services

API Reference

Complete API documentation for all logger options

Build docs developers (and LLMs) love