Structured logging via logtape with redaction support for Outfitter
npm install @outfitter/loggingStructured logging via logtape with automatic sensitive data redaction. Provides consistent log formatting across CLI, MCP, and server contexts.
``bash`
bun add @outfitter/logging
`typescript
import {
createLogger,
createConsoleSink,
configureRedaction,
} from "@outfitter/logging";
// Configure global redaction (optional - defaults already cover common sensitive keys)
configureRedaction({
keys: ["apiKey", "accessToken"],
patterns: [/sk-[a-zA-Z0-9]+/g],
});
// Create a logger
const logger = createLogger({
name: "my-service",
level: "debug",
sinks: [createConsoleSink()],
redaction: { enabled: true },
});
// Log with metadata
logger.info("Request received", {
path: "/api/users",
apiKey: "secret-key-123", // Will be redacted to "[REDACTED]"
});
`
| Level | Priority | Use For |
| -------- | -------- | ------------------------------------------ |
| trace | 0 | Very detailed debugging (loops, internals) |debug
| | 1 | Development debugging |info
| | 2 | Normal operations |warn
| | 3 | Unexpected but handled situations |error
| | 4 | Failures requiring attention |fatal
| | 5 | Unrecoverable failures |silent
| | 6 | Disable all logging |
Messages are filtered by minimum level. Setting level: "warn" filters out trace, debug, and info.
`typescript
const logger = createLogger({
name: "app",
level: "warn", // Only warn, error, fatal will be logged
sinks: [createConsoleSink()],
});
logger.debug("Filtered out");
logger.warn("This appears");
`
`typescript`
logger.setLevel("debug"); // Enable debug logging
logger.setLevel("silent"); // Disable all logging
Automatic redaction protects sensitive data from appearing in logs.
These keys are redacted by default (case-insensitive matching):
- passwordsecret
- token
- apikey
-
`typescript`
configureRedaction({
patterns: [
/Bearer [a-zA-Z0-9._-]+/g, // Bearer tokens
/sk-[a-zA-Z0-9]{20,}/g, // OpenAI keys
/ghp_[a-zA-Z0-9]{36}/g, // GitHub PATs
],
keys: ["credentials", "privateKey"],
});
`typescript`
const logger = createLogger({
name: "auth",
redaction: {
enabled: true,
patterns: [/custom-secret-\d+/g],
keys: ["myCustomKey"],
replacement: "*", // Custom replacement (default: "[REDACTED]")
},
});
Redaction is recursive and applies to nested objects:
`typescript`
logger.info("Config loaded", {
database: {
host: "localhost",
password: "super-secret", // Redacted
},
api: {
url: "https://api.example.com",
token: "jwt-token", // Redacted
},
});
// Output: { database: { host: "localhost", password: "[REDACTED]" }, ... }
Create scoped loggers that inherit parent configuration and merge context:
`typescript
const parent = createLogger({
name: "app",
context: { service: "api" },
sinks: [createConsoleSink()],
redaction: { enabled: true },
});
const child = createChildLogger(parent, { handler: "getUser" });
child.info("Processing request");
// Output includes merged context: { service: "api", handler: "getUser" }
`
Child loggers:
- Inherit parent's sinks, level, and redaction config
- Merge context (child overrides parent for conflicting keys)
- Share the same setLevel() and addSink() behavior
Machine-readable output for log aggregation:
`typescript
import { createJsonFormatter } from "@outfitter/logging";
const formatter = createJsonFormatter();
// Output: {"timestamp":1705936800000,"level":"info","category":"app","message":"Hello","userId":"123"}
`
Human-readable output with optional ANSI colors:
`typescript
import { createPrettyFormatter } from "@outfitter/logging";
const formatter = createPrettyFormatter({ colors: true, timestamp: true });
// Output: 2024-01-22T12:00:00.000Z [INFO] app: Hello {"userId":"123"}
`
Routes logs to stdout/stderr based on level:
- trace, debug, info -> stdoutwarn
- , error, fatal -> stderrconsole.*
- Falls back to when process streams are unavailable (edge/serverless)
`typescript
import { createConsoleSink } from "@outfitter/logging";
const logger = createLogger({
name: "app",
sinks: [createConsoleSink()],
});
`
Buffered writes to a file path:
`typescript
import { createFileSink, flush } from "@outfitter/logging";
const logger = createLogger({
name: "app",
sinks: [createFileSink({ path: "/var/log/app.log" })],
});
logger.info("Application started");
// Call flush() before exit to ensure all logs are written
await flush();
`
Implement the Sink interface for custom destinations:
`typescript
import type { Sink, LogRecord, Formatter } from "@outfitter/logging";
const customSink: Sink = {
formatter: createJsonFormatter(), // Optional
write(record: LogRecord, formatted?: string): void {
// Send to your destination
sendToRemote(formatted ?? JSON.stringify(record));
},
async flush(): Promise
// Optional: ensure pending writes complete
await flushPendingWrites();
},
};
`
Logs can be sent to multiple destinations:
`typescript`
const logger = createLogger({
name: "app",
sinks: [
createConsoleSink(),
createFileSink({ path: "/var/log/app.log" }),
customRemoteSink,
],
});
`typescript`
logger.info("User logged in", {
userId: "u123",
email: "user@example.com",
});
Error objects are automatically serialized with name, message, and stack:
`typescript`
try {
await riskyOperation();
} catch (error) {
logger.error("Operation failed", { error });
// error is serialized as: { name: "Error", message: "...", stack: "..." }
}
Logger context is merged with per-call metadata:
`typescript
const logger = createLogger({
name: "api",
context: { requestId: "abc123" },
sinks: [createConsoleSink()],
});
logger.info("Processing", { step: 1 });
// Metadata: { requestId: "abc123", step: 1 }
`
Call flush() before process exit to ensure buffered logs are written:
`typescript
import { flush } from "@outfitter/logging";
process.on("beforeExit", async () => {
await flush();
});
// Or before explicit exit
logger.info("Shutting down");
await flush();
process.exit(0);
`
| Function | Description |
| ----------------------- | --------------------------------------------------- |
| createLogger | Create a configured logger instance |createChildLogger
| | Create a child logger with merged context |configureRedaction
| | Configure global redaction patterns and keys |flush
| | Flush all pending log writes across all sinks |createJsonFormatter
| | Create a JSON formatter for structured output |createPrettyFormatter
| | Create a human-readable formatter with colors |createConsoleSink
| | Create a console sink (stdout/stderr routing) |createFileSink
| | Create a file sink with buffered writes |
| Type | Description |
| ------------------------ | ----------------------------------------------- |
| LogLevel | Union of log level strings |LogRecord
| | Structured log record with timestamp/metadata |LoggerConfig
| | Configuration options for createLogger |LoggerInstance
| | Logger interface with level methods |RedactionConfig
| | Per-logger redaction configuration |GlobalRedactionConfig
| | Global redaction patterns and keys |Formatter
| | Interface for log record formatting |Sink
| | Interface for log output destinations |PrettyFormatterOptions
| | Options for human-readable formatter |FileSinkOptions` | Options for file sink configuration |
|
MIT