Skip to content

Logging

Prerequisites: npm install jaypie (includes @jaypie/logger)

Jaypie provides structured logging with request-scoped trace IDs. Logs are JSON-formatted for Datadog ingestion. Follow the log level conventions to maintain clean, actionable logs.

Level Use Case Example
trace Happy path checkpoints Entering function, completing step
debug Unexpected but handled cases Cache miss, retry attempt
info Significant events (rare) Server started, migration complete
warn Concerning but recoverable Deprecation, approaching limit
error Failures requiring attention External service down
fatal Application cannot continue Missing critical config
import { log } from "jaypie";
log.trace("Starting request processing");
log.debug("Cache miss, fetching from database");
log.info("Server started on port 3000");
log.warn("API rate limit at 80%");
log.error("External payment service unavailable");
log.fatal("Missing required MONGODB_URI");

Use log.var() for structured key-value logging:

// Single key-value pair (preferred)
log.var({ userId: "abc-123" });
// Output:
// { "status": "trace", "message": "abc-123", "var": "userId", "data": "abc-123", "dataType": "string" }

Variable logging is 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 - Log one variable at a time:
// Good
log.var({ userId: "123" });
log.var({ orderId: "456" });
// Bad - multiple keys
log.var({ userId: "123", orderId: "456" });
  1. Don’t log large structures - Log summaries instead:
// Good
log.var({ userCount: users.length });
// Bad - large array
log.var({ users: users });
  1. Separate strings from variables:
// Good
log.trace("[processOrder] starting");
log.var({ orderId: order.id });
// Bad - mixing string and variable
log.trace(`Processing order ${order.id}`);

Use for normal execution flow. Should be the only level in successful requests:

async function getUser(id) {
log.trace("[getUser] fetching user");
const user = await db.users.findById(id);
log.trace("[getUser] user found");
return user;
}

Use when something unexpected happens but is handled:

async function getCachedUser(id) {
const cached = cache.get(id);
if (!cached) {
log.debug("[getCachedUser] cache miss");
return await fetchAndCacheUser(id);
}
return cached;
}

Reserve for significant application events:

// Server lifecycle
log.info("Server started");
// Major state changes
log.info("Database migration complete");

Use for recoverable issues that need attention:

if (apiCallsThisHour > limit * 0.8) {
log.warn("Approaching API rate limit");
log.var({ usage: apiCallsThisHour });
}

Use for failures that need investigation:

try {
await externalService.call();
} catch (error) {
log.error("External service call failed");
log.var({ service: "payment" });
throw BadGatewayError();
}

Use when the application must stop:

if (!process.env.MONGODB_URI) {
log.fatal("Missing required MONGODB_URI");
process.exit(1);
}

Tag all subsequent logs with context:

import { log } from "jaypie";
// Tag logs for this request
log.tag({ requestId: "req-abc-123" });
log.tag({ userId: "user-456" });
// All subsequent logs include these tags
log.trace("Processing request");
// Output includes: { requestId: "req-abc-123", userId: "user-456", ... }

Create loggers with additional context:

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

Reset logger state between Lambda invocations:

import { lambdaHandler, log } from "jaypie";
export const handler = lambdaHandler(async (event) => {
// log.init() called automatically by handler
log.trace("Processing event");
});

For manual initialization:

log.init(); // Clears tags, resets state

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 (after setup(), before teardown()); 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. Outside an active session log.tally() silently no-ops, so libraries can tally unconditionally.

log.tally({ llm: { operates: 1, turns: 2 } });
log.tally({ llm: { operates: 1, turns: 3 } });
// => teardown report includes { llm: { operates: 2, turns: 5 } }

Inside a handler, @jaypie/llm tallies an llm key automatically — operate() and stream() report turns, tool calls, and usage by model with no code changes.

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

Oversized entries truncate deliberately with visible markers (… [truncated 612,340 chars]) so payloads fit the log pipeline — see Serialization Limits for details and log.config() runtime overrides.

Use bracketed function names in log messages:

async function processOrder(orderId) {
log.trace("[processOrder] starting");
log.var({ orderId });
const order = await fetchOrder(orderId);
log.trace("[processOrder] order fetched");
await validateOrder(order);
log.trace("[processOrder] order validated");
return order;
}

Use @jaypie/testkit for log assertions:

import { log } from "jaypie";
import { spyLog, restoreLog } from "@jaypie/testkit";
beforeEach(() => {
spyLog(log);
});
afterEach(() => {
restoreLog(log);
});
it("logs at trace level on happy path", async () => {
await processOrder("order-123");
// Verify no logs above trace
expect(log.debug).not.toHaveBeenCalled();
expect(log.warn).not.toHaveBeenCalled();
expect(log.error).not.toHaveBeenCalled();
});
it("logs error on failure", async () => {
await expect(processOrder("invalid")).rejects.toThrow();
expect(log.error).toHaveBeenCalled();
});