Skip to content

Logging Middleware

The logging middleware provides observability for content operations by capturing and recording operation details, timing information, and error states. It offers configurable logging levels, formatting options, and integration with external logging systems.

Overview

Logging middleware intercepts content operations and logs their execution details. It provides visibility into the operation flow, performance metrics, and error conditions, helping with debugging, monitoring, and auditing the content system.

typescript
import { withLogging } from '@lib/content/middleware'

// Create logging middleware
const loggingMiddleware = withLogging({
  level: 'info',
  prefix: 'CONTENT',
})

API Reference

withLogging

typescript
function withLogging(options?: LoggingOptions): Middleware

Creates a new logging middleware instance.

Parameters:

  • options: Configuration options for logging

Returns:

  • A middleware function that logs operation details

LoggingOptions

typescript
interface LoggingOptions {
  /**
   * Log level (default: 'info')
   */
  level?: 'error' | 'warn' | 'info' | 'debug' | 'trace'

  /**
   * Prefix for log messages (default: '')
   */
  prefix?: string

  /**
   * Custom logger implementation (default: console)
   */
  logger?: Logger

  /**
   * Whether to log timing information (default: true)
   */
  timing?: boolean

  /**
   * Log formatter function
   */
  formatter?: (message: string, data: LogData) => string

  /**
   * Operations to log (default: all)
   */
  operations?: Array<'read' | 'write' | 'delete' | 'list' | 'exists' | 'watch'>

  /**
   * Additional fields to include in logs
   */
  fields?: Record<string, any> | (() => Record<string, any>)

  /**
   * Whether to log operation parameters (default: false)
   */
  logParams?: boolean

  /**
   * Whether to log operation results (default: false)
   */
  logResults?: boolean

  /**
   * Maximum string length for result/content logging (default: 100)
   */
  maxLogLength?: number

  /**
   * Whether to log as JSON (default: false)
   */
  json?: boolean

  /**
   * Whether to include stack traces for errors (default: true)
   */
  includeStack?: boolean
}

Logger Interface

typescript
interface Logger {
  error(message: string, ...args: any[]): void
  warn(message: string, ...args: any[]): void
  info(message: string, ...args: any[]): void
  debug(message: string, ...args: any[]): void
  trace(message: string, ...args: any[]): void
}

Logging Process

The logging middleware follows these steps when processing content operations:

  1. Log Operation Start: Record the beginning of an operation
  2. Record Start Time: Track when the operation began
  3. Execute Operation: Allow the operation to proceed
  4. Log Operation Result: Record success or failure
  5. Calculate Duration: Measure operation execution time
typescript
// Simplified implementation
function withLogging(options: LoggingOptions = {}): Middleware {
  const {
    level = 'info',
    prefix = '',
    logger = console,
    timing = true,
    operations = ['read', 'write', 'delete', 'list', 'exists', 'watch'],
    logParams = false,
    logResults = false,
    maxLogLength = 100,
    json = false,
    includeStack = true,
  } = options

  // Function to format log data
  const formatter = options.formatter || defaultFormatter

  // Function to get additional fields
  const getFields =
    typeof options.fields === 'function'
      ? options.fields
      : () => options.fields || {}

  return async (context, next) => {
    // Skip logging for non-configured operations
    if (!operations.includes(context.operation)) {
      return next()
    }

    // Prepare base log data
    const baseData = {
      operation: context.operation,
      uri: context.uri,
      ...getFields(),
    }

    // Add parameters if configured
    if (logParams) {
      baseData.params = truncate(context.options, maxLogLength)
    }

    // Log operation start
    const startTime = Date.now()
    const message = `${prefix} ${context.operation.toUpperCase()} ${context.uri}`

    if (json) {
      logger[level](
        JSON.stringify({
          message,
          ...baseData,
          phase: 'start',
          time: new Date(startTime).toISOString(),
        })
      )
    } else {
      logger[level](formatter(message, { ...baseData, phase: 'start' }))
    }

    try {
      // Execute operation
      const result = await next()

      // Calculate duration
      const endTime = Date.now()
      const duration = endTime - startTime

      // Prepare success log data
      const successData = {
        ...baseData,
        phase: 'complete',
        duration,
      }

      // Add result data if configured
      if (logResults) {
        if (context.operation === 'read' && context.content) {
          successData.contentType = context.content.contentType
          successData.contentSize =
            typeof context.content.data === 'string'
              ? context.content.data.length
              : 'binary'

          if (context.content.metadata) {
            successData.metadata = truncate(
              context.content.metadata,
              maxLogLength
            )
          }
        } else if (context.operation === 'list' && context.results) {
          successData.count = context.results.length
        }
      }

      // Log operation success
      if (json) {
        logger[level](
          JSON.stringify({
            message: `${message} completed`,
            ...successData,
            time: new Date(endTime).toISOString(),
          })
        )
      } else {
        logger[level](
          formatter(`${message} completed in ${duration}ms`, successData)
        )
      }

      return result
    } catch (error) {
      // Calculate duration
      const endTime = Date.now()
      const duration = endTime - startTime

      // Prepare error log data
      const errorData = {
        ...baseData,
        phase: 'error',
        duration,
        error: error.message,
        errorName: error.name,
      }

      // Add stack trace if configured
      if (includeStack && error.stack) {
        errorData.stack = error.stack
      }

      // Log operation error
      if (json) {
        logger.error(
          JSON.stringify({
            message: `${message} failed`,
            ...errorData,
            time: new Date(endTime).toISOString(),
          })
        )
      } else {
        logger.error(
          formatter(
            `${message} failed after ${duration}ms: ${error.message}`,
            errorData
          )
        )
      }

      // Re-throw error
      throw error
    }
  }
}

