Logging Guide
Guren provides a flexible logging system with multiple channels, log levels following RFC 5424, and contextual logging support.
Logging Guide
Guren provides a flexible logging system with multiple channels, log levels following RFC 5424, and contextual logging support.
Core Concepts
- LogManager – Central hub for managing logging channels.
- Logger – Instance for writing log entries with context support.
- LogChannel – Destination for log entries (console, file, etc.).
- LogLevel – Severity level following RFC 5424 (emergency, alert, critical, error, warning, notice, info, debug).
Basic Usage
Quick Start (Facade)
The simplest way to log is through the LogManager, which resolves the LogManager from the container lazily:
// Resolve the logger from the container
const Log = app.container.make('log')
Log.info('Application started')
Log.error('Something went wrong', { error: 'Connection failed' })
Log.channel('daily').warning('Disk space low')
Direct Instantiation
You can also create a LogManager directly:
import { LogManager } from '@guren/core'
const log = new LogManager({
default: 'console',
channels: {
console: { driver: 'console', level: 'debug' },
},
})
log.info('Application started')
log.error('Something went wrong', { error: 'Connection failed' })
Log Levels
Guren supports RFC 5424 severity levels (from most to least severe):
log.emergency('System is unusable')
log.alert('Action must be taken immediately')
log.critical('Critical conditions')
log.error('Error conditions')
log.warning('Warning conditions') // or log.warn()
log.notice('Normal but significant conditions')
log.info('Informational messages')
log.debug('Debug-level messages')
Logging with Context
// Add context to individual log entries
log.info('User logged in', { userId: 123, ip: '192.168.1.1' })
// Create a logger with persistent context
const requestLog = log.withContext({ requestId: 'abc-123' })
requestLog.info('Processing request') // Includes requestId
requestLog.info('Request completed') // Includes requestId
// Create child loggers
const userLog = requestLog.child({ userId: 456 })
userLog.info('User action') // Includes requestId and userId
Channels
Console Channel
Output logs to the console with colors and formatting:
const log = new LogManager({
default: 'console',
channels: {
console: {
driver: 'console',
level: 'debug', // Minimum level to log
colors: true, // Enable ANSI colors
timestamps: true, // Include timestamps
format: 'text', // 'text' or 'json'
},
},
})
File Channel
Write logs to a single file:
const log = new LogManager({
default: 'file',
channels: {
file: {
driver: 'file',
path: './storage/logs/app.log',
level: 'info',
format: 'json', // 'text' or 'json'
},
},
})
Daily File Channel
Rotate log files daily with automatic cleanup:
const log = new LogManager({
default: 'daily',
channels: {
daily: {
driver: 'daily',
path: './storage/logs/app.log',
days: 14, // Keep logs for 14 days
level: 'info',
format: 'json',
},
},
})
Files are named with date suffix: app-2024-01-15.log
Stack Channel
Combine multiple channels:
const log = new LogManager({
default: 'stack',
channels: {
console: { driver: 'console', level: 'debug' },
file: { driver: 'daily', path: './storage/logs/app.log', days: 14 },
stack: {
driver: 'stack',
channels: ['console', 'file'], // Write to both
},
},
})
Using Multiple Channels
Switch Between Channels
// Use default channel
log.info('Using default')
// Use specific channel
log.channel('console').debug('Debug info')
log.channel('file').error('Logged to file only')
Ad-hoc Stack
// Create a temporary stack
log.stack(['console', 'file']).critical('Critical error!')
Configuration
Full Configuration Example
import { LogManager } from '@guren/core'
const log = new LogManager({
default: 'stack',
channels: {
// Console for development
console: {
driver: 'console',
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
colors: true,
timestamps: true,
format: 'text',
},
// Daily rotating file for production
daily: {
driver: 'daily',
path: './storage/logs/app.log',
level: 'info',
days: 30,
format: 'json',
},
// Error-only log
errors: {
driver: 'daily',
path: './storage/logs/errors.log',
level: 'error',
days: 90,
format: 'json',
},
// Stack for default logging
stack: {
driver: 'stack',
channels: ['console', 'daily'],
},
// Critical alerts
critical: {
driver: 'stack',
channels: ['console', 'daily', 'errors'],
},
},
})
Container Integration
The logging subsystem is registered as a singleton via a ServiceProvider. You can resolve it from the container:
// Access via app.container or this.container in providers
const log = container.make('log') // LogManager
log.info('Using container-resolved logger')
Global Logger
Setting Up Global Logger
Note
In most applications you should prefer the LogManager or container.make('log') instead of the global setter. The global functions below are kept for backward compatibility.
import { setLogManager, getLogManager, LogManager } from '@guren/core'
// In your bootstrap file
const log = new LogManager({
default: 'stack',
channels: { /* ... */ },
})
setLogManager(log)
// Anywhere in your application
const log = getLogManager()
log.info('Using global logger')
Custom Channels
Creating a Custom Channel
import type { LogChannel, LogEntry } from '@guren/core'
class SlackChannel implements LogChannel {
constructor(private webhookUrl: string) {}
async log(entry: LogEntry): Promise<void> {
if (entry.level !== 'critical' && entry.level !== 'emergency') {
return // Only send critical/emergency to Slack
}
await fetch(this.webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `[${entry.level.toUpperCase()}] ${entry.message}`,
attachments: [{
fields: Object.entries(entry.context).map(([k, v]) => ({
title: k,
value: String(v),
short: true,
})),
}],
}),
})
}
}
Registering Custom Drivers
const log = new LogManager({
default: 'stack',
channels: {
slack: { driver: 'slack', webhookUrl: process.env.SLACK_WEBHOOK },
stack: { driver: 'stack', channels: ['console', 'slack'] },
},
})
log.registerDriver('slack', (config) => {
return new SlackChannel(config.webhookUrl as string)
})
Request Logging
Middleware Example
import { defineMiddleware } from '@guren/core'
import { getLogManager } from '@guren/core'
export const requestLogging = defineMiddleware(async (c, next) => {
const log = getLogManager()
const requestId = crypto.randomUUID()
const requestLog = log.withContext({
requestId,
method: c.req.method,
path: c.req.path,
})
const start = Date.now()
requestLog.info('Request started')
// Store logger in context for use in handlers
c.set('log', requestLog)
try {
await next()
requestLog.info('Request completed', {
status: c.res.status,
duration: Date.now() - start,
})
} catch (error) {
requestLog.error('Request failed', {
error: error instanceof Error ? error.message : 'Unknown error',
duration: Date.now() - start,
})
throw error
}
})
Testing
Using container.fake()
Swap the log manager in tests to capture log output:
// Access via app.container or this.container in providers
import { LogManager } from '@guren/core'
test('logs error on failure', async () => {
const fakeLog = new LogManager({
default: 'memory',
channels: { memory: { driver: 'memory' } },
})
using _ = container.fake('log', fakeLog)
// Run code under test — all logging via the facade or container
// is captured by fakeLog
})
Custom Memory Channel
import { describe, test, expect, beforeEach, mock } from 'bun:test'
import { LogManager, Logger, LOG_LEVEL_PRIORITY } from '@guren/core'
import type { LogChannel, LogEntry } from '@guren/core'
// Create a memory channel for testing
class MemoryChannel implements LogChannel {
entries: LogEntry[] = []
log(entry: LogEntry): void {
this.entries.push(entry)
}
clear(): void {
this.entries = []
}
}
describe('Logging', () => {
let memoryChannel: MemoryChannel
let log: LogManager
beforeEach(() => {
memoryChannel = new MemoryChannel()
log = new LogManager({
default: 'memory',
channels: {
memory: { driver: 'memory' },
},
})
log.registerDriver('memory', () => memoryChannel)
})
test('logs at different levels', () => {
log.info('Info message')
log.error('Error message')
expect(memoryChannel.entries).toHaveLength(2)
expect(memoryChannel.entries[0].level).toBe('info')
expect(memoryChannel.entries[1].level).toBe('error')
})
test('includes context in log entries', () => {
log.info('User action', { userId: 123 })
expect(memoryChannel.entries[0].context).toEqual({ userId: 123 })
})
test('withContext adds persistent context', () => {
const requestLog = log.withContext({ requestId: 'abc' })
requestLog.info('First')
requestLog.info('Second', { extra: 'data' })
expect(memoryChannel.entries[0].context).toEqual({ requestId: 'abc' })
expect(memoryChannel.entries[1].context).toEqual({ requestId: 'abc', extra: 'data' })
})
})
Log Output Formats
Text Format
[2024-01-15T10:30:00.000Z] INFO User logged in {"userId":123,"ip":"192.168.1.1"}
[2024-01-15T10:30:01.000Z] ERROR Database connection failed {"error":"ECONNREFUSED"}
JSON Format
{"timestamp":"2024-01-15T10:30:00.000Z","level":"info","message":"User logged in","userId":123,"ip":"192.168.1.1"}
{"timestamp":"2024-01-15T10:30:01.000Z","level":"error","message":"Database connection failed","error":"ECONNREFUSED"}
Best Practices
Use appropriate log levels: Don't log everything as error; use debug for development info.
Include context: Always add relevant context (user ID, request ID, etc.) for debugging.
Use structured logging: JSON format is easier to parse and search in production.
Rotate log files: Use the daily driver to prevent disk space issues.
Filter by level in production: Set console to 'info' or higher in production.
Create contextual loggers: Use
withContext()for request-scoped logging.Don't log sensitive data: Never log passwords, tokens, or PII.
Close channels on shutdown: Call
log.close()to flush pending writes.