playbook/outfitter-agents/plugins/outfitter-stack/skills/stack-patterns/references/logging.md

5.6 KiB

Logging Patterns

Deep dive into @outfitter/logging patterns.

Creating a Logger

import { createLogger, createConsoleSink } from "@outfitter/logging";

const logger = createLogger({
  name: "my-app",
  level: "info",
  sinks: [createConsoleSink()],
  redaction: { enabled: true },
});

Log Levels

Level Method Use For
trace logger.trace() Very detailed debugging
debug logger.debug() Development debugging
info logger.info() Normal operations
warn logger.warn() Unexpected but handled
error logger.error() Failures requiring attention
fatal logger.fatal() Unrecoverable failures

Level hierarchy: trace < debug < info < warn < error < fatal

Setting level to info hides trace and debug.

Structured Logging

Always use metadata objects:

// GOOD: Structured metadata
logger.info("User created", {
  userId: user.id,
  email: user.email,
  duration: performance.now() - start,
});

// BAD: String concatenation
logger.info("User " + user.name + " created in " + duration + "ms");

Child Loggers

Add context that persists across calls:

import { createChildLogger } from "@outfitter/logging";

const requestLogger = createChildLogger(logger, {
  requestId: ctx.requestId,
  handler: "createUser",
});

// All logs include requestId and handler
requestLogger.info("Processing");           // Has requestId, handler
requestLogger.debug("Validated input");     // Has requestId, handler
requestLogger.info("User created", { userId }); // Has requestId, handler, userId

Redaction

Enable Redaction

const logger = createLogger({
  name: "my-app",
  level: "info",
  sinks: [createConsoleSink()],
  redaction: { enabled: true },
});

logger.info("Config", {
  apiKey: "secret-123",     // Logged as "[REDACTED]"
  password: "hunter2",      // Logged as "[REDACTED]"
  email: "user@example.com" // Not redacted
});

Default Redaction Patterns

Automatically redacted:

  • password, pwd
  • apiKey, api_key
  • secret, secretKey
  • token, accessToken
  • auth, authorization
  • key (when containing sensitive data)
  • credential, credentials

Custom Patterns

const logger = createLogger({
  name: "my-app",
  redaction: {
    enabled: true,
    patterns: [
      "password",
      "apiKey",
      "myCustomSecret",
      "internalToken",
    ],
  },
});

Deep Redaction

Nested values are also redacted:

logger.info("Request", {
  headers: {
    authorization: "Bearer token",  // Redacted
  },
  body: {
    user: {
      password: "secret",  // Redacted
    },
  },
});

Sinks

Console Sink

import { createConsoleSink } from "@outfitter/logging";

const consoleSink = createConsoleSink({
  colorize: true,           // ANSI colors
  prettyPrint: true,        // Formatted output
  timestampFormat: "iso",   // ISO 8601 timestamps
});

File Sink

import { createFileSink } from "@outfitter/logging";

const fileSink = createFileSink({
  path: "/var/log/myapp/app.log",
  maxSize: 10 * 1024 * 1024,  // 10MB
  maxFiles: 5,                 // Keep 5 rotated files
});

Multiple Sinks

const logger = createLogger({
  name: "my-app",
  level: "debug",
  sinks: [
    createConsoleSink({ level: "info" }),     // Console: info+
    createFileSink({                           // File: debug+
      path: "/var/log/myapp/debug.log",
      level: "debug",
    }),
  ],
});

Custom Sink

const customSink = {
  log: (record) => {
    // Send to external service
    externalService.send({
      level: record.level,
      message: record.message,
      metadata: record.metadata,
      timestamp: record.timestamp,
    });
  },
};

const logger = createLogger({
  name: "my-app",
  sinks: [customSink],
});

Environment Configuration

const logger = createLogger({
  name: "my-app",
  level: process.env.LOG_LEVEL || "info",
  sinks: [
    createConsoleSink({
      colorize: process.stdout.isTTY,
      prettyPrint: process.env.NODE_ENV !== "production",
    }),
  ],
});

Handler Context Integration

import { createContext } from "@outfitter/contracts";
import { createLogger, createChildLogger } from "@outfitter/logging";

const baseLogger = createLogger({ name: "my-app", level: "info" });

export function createHandlerContext() {
  const ctx = createContext({ logger: baseLogger });

  // Child logger with requestId
  return {
    ...ctx,
    logger: createChildLogger(baseLogger, { requestId: ctx.requestId }),
  };
}

// In handler
const myHandler: Handler<Input, Output, Error> = async (input, ctx) => {
  ctx.logger.info("Processing", { input });  // Includes requestId
  // ...
};

Performance

Conditional Logging

// Level check before expensive operations
if (logger.isEnabled("debug")) {
  const expensiveData = computeDebugInfo();
  logger.debug("Debug info", { data: expensiveData });
}

Lazy Evaluation

logger.debug("State", () => ({
  // Only computed if debug level is enabled
  memory: process.memoryUsage(),
  connections: getActiveConnections(),
}));

Best Practices

  1. Structured metadata - Always use objects, not string concatenation
  2. Child loggers - Add request context that persists
  3. Enable redaction - Prevent secrets from leaking
  4. Level per environment - Debug in dev, info in prod
  5. Request IDs - Include for tracing across handlers
  6. Lazy evaluation - Avoid expensive computations at disabled levels