#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 an app field 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(server) /warn(browser) - 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, module and context | PrettyFormatterProvider |
cli |
Compact output for CLI sessions: HH:MM:SS L message {json} (no module/context) |
CliFormatterProvider |
json |
Structured JSON, one object per line | JsonFormatterProvider |
raw |
Plain message text, no metadata (best for piping) | RawFormatterProvider |
If LOG_FORMAT is not set:
- Production (non-browser): defaults to
json - Everything else: defaults to
pretty
The alepha and create-alepha CLIs default to cli. Pass --verbose to a
CLI command to switch to pretty at trace level when you need module/context
and the framework's internal logs. An agent session (Claude Code sets the
CLAUDECODE env var) implies --verbose.
#Sub-process output
CLI tasks that shell out (yarn lint, vite build, nested alepha
subcommands, …) only stream their output live when DEBUG or a more verbose
level is enabled - i.e. under --verbose, CLAUDECODE, or LOG_LEVEL=debug.
Below DEBUG (the default), that output is captured instead of streamed, so a
quiet run such as alepha verify is not buried under thousands of sub-process
lines. Captured output is still surfaced (stdout and stderr) if the task
fails, and the Starting … / Finished … after Ns lines always print.
#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});
#Per-request breadcrumbs
Every HTTP request and every job run keeps its own bounded ring of log entries, so that when something throws you can ship the lines that led to it - not just the stack.
1import { $hook, $inject } from "alepha"; 2import { LogBufferProvider } from "alepha/logger"; 3 4class ErrorReporter { 5 logBuffer = $inject(LogBufferProvider); 6 7 onError = $hook({ 8 on: "server:onError", 9 handler: ({ request, error }) => {10 sendToYourTool(error, {11 requestId: request.requestId,12 breadcrumbs: this.logBuffer.snapshot(),13 });14 },15 });16}
snapshot() returns the entries logged so far in the current context, oldest first, or undefined when no buffer is active. Two properties make it useful in production:
- Entries below the active
LOG_LEVELare captured. Running atinfo, thedebugandtracecalls that explain the failure are still in the buffer even though they were never printed. - Values are already redacted. Credential-bearing keys are masked before the entry reaches the buffer, so a snapshot is safe to send off-box.
The ring keeps the last size entries. When it discards older ones, the snapshot opens with a WARN saying how many - a truncated buffer never passes itself off as complete.
Size is controlled by the alepha.logger.buffer atom:
1alepha.store.set("alepha.logger.buffer", { size: 200 }); // default: 50
Set size to 0 to disable capture entirely: no buffer is created and the write path becomes a no-op. Job runs use alepha.jobs.logMaxEntries instead, and persist their breadcrumbs onto the execution row when they fail.
To read the buffer somewhere other than an error hook - inside a handler, a middleware, another hook - inject LogBufferProvider and call snapshot() the same way. Outside any request or job, it returns undefined.
#Correlating with the client
request.requestId is the same value as the context field on every entry the request logged, and it is what the server returns in error responses. An id quoted by a user therefore finds their logs directly:
grep '"context":"<the id they gave you>"' app.log
Put x-request-id (or x-correlation-id) on the request at your proxy and that id is used instead of a generated one, extending the correlation across services.
#Testing
In test mode, Alepha routes logs to MemoryDestinationProvider by default (unless LOG_LEVEL or DEBUG is set, which switches back to console output). 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 { 3 $logger, 4 LogDestinationProvider, 5 MemoryDestinationProvider, 6} from "alepha/logger"; 7 8class App { 9 log = $logger();10}11 12test("should log info message", ({ expect }) => {13 const alepha = Alepha.create({14 env: { LOG_LEVEL: "trace" },15 }).with({16 provide: LogDestinationProvider,17 use: MemoryDestinationProvider,18 });19 20 const output = alepha.inject(MemoryDestinationProvider);21 const app = alepha.inject(App);22 23 app.log.info("Test log message");24 25 expect(output.logs[0].message).toBe("Test log message");26 expect(output.logs[0].level).toBe("INFO");27 expect(output.logs[0].service).toBe("App");28});
#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});