Skip to content

@jaypie/logger

Prerequisites: npm install jaypie or npm install @jaypie/logger

@jaypie/logger provides structured JSON logging with request-scoped trace IDs for Datadog integration.

Terminal window
npm install jaypie
# or
npm install @jaypie/logger
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
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.tally() Accumulate combining report data (numbers sum)
log.teardown() Emit session report, end session
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");

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" });
  1. Single key per call:
// Good
log.var({ userId: "123" });
log.var({ orderId: "456" });
// Bad
log.var({ userId: "123", orderId: "456" });
  1. Log summaries, not large objects:
// Good
log.var({ itemCount: items.length });
// Bad
log.var({ items: items });

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", ... }

Create loggers with additional context:

const userLog = log.with({ userId: user.id });
userLog.trace("Action started");
userLog.trace("Action completed");
// Both include userId

Reset state between Lambda invocations:

log.init();
// Clears tags, resets trace ID

Handlers call log.init() automatically.

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 request
log.report({ userId: "456" });
log.report({ itemCount: 3 });
// Teardown emits the report with warn/error stats
log.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() warns when a key is written twice. Use log.tally() for data written repeatedly — 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.

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;
}

Authorization headers are automatically redacted before logging to prevent credential leaks:

  • sk-* tokens → sk_<last 4 chars>
  • Other values → md5_<last 4 chars of hash>

Handles top-level authorization keys and nested headers.authorization. Original objects are never mutated.

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
Terminal window
LOG_LEVEL=debug npm start

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 limit

Limits apply at serialization time only; the logged object is never mutated.

log.info("Server started");

Outputs:

{
"status": "info",
"message": "Server started",
"invoke": "abc-123",
"timestamp": "2024-01-15T10:30:00.000Z"
}

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);
}

For local dev, eval runs, and CI where CloudWatch subscription filters aren’t available, enable direct HTTP forwarding to Datadog’s Logs API:

Terminal window
DATADOG_LOCAL_FORWARDING=true DATADOG_API_KEY=your-key npm run dev

Zero 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
}
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();
});