4.0 KiB
Logger
Source: src/lib/server/utils/logger/ ($logger/*)
Table of Contents
The logger is a singleton class that writes to two outputs simultaneously:
colored console lines for development and JSON files for persistence. Settings
are stored in SQLite and can be changed at runtime without a restart. Log files
rotate daily (YYYY-MM-DD.log) and are cleaned up by the logsCleanup job
based on a configurable retention window.
Log Levels
Four levels, ordered from least to most severe:
| Level | Color | Purpose |
|---|---|---|
DEBUG |
Cyan | Internal diagnostics, step-by-step detail |
INFO |
Green | Normal operational events worth recording |
WARN |
Yellow | Unexpected but handled situations |
ERROR |
Red | Failures that need attention |
The min_level setting acts as a threshold filter. Setting it to WARN drops
all DEBUG and INFO entries. The default is DEBUG (log everything).
Configuration
Settings live in the log_settings table (singleton row, id = 1):
| Column | Type | Default | Purpose |
|---|---|---|---|
retention_days |
integer | 30 | Days before cleanup deletes |
min_level |
text | DEBUG | Minimum severity to record |
enabled |
integer | 1 | Master toggle for all logging |
file_logging |
integer | 1 | Write to daily JSON files |
console_logging |
integer | 1 | Write to stdout with colors |
LogSettingsManager (settings.ts) caches these values in memory and exposes a
reload() method that the settings UI calls after saving. If the database is
unavailable (e.g. during early startup), the manager falls back to hardcoded
defaults: INFO level, both outputs enabled, 30-day retention.
Output Formats
Console -- human-readable, ANSI-colored:
2026-04-14T12:00:00.000Z | INFO | Sync processing complete | [SyncProcessor] | {"totalSynced":4}
Each segment is pipe-delimited. Timestamps and metadata are grey; the level token is colored per the table above.
File -- one JSON object per line, appended to {logsDir}/YYYY-MM-DD.log:
{
"timestamp": "2026-04-14T12:00:00.000Z",
"level": "INFO",
"message": "Sync processing complete",
"source": "SyncProcessor",
"meta": { "totalSynced": 4 }
}
File writes are async and non-blocking. If a write fails the entry is lost but the application continues normally.
Usage
Import the singleton and call a level method:
import { logger } from '$logger/logger.ts';
await logger.info('Sync processing complete', {
source: 'SyncProcessor',
meta: { totalSynced: 4, instanceCount: 2 }
});
All four level methods (debug, info, warn, error) share the same
signature: (message: string, options?: { source?: string; meta?: unknown }).
They are async -- use await so file writes complete before the caller
continues.
errorWithTrace is a variant that also prints the stack trace to console (in
grey) and writes it to the file log's meta.stack field:
await logger.errorWithTrace('Failed to compile cache', error, {
source: 'PCDManager',
meta: { databaseId: id }
});
Conventions:
sourceis the module or subsystem name (PCDManager,SyncProcessor,ArrSyncJob,NotificationManager). Some modules use sub-tags likeEntitySync:Cooldown.metacarries structured context: IDs, counts, timing, error messages. Keep it serializable -- no class instances or circular references.- For testing, instantiate the
Loggerclass directly with a custom config instead of importing the singleton:
import { Logger } from '$logger/logger.ts';
const testLogger = new Logger({ logsDir: '/tmp/test-logs', minLevel: 'DEBUG' });