@outfitter/logging
v0.1.0-rc.3
Published
Structured logging via logtape with redaction support for Outfitter
Maintainers
Readme
@outfitter/logging
Structured logging via logtape with automatic sensitive data redaction. Provides consistent log formatting across CLI, MCP, and server contexts.
Installation
bun add @outfitter/loggingQuick Start
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]"
});Log Levels
| 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.
const logger = createLogger({
name: "app",
level: "warn", // Only warn, error, fatal will be logged
sinks: [createConsoleSink()],
});
logger.debug("Filtered out");
logger.warn("This appears");Changing Level at Runtime
logger.setLevel("debug"); // Enable debug logging
logger.setLevel("silent"); // Disable all loggingRedaction
Automatic redaction protects sensitive data from appearing in logs.
Default Sensitive Keys
These keys are redacted by default (case-insensitive matching):
passwordsecrettokenapikey
Custom Redaction Patterns
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"],
});Per-Logger Redaction
const logger = createLogger({
name: "auth",
redaction: {
enabled: true,
patterns: [/custom-secret-\d+/g],
keys: ["myCustomKey"],
replacement: "***", // Custom replacement (default: "[REDACTED]")
},
});Nested Object Redaction
Redaction is recursive and applies to nested objects:
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]" }, ... }Child Loggers
Create scoped loggers that inherit parent configuration and merge context:
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()andaddSink()behavior
Formatters
JSON Formatter
Machine-readable output for log aggregation:
import { createJsonFormatter } from "@outfitter/logging";
const formatter = createJsonFormatter();
// Output: {"timestamp":1705936800000,"level":"info","category":"app","message":"Hello","userId":"123"}Pretty Formatter
Human-readable output with optional ANSI colors:
import { createPrettyFormatter } from "@outfitter/logging";
const formatter = createPrettyFormatter({ colors: true, timestamp: true });
// Output: 2024-01-22T12:00:00.000Z [INFO] app: Hello {"userId":"123"}Sinks
Console Sink
Routes logs to stdout/stderr based on level:
trace,debug,info-> stdoutwarn,error,fatal-> stderr
import { createConsoleSink } from "@outfitter/logging";
const logger = createLogger({
name: "app",
sinks: [createConsoleSink()],
});File Sink
Buffered writes to a file path:
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();Custom Sinks
Implement the Sink interface for custom destinations:
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<void> {
// Optional: ensure pending writes complete
await flushPendingWrites();
},
};Multiple Sinks
Logs can be sent to multiple destinations:
const logger = createLogger({
name: "app",
sinks: [
createConsoleSink(),
createFileSink({ path: "/var/log/app.log" }),
customRemoteSink,
],
});Structured Metadata
Basic Metadata
logger.info("User logged in", {
userId: "u123",
email: "[email protected]",
});Error Serialization
Error objects are automatically serialized with name, message, and stack:
try {
await riskyOperation();
} catch (error) {
logger.error("Operation failed", { error });
// error is serialized as: { name: "Error", message: "...", stack: "..." }
}Context Inheritance
Logger context is merged with per-call metadata:
const logger = createLogger({
name: "api",
context: { requestId: "abc123" },
sinks: [createConsoleSink()],
});
logger.info("Processing", { step: 1 });
// Metadata: { requestId: "abc123", step: 1 }Flushing
Call flush() before process exit to ensure buffered logs are written:
import { flush } from "@outfitter/logging";
process.on("beforeExit", async () => {
await flush();
});
// Or before explicit exit
logger.info("Shutting down");
await flush();
process.exit(0);API Reference
Functions
| 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 |
Types
| 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 |
License
MIT