Log Formatting

The middleware offers flexible log formatting options:

typescript
// Default formatter
function defaultFormatter(message: string, data: LogData): string {
  const details = Object.entries(data)
    .filter(([key]) => key !== 'phase') // exclude phase
    .map(([key, value]) => `${key}=${formatValue(value)}`)
    .join(' ')

  return `${message} ${details}`
}

// Format value for logging
function formatValue(value: any): string {
  if (value === null || value === undefined) {
    return 'null'
  }

  if (typeof value === 'object') {
    try {
      return JSON.stringify(value)
    } catch (e) {
      return '[Object]'
    }
  }

  return String(value)
}

// Truncate long strings
function truncate(value: any, maxLength: number): any {
  if (typeof value === 'string' && value.length > maxLength) {
    return value.substring(0, maxLength) + '...'
  }

  if (typeof value === 'object' && value !== null) {
    const result = Array.isArray(value) ? [] : {}

    for (const [key, innerValue] of Object.entries(value)) {
      result[key] = truncate(innerValue, maxLength)
    }

    return result
  }

  return value
}

Integration with Telemetry

The logging middleware can integrate with the broader telemetry system:

typescript
import { createLogger, LogLevel } from '@lib/telemetry/logger'
import { withLogging } from '@lib/content/middleware'

// Create a logger with telemetry integration
const contentLogger = createLogger({
  name: 'content',
  level: LogLevel.INFO,
  transport: 'console',
  format: 'json',
})

// Create logging middleware with telemetry
const loggingMiddleware = withLogging({
  logger: contentLogger,
  json: true,
  fields: {
    component: 'content-store',
    version: '1.0.0',
  },
})

Log Levels

The middleware supports different log levels for fine-grained control:

  • error: Records operation failures and errors
  • warn: Records potential issues and warnings
  • info: Records normal operation flow (default)
  • debug: Records detailed debugging information
  • trace: Records highly detailed operation tracing

Performance Considerations

To minimize performance impact, consider these optimization options:

  • Set appropriate log level to control verbosity
  • Disable result logging for large content
  • Use selective operation logging for high-volume operations
  • Configure a custom logger with efficient transport
  • Implement asynchronous logging to avoid blocking

Advanced Usage

Selective Operation Logging

typescript
import { withLogging } from '@lib/content/middleware'

// Only log write and delete operations
const mutationLogger = withLogging({
  operations: ['write', 'delete'],
  level: 'info',
  prefix: 'MUTATION',
})

// Only log read operations with more detail
const readLogger = withLogging({
  operations: ['read'],
  level: 'debug',
  logResults: true,
  prefix: 'READ',
})

Environment-Specific Logging

typescript
import { withLogging } from '@lib/content/middleware'
import { isProduction, isDevelopment } from '@lib/utils/env'

// Create appropriate logging based on environment
const environmentLogging = withLogging({
  level: isProduction() ? 'error' : isDevelopment() ? 'debug' : 'info',
  json: isProduction(), // JSON in production, formatted in development
  logResults: !isProduction(), // Don't log results in production
  prefix: isProduction() ? 'CONTENT' : '[CONTENT]',
})

Custom Log Transport

typescript
import { withLogging } from '@lib/content/middleware'

// Custom logger with external transport
const customLogger = {
  error: (message, ...args) => logToService('ERROR', message, args),
  warn: (message, ...args) => logToService('WARN', message, args),
  info: (message, ...args) => logToService('INFO', message, args),
  debug: (message, ...args) => logToService('DEBUG', message, args),
  trace: (message, ...args) => logToService('TRACE', message, args),
}

// Function to send logs to external service
async function logToService(level, message, args) {
  try {
    await fetch('https://logging.service.com/api/logs', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        level,
        message,
        data: args[0] || {},
        timestamp: new Date().toISOString(),
        service: 'content-system',
      }),
    })
  } catch (error) {
    // Fallback to console
    console[level.toLowerCase()](`[LOG SERVICE ERROR] ${message}`, ...args)
  }
}

// Create logging middleware with custom transport
const remoteLogging = withLogging({
  logger: customLogger,
  json: true, // Prepare data for JSON transmission
})

Examples

Basic Logging

typescript
import { withLogging } from '@lib/content/middleware'
import { createContentStore } from '@lib/content'

// Create store with logging
const store = createContentStore({
  adapter: createFileSystemAdapter({ basePath: './content' }),
  middleware: [withLogging({ level: 'info' })],
})

// Reading content will produce logs
await store.read('articles/welcome.md')
// [INFO] READ articles/welcome.md operation=read uri=articles/welcome.md phase=start
// [INFO] READ articles/welcome.md completed in 15ms operation=read uri=articles/welcome.md phase=complete duration=15

Comprehensive Logging

typescript
import { withLogging } from '@lib/content/middleware'
import { pipe } from '@lib/content/middleware'
import { createContentStore, createFileSystemAdapter } from '@lib/content'

// Create store with detailed logging
const store = pipe(
  createContentStore({
    adapter: createFileSystemAdapter({ basePath: './content' }),
  }),
  withLogging({
    level: 'debug',
    prefix: '[CONTENT]',
    logParams: true,
    logResults: true,
    fields: () => ({
      timestamp: new Date().toISOString(),
      pid: process.pid,
      hostname: os.hostname(),
    }),
    formatter: (message, data) => {
      const { timestamp, ...rest } = data
      return `${timestamp} ${message} ${JSON.stringify(rest)}`
    },
  })
)

// Example usage
try {
  await store.read('non-existent.md')
} catch (error) {
  // Error will be logged but still propagated
  console.log('Caught error:', error.message)
}

Released under the MIT License.