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.
import { withLogging } from '@lib/content/middleware'
// Create logging middleware
const loggingMiddleware = withLogging({
level: 'info',
prefix: 'CONTENT',
})
API Reference
withLogging
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
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
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:
- Log Operation Start: Record the beginning of an operation
- Record Start Time: Track when the operation began
- Execute Operation: Allow the operation to proceed
- Log Operation Result: Record success or failure
- Calculate Duration: Measure operation execution time
// 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:
// 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:
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
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
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
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
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
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)
}