All server-side logging goes through Winston. A single root logger is created at module load and shared across the process. Each domain gets a child logger that automatically attaches a domain field to every log entry, making it easy to filter, tail, and route logs by domain in any log aggregation system.
Logging is server-only. Client-side code does not import from $lib/server/.
Every request is assigned a UUID (requestId) generated in handle() and stored in both event.locals.requestId and the request-scoped AsyncLocalStorage context (src/lib/server/context.ts).
A custom Winston format reads from AsyncLocalStorage on every log call and injects requestId automatically. This means every log line emitted anywhere during a request — across domain loggers, infrastructure clients, auth — carries the same requestId with no manual threading required.
Format is selected by NODE_ENV:
NODE_ENV |
Format | Use case |
|---|---|---|
development |
Colorized, human-readable HH:mm:ss LEVEL [domain] (requestId) message {...extra} |
Local dev |
| anything else | Structured JSON with timestamp, level, domain, requestId, message, and any extra fields |
Production / staging |
JSON output is structured so every field is at the top level — no nested meta objects — which makes querying in tools like Loki, Elasticsearch, or CloudWatch straightforward.
Standard Winston levels in descending severity: error, warn, info, http, verbose, debug, silly.
Set LOG_LEVEL env var to control the minimum level emitted (default: info).
Each server module imports its own named logger from $lib/server/logger.ts. The domain field is injected automatically by the child logger — callers never set it manually.
| Logger export | Domain tag | Used in |
|---|---|---|
logger |
(root, no tag) | General / unclassified |
serverLogger |
server |
server.ts, hooks.server.ts |
authLogger |
auth |
auth.ts |
gitLogger |
git |
git/github.ts, git/cache.ts |
k8sLogger |
k8s |
k8s.ts |
Domain-specific loggers for agents, models, tools, knowledge-bases, and gateways are added as those modules are implemented.
import { serverLogger as log } from '$lib/server/logger';
log.info('Server ready');
log.warn('K8s CR not found', { name, namespace });
log.error('Failed to open PR', { err, agentName }); // always pass error as `err` for stack capture
Pass structured fields as the second argument rather than interpolating into the message string. This keeps the message string stable (searchable as a constant) and fields queryable individually.
| Event | Level | Fields |
|---|---|---|
| Server init / shutdown | info |
— |
| Component initialized | info |
component name, key config (no secrets) |
| Login success | info |
username |
| Login failure | warn |
username (never log passwords) |
| Outbound API call | debug |
url, method |
| Outbound API error | error |
url, status, err |
| Unhandled exception | error |
err (stack) |
Never log secrets, passwords, tokens, or full request/response bodies.