@jaypie/logger
Prerequisites: npm install jaypie or npm install @jaypie/logger
Overview
Section titled “Overview”@jaypie/logger provides structured JSON logging with request-scoped trace IDs for Datadog integration.
Installation
Section titled “Installation”npm install jaypie# ornpm install @jaypie/loggerQuick Reference
Section titled “Quick Reference”Log Levels
Section titled “Log Levels”| Level | Method | Use Case |
|---|---|---|
| trace | log.trace() |
Happy path checkpoints |
| debug | log.debug() |
Unexpected but handled |
| info | log.info() |
Significant events (rare) |
| warn | log.warn() |
Concerning situations |
| error | log.error() |
Failures |
| fatal | log.fatal() |
Cannot continue |
Additional Methods
Section titled “Additional Methods”| Method | Purpose |
|---|---|
log.var() |
Structured key-value logging |
log.tag() |
Add context to all logs |
log.with() |
Create child logger |
log.init() |
Reset logger state |
log.setup() |
Start a report session |
log.report() |
Accumulate session report data |
log.sessionActive |
Whether a report session is open |
log.tally() |
Accumulate combining report data (numbers sum) |
log.teardown() |
Emit session report, end session |
Basic Usage
Section titled “Basic Usage”import { log } from "jaypie";
log.trace("Starting request");log.debug("Cache miss");log.info("Server started");log.warn("Approaching limit");log.error("Service failed");log.fatal("Missing config");Variable Logging
Section titled “Variable Logging”Log structured key-value pairs:
log.var({ userId: "123" });// Output: { log: "trace", message: "123", var: "userId", data: "123" }Available at all levels:
log.trace.var({ requestId: "req-123" });log.debug.var({ cacheHit: false });log.error.var({ errorCode: "E001" });- Single key per call:
// Goodlog.var({ userId: "123" });log.var({ orderId: "456" });
// Badlog.var({ userId: "123", orderId: "456" });- Log summaries, not large objects:
// Goodlog.var({ itemCount: items.length });
// Badlog.var({ items: items });Request Tagging
Section titled “Request Tagging”Add context to all subsequent logs:
log.tag({ requestId: "req-abc-123" });log.tag({ userId: "user-456" });
log.trace("Processing");// Includes: { requestId: "req-abc-123", userId: "user-456", ... }Child Loggers
Section titled “Child Loggers”Create loggers with additional context:
const userLog = log.with({ userId: user.id });
userLog.trace("Action started");userLog.trace("Action completed");// Both include userIdLogger Initialization
Section titled “Logger Initialization”Reset state between Lambda invocations:
log.init();// Clears tags, resets trace IDHandlers call log.init() automatically.
Session Management
Section titled “Session Management”Handlers automatically call log.setup() and log.teardown() to bookend each request. On teardown, a report is emitted as log.info.var({ report }) containing accumulated data and warn/error counts.
// Manual session (handlers do this automatically)log.setup({ handler: "myHandler", invoke: "abc-123" });
// Accumulate report data during the requestlog.report({ userId: "456" });log.report({ itemCount: 3 });
// Teardown emits the report with warn/error statslog.teardown();// => { report: { userId: "456", itemCount: 3, log: { warn: false, warns: 0, error: false, errors: 0 } } }log.report() only accumulates data while a session is active; calling it outside a session logs a warning and is a no-op. Warn and error calls made during the session are counted automatically and included in the final report.
log.report() records values once. Plain objects merge recursively, so separate calls can fill sibling keys under one parent (report({ agent: { a: {...} } }) then report({ agent: { b: {...} } }) keeps both). Arrays and scalars are leaves: the last write wins, and a changed leaf logs [logger] Overwriting report key: <dotted.path> at debug. Use log.tally() for data that accumulates — keys combine instead: numbers sum, strings collect into an array of strings, booleans AND, and objects merge recursively.
log.tally({ llm: { operates: 1, turns: 2 } });log.tally({ llm: { operates: 1, turns: 3 } });// => teardown report includes { llm: { operates: 2, turns: 5 } }Outside an active session log.tally() silently no-ops, so libraries can tally unconditionally. @jaypie/llm uses this to report an llm key (turns, tool calls, usage by model) automatically.
log.sessionActive reports whether a session is open, so a library can skip assembling a payload nothing will collect:
if (log.sessionActive !== false) { log.tally({ llm: { operates: 1, turns: 2 } });}Compare against false rather than testing truthiness: an older logger without the accessor reports undefined, and the tally should still be attempted there.
Function Prefix Convention
Section titled “Function Prefix Convention”Use bracketed function names:
async function processOrder(orderId) { log.trace("[processOrder] starting"); log.var({ orderId });
await fetchOrder(orderId); log.trace("[processOrder] order fetched");
return order;}Redaction
Section titled “Redaction”Every logged value is scrubbed before serialization to prevent credential leaks. Three layers apply, in order:
- Name denylist. Field names match case-insensitively, ignoring
_/-/spaces. Credential-shaped names (password,apiKey,authorization,clientSecret, …) render assk_<last4>ormd5_<last4>; bank identifiers (accountNumber,routingNumber,iban,ssn,taxId) render as…<last4>; card numbers (cardNumber,pan, …) render as<bin>…<last4>;cvv/pin/otp/securityCoderender fully asredacted. Values too short for a partial reveal render fully asredacted. - Value heuristics. Strings matching known credential shapes (
sk-…,AKIA…,ghp_…,xox?-…,whsec_…, JWTs) redact regardless of field name. Ambiguous names (key,token,auth,credential) redact only when the value looks like a secret (16-512 characters, no whitespace, not a path, not a UUID). secret()brand. Wrap a value withsecret()to force redaction wherever it is logged, independent of name or shape:
import { log, secret, isSecret } from "jaypie";
log.var({ apiKey: secret(rawKey) }); // always redacted in logsJSON.stringify({ apiKey: secret(rawKey) }); // unredacted — secret() only affects loggingisSecret(rawKey); // false; isSecret(secret(rawKey)) // trueHandles nested objects and arrays (not just top-level keys), and never mutates the original object.
Disable redaction entirely with LOG_REDACT=false, or add field names to the denylist with LOG_REDACT_KEYS (comma-separated). Both are also configurable at runtime:
log.config({ redactKeys: ["internalToken"] });log.config({ redact: false }); // disable, including secret() handlingConfiguration
Section titled “Configuration”| Variable | Values | Default |
|---|---|---|
LOG_LEVEL |
trace, debug, info, warn, error, fatal | info |
LOG_FORMAT |
json, text | json |
LOG_MAX_ENTRY_BYTES |
bytes; false disables |
262144 (256KB) |
LOG_MAX_STRING |
characters; false disables |
off |
LOG_MAX_DEPTH |
levels; false disables |
off |
LOG_REDACT |
false/0/none/off disables |
on |
LOG_REDACT_KEYS |
comma-separated field names added to the denylist | - |
LOG_LEVEL=debug npm startSerialization Limits
Section titled “Serialization Limits”Entries are capped at 256KB by default — the CloudWatch Logs event limit that fronts Datadog in Lambda. Oversized entries truncate the top-level attributes of data largest-first, each keeping a 72-character preview plus a visible marker:
data:application/pdf;base64,JVBERi0xLjcK… [truncated 612,340 chars]Two more limits are available, off by default: maxStringLength (truncate each string field beyond N characters) and maxDepth (replace objects nested beyond N levels with [Object] / [Array(n)]).
Override at runtime with log.config() — it propagates to derived loggers and persists across init():
import { log } from "jaypie";
log.config({ maxStringLength: 1024 });log.config({ maxEntryBytes: false }); // false disables a limitLimits apply at serialization time only; the logged object is never mutated.
JSON Output
Section titled “JSON Output”log.info("Server started");Outputs:
{ "status": "info", "message": "Server started", "invoke": "abc-123", "timestamp": "2024-01-15T10:30:00.000Z"}Level Guidelines
Section titled “Level Guidelines”Normal execution flow:
async function getUser(id) { log.trace("[getUser] fetching"); const user = await db.findById(id); log.trace("[getUser] found"); return user;}Unexpected but handled:
const cached = cache.get(id);if (!cached) { log.debug("[getUser] cache miss"); return await fetchUser(id);}Significant events (rare):
log.info("Server started");log.info("Migration complete");Concerning situations:
if (usage > limit * 0.8) { log.warn("Approaching rate limit"); log.var({ usage });}Failures:
try { await externalApi.call();} catch (error) { log.error("External API failed"); log.var({ error: error.message }); throw BadGatewayError();}Cannot continue:
if (!process.env.MONGODB_URI) { log.fatal("Missing MONGODB_URI"); process.exit(1);}Datadog Log Forwarding
Section titled “Datadog Log Forwarding”For local dev, eval runs, and CI where CloudWatch subscription filters aren’t available, enable direct HTTP forwarding to Datadog’s Logs API:
DATADOG_LOCAL_FORWARDING=true DATADOG_API_KEY=your-key npm run devZero code changes — the logger auto-detects the env vars and ships logs alongside normal console output.
| Variable | Description | Default |
|---|---|---|
DATADOG_LOCAL_FORWARDING |
Enable forwarding | false |
DATADOG_API_KEY |
Datadog API key (required) | — |
DD_SITE |
Datadog intake site | datadoghq.com |
DD_SERVICE / PROJECT_SERVICE |
Service tag | unknown |
DD_ENV / PROJECT_ENV |
Environment tag | local |
DD_HOST / PROJECT_HOST |
Hostname tag | os.hostname() |
import { isDatadogForwardingEnabled } from "@jaypie/logger";
if (isDatadogForwardingEnabled()) { // Transport is active}Testing
Section titled “Testing”import { log } from "jaypie";import { spyLog, restoreLog } from "@jaypie/testkit";
beforeEach(() => { spyLog(log);});
afterEach(() => { restoreLog(log);});
it("logs at trace only", async () => { await successfulOperation(); expect(log.trace).toHaveBeenCalled(); expect(log.debug).not.toHaveBeenCalled();});Related
Section titled “Related”- Logging - Best practices
- Testing - Testing logs
- Handler Lifecycle - Automatic initialization