@akms/logger
v0.2.1
Published
Winston-based logger and console helper with environment-aware configuration, sensitive data masking, daily log rotation, and call-site preserving console bindings.
Readme
@akms/logger
🔒 Internal use only — built for our organization's services.
Winston-based logger for Node.js services with a winston-free browser counterpart. The same $logger import works in SSR loaders and client components; the bundler picks the right entry automatically. On the browser side $logger is backed by a getter + native console bind, so DevTools "source" links point to the caller line. Environment-aware log level, sensitive data masking, and hourly log rotation.
📦 Installation
npm install @akms/logger🚀 Usage
import { $logger, createLogger } from "@akms/logger";
$logger.info("server started", { port: 3000 });
const logger = createLogger();
logger.debug("debug info", { context: "MyService" });
const scoped = $logger.child({ requestId: "req-123" });
scoped.error("payment failed", { orderId: 42 });On Node, child(bindings) adds metadata fields to every record via winston's binding mechanism. On the browser, the bindings object is passed as a separate argument to console.* (not stringified), so DevTools renders it as an expandable object you can inspect inline.
🧵 Request-scoped logging
Request scoping is the application's concern, not the logger's — own the AsyncLocalStorage in your server and put a bound child logger inside it. The logger stays generic; child(bindings) merges those fields into every record (call-time fields win on key collision), and you reach the bound logger from any depth without threading it through the call stack.
import { AsyncLocalStorage } from "node:async_hooks";
import { $logger, type Logger } from "@akms/logger";
import { randomUUID } from "node:crypto";
const als = new AsyncLocalStorage<Logger>();
export const reqLogger = (): Logger => als.getStore() ?? $logger;
// at the request entry point (e.g. express middleware)
function requestContext(req, res, next) {
const logger = $logger.child({
req_id: randomUUID(),
ip: req.ip,
method: req.method,
url: req.originalUrl,
});
als.run(logger, () => next());
}
// later, after JWT verification — re-bind with the enriched context
function onAuth(payload) {
const enriched = reqLogger().child({ member_id: payload.sub, session_id: payload.sid });
als.enterWith(enriched);
}
// anywhere downstream in the same async chain
reqLogger().info("payment processed", { orderId: 42 });
// → metadata includes req_id, ip, method, url, member_id, session_id, orderId🔒 Don't put raw tokens in the bindings (security) — attach a
session_idor a token prefix/hash instead. Avoid thecontextkey too; it collides with the child-logger label (e.g."ServiceName.methodName").
🌐 Environments
The package ships two entries, picked automatically by exports conditions:
| Condition | Entry | Contents |
|-----------|-------|----------|
| browser / workerd / edge-light | lib/browser.js | Call-site-preserving console adapter — no winston, no Node built-ins. |
| import / require | lib/index.js | Full winston logger (file transport, masking, colorized output). |
So a single import { $logger } from "@akms/logger" resolves to winston on the SSR server (React Router loader/action, Next.js route handler, 'use server' module) and to the call-site-preserving console wrapper in the client chunk. A build-time guard (scripts/verify-browser-bundle.mjs) fails the build if winston or any Node built-in leaks into the browser bundle.
The browser entry exposes the same Logger shape (fatal/error/warn/info/debug/verbose/child) so consumer code does not need to branch on environment.
⚠️ Do not add
@akms/loggerto Next.jstranspilePackagesor Vite'sssr.noExternal. Doing so can drag the Node entry into client chunks and bypass thebrowsercondition.
⚙️ Environment Variables
Only the Node entry reads these. In the browser entry they are ignored.
| Variable | Description | Default |
|----------|-------------|---------|
| LOG_LEVEL | fatal / error / warn / info / debug / verbose (trace alias) | info (prod), debug (dev) |
| LOG_SENSITIVE_KEYS | Comma-separated keys to mask recursively (e.g., password,token) | - |
| LOG_USE_SINGLE_LINE_OBJ | Single-line object output | false |
| LOG_USE_FILE | Write to file with hourly rotation (gzip). Recommended true in prod/testprod. | false |
| LOG_DIR | Directory log files are written to. Empty → <cwd>/logs. In prod, prefer an OS-standard or absolute path outside the PM2 cwd (e.g. /var/log/<org>/<service>). | <cwd>/logs |
| LOG_FILE_PREFIX | Filename prefix to avoid collisions when several services/envs share one directory. Usually <service>_<MODE>. Attached to the filename (not the folder), so a copied file is self-identifying. E.g. auth_api_development → 2026_06/0601/auth_api_development_2026_0601_15.log. Empty → no prefix. | - |
| LOG_FILE_MAX_SIZE | Max size per file; on overflow, splits within the same hour as .1, .2. E.g. 20m, 1g. Empty → unlimited. | unlimited |
| LOG_FILE_MAX_DAYS | Retention. 30d (days) or 30 (file count). Empty → unlimited (never auto-deletes). Days recommended — count-based retention (30 files = 30 hours under hourly rotation) may already have purged yesterday's logs during an incident. | unlimited |
| MODE | development / production / testprod | - |
Both process.env (Node.js) and import.meta.env (Vite) are supported. process.env takes priority when defined, so an operator-set value wins on Node; Vite client builds (where process is undefined) fall back to import.meta.env.
🔧 Exports
| Name | Purpose |
|------|---------|
| $logger, createLogger | Logger — singleton / factory. Winston on Node, call-site-preserving console adapter on the browser. |
| getLoggerOptions | Inspect resolved winston config (Node entry only — no-op in the browser entry). |
| Logger, LoggerOptions | Type exports. Logger is environment-agnostic; LoggerOptions is winston-specific on Node and a no-op Record on the browser. |
🛝 Playground
A React Router 7 SSR sandbox lives in playground/ for verifying the dual-entry split end-to-end. The same $logger import resolves to winston in the SSR loader (terminal output) and to the call-site-preserving console adapter in the client chunk (DevTools).
npm run build # populate lib/ (or `npm run watch` for HMR alongside)
npm run playground # http://localhost:3000📒 Changelog
See CHANGELOG.md.
