#Logging
Alepha provides structured logging via the $logger primitive from alepha/logger.
#Basic Usage
1import { $logger } from "alepha/logger"; 2 3class UserService { 4 log = $logger(); 5 6 async createUser(name: string) { 7 this.log.info("Creating user", { name }); 8 // prints: [23:45:53.326] INFO <app.UserService>: Creating user {"name":"alice"} 9 }10}
$logger() returns a Logger instance. The logger name defaults to the class name.
The module defaults to "app" (or the module name if the service belongs to a $module).
You can override the name:
1class App {2 log = $logger({ name: "Bootstrap" });3}
You can add "appName" to all log entries by setting the APP_NAME environment variable:
APP_NAME=my-app
This is useful for identifying logs from different applications in a shared logging system.
#Log Levels
The LoggerInterface exposes five methods:
1export interface LoggerInterface {2 trace(message: string, data?: unknown): void;3 debug(message: string, data?: unknown): void;4 info(message: string, data?: unknown): void;5 warn(message: string, data?: unknown): void;6 error(message: string, data?: unknown): void;7}
Severity order from lowest to highest: TRACE, DEBUG, INFO, WARN, ERROR, SILENT.
A log call is only written to the destination if its level is at or above the configured threshold. SILENT suppresses all output.
#Configuration
#LOG_LEVEL
Set via the LOG_LEVEL environment variable. Case-insensitive.
# Global level
LOG_LEVEL=debug
# Per-module level with global fallback
LOG_LEVEL=alepha.core:trace,info
# Multiple module overrides
LOG_LEVEL=alepha.core:trace,alepha.server:debug,my.app:error,info
The syntax is module_prefix:level pairs separated by commas or semicolons, with an optional global level at the end. Module matching uses prefix matching: alepha matches alepha.core, alepha.server, etc.
Wildcard patterns are supported:
LOG_LEVEL=alepha.*:debug,*.test:silent,info
Defaults by environment:
- dev:
info - prod:
info - test:
trace(but logs go to memory, only printed on test failure)
#LOG_FORMAT
Set via the LOG_FORMAT environment variable.
| Value | Description | Provider |
|---|---|---|
pretty |
Colored, human-readable output with timestamps | PrettyFormatterProvider |
json |
Structured JSON, one object per line | JsonFormatterProvider |
raw |
Plain message text, no metadata | RawFormatterProvider |
If LOG_FORMAT is not set:
- Production (non-browser): defaults to
json - Everything else: defaults to
pretty
#Log Entry Structure
Every log call produces a LogEntry:
1interface LogEntry { 2 level: "SILENT" | "TRACE" | "DEBUG" | "INFO" | "WARN" | "ERROR"; 3 message: string; 4 service: string; // class name, e.g. "UserService" 5 module: string; // module name, e.g. "app" or "my.project.users" 6 context?: string; // request-scoped correlation ID (from AsyncLocalStorage) 7 app?: string; // APP_NAME env variable 8 data?: unknown; // arbitrary payload or Error object 9 timestamp: number; // milliseconds since epoch10}
#Log Events
Every log call emits a "log" event on the Alepha event system, regardless of whether the message was above the configured threshold. This allows external listeners to capture all log activity:
1alepha.events.on("log", (event) => {2 // event.message - formatted string (or undefined if below threshold)3 // event.entry - the raw LogEntry4});
#Testing
In test mode, Alepha routes logs to MemoryDestinationProvider by default. Logs are buffered in memory and only printed to the console if a test fails.
To capture and assert on logs in tests:
1import { Alepha } from "alepha"; 2import { $logger, LogDestinationProvider, MemoryDestinationProvider } from "alepha/logger"; 3 4class App { 5 log = $logger(); 6} 7 8test("should log info message", ({ expect }) => { 9 const alepha = Alepha.create({10 env: { LOG_LEVEL: "trace" },11 }).with({12 provide: LogDestinationProvider,13 use: MemoryDestinationProvider,14 });15 16 const output = alepha.inject(MemoryDestinationProvider);17 const app = alepha.inject(App);18 19 app.log.info("Test log message");20 21 expect(output.logs[0].message).toBe("Test log message");22 expect(output.logs[0].level).toBe("INFO");23 expect(output.logs[0].service).toBe("App");24});
#Custom Destination
Replace the log destination by substituting LogDestinationProvider:
1import { LogDestinationProvider } from "alepha/logger"; 2import type { LogEntry } from "alepha/logger"; 3 4class MyDestination extends LogDestinationProvider { 5 write(message: string, entry: LogEntry): void { 6 // send to external service, write to file, etc. 7 } 8} 9 10const alepha = Alepha.create().with({11 provide: LogDestinationProvider,12 use: MyDestination,13});
#Custom Formatter
Replace the log formatter by substituting LogFormatterProvider:
1import { LogFormatterProvider } from "alepha/logger"; 2import type { LogEntry } from "alepha/logger"; 3 4class MyFormatter extends LogFormatterProvider { 5 format(entry: LogEntry): string { 6 return `[${entry.level}] ${entry.module}.${entry.service}: ${entry.message}`; 7 } 8} 9 10const alepha = Alepha.create().with({11 provide: LogFormatterProvider,12 use: MyFormatter,13});