AgentSkillsCN

outfitter-logging

深入探讨 @outfitter/logging 的各项模式,包括结构化日志、日志接收器、脱敏处理以及子日志记录器。适用于配置日志记录、添加脱敏处理、创建日志接收器,或当提及“日志记录器”、“结构化日志”、“脱敏”、“日志级别”或“@outfitter/logging”时使用。

SKILL.md
--- frontmatter
name: outfitter-logging
version: 0.1.0
description: Patterns for @outfitter/logging including structured logging, sinks, redaction, and child loggers. Use when configuring logging, adding redaction, creating sinks, or when "logger", "structured logging", "redaction", "log level", or "@outfitter/logging" are mentioned.
allowed-tools: Read Write Edit Glob Grep

Logging Patterns

Deep dive into @outfitter/logging patterns.

Creating a Logger

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

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

Log Levels

LevelMethodUse For
tracelogger.trace()Very detailed debugging
debuglogger.debug()Development debugging
infologger.info()Normal operations
warnlogger.warn()Unexpected but handled
errorlogger.error()Failures requiring attention
fatallogger.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:

typescript
// 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:

typescript
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

typescript
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

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

Deep Redaction

Nested values are also redacted:

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

Sinks

Console Sink

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

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

File Sink

typescript
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

typescript
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

typescript
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

typescript
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

typescript
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

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

Lazy Evaluation

typescript
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

Related Skills

  • stack:patterns — Context creation
  • stack:outfitter-daemon — Daemon logging
  • stack:debug — Troubleshooting logging