10 KiB
ccStructLog
A modern, structured logging library for CC:Tweaked, inspired by Python's structlog. This library provides a flexible, extensible logging framework based on processors, renderers, and streams.
Features
- Structured Logging: Log events are represented as key-value pairs, not just strings.
- Extensible: Easy to customize with processors, renderers, and streams.
- Type Safe: Full TypeScript support with proper type definitions.
- CC:Tweaked Optimized: Designed specifically for Minecraft's ComputerCraft environment, with features like file rotation and colored console output.
Quick Start
The easiest way to get started is to create a Logger instance and configure it with processors, a renderer, and streams.
Here's a simple example of a logger that prints colored, human-readable messages to the console:
import {
Logger,
LogLevel,
processor,
textRenderer,
ConsoleStream,
} from "@/lib/ccStructLog";
// Create a logger
const logger = new Logger({
processors: [
processor.addTimestamp({ format: "%T" }), // Add HH:MM:SS timestamp
processor.filterByLevel(LogLevel.Info), // Log Info and higher
processor.addSource("MyApp"),
],
renderer: textRenderer,
streams: [new ConsoleStream()],
});
// Log messages with context
logger.info("Server started", { port: 8080, version: "1.0.0" });
logger.warn("Low disk space", { available: 1024, threshold: 2048 });
logger.error("Connection failed", { host: "example.com", retries: 3 });
// This debug message will be filtered out by `filterByLevel`
logger.debug("This is a debug message.");
Core Concepts
Log Levels
export enum LogLevel {
Trace = 0, // Very detailed diagnostic information
Debug = 1, // Diagnostic information for development
Info = 2, // General informational messages
Warn = 3, // Potentially harmful situations
Error = 4, // Error events that might allow continued execution
Fatal = 5, // Very severe errors that might cause termination
}
Data Flow
- Capture: User calls
logger.info("message", {key: "value"}). - Package: A
LogEventobject (Map<string, unknown>) is created with the message, context, and metadata. - Process: The event is passed through a chain of processors (e.g., to add a timestamp, filter by level).
- Render: The final event is converted to a string by a renderer (e.g.,
textRenderer,jsonRenderer). - Output: The string is sent to one or more streams (e.g., console, file).
Common Configurations
Development Logger
A typical development logger is configured for human-readable console output with timestamps and colors.
import {
Logger,
processor,
textRenderer,
ConsoleStream,
} from "@/lib/ccStructLog";
const devLogger = new Logger({
processors: [
processor.addTimestamp({ format: "%F %T" }), // YYYY-MM-DD HH:MM:SS
processor.addSource("DevApp"),
processor.addComputerId(),
],
renderer: textRenderer,
streams: [new ConsoleStream()],
});
devLogger.debug("This is a debug message.", { user: "dev" });
Production Logger
A production logger is often configured to write machine-readable JSON logs to a file with daily rotation.
import {
Logger,
LogLevel,
processor,
jsonRenderer,
FileStream,
DAY,
} from "@/lib/ccStructLog";
const prodLogger = new Logger({
processors: [
processor.addTimestamp(), // Default format is %F %T
processor.filterByLevel(LogLevel.Info),
processor.addSource("ProdApp"),
processor.addComputerId(),
],
renderer: jsonRenderer,
streams: [
new FileStream("app.log", DAY), // Rotate daily
],
});
prodLogger.info("Application is running in production.");
Custom Configuration
You can create a logger with any combination of processors, renderers, and streams.
import {
Logger,
processor,
jsonRenderer,
FileStream,
ConsoleStream,
HOUR,
} from "@/lib/ccStructLog";
const logger = new Logger({
processors: [
processor.addTimestamp(),
processor.addComputerId(),
processor.addSource("MyApplication"),
],
renderer: jsonRenderer,
streams: [
new ConsoleStream(),
new FileStream("custom.log", HOUR), // Rotate every hour
],
});
logger.info("Custom logger reporting for duty.", { user: "admin" });
Processors
Processors are functions that modify, enrich, or filter log events before they are rendered. They are all available under the processor namespace.
Built-in Processors
import { Logger, LogLevel, processor } from "@/lib/ccStructLog";
// Usage example
const logger = new Logger({
processors: [
// Adds a timestamp. Format is compatible with os.date().
// Default: "%F %T" (e.g., "2023-10-27 15:30:00")
processor.addTimestamp({ format: "%T" }), // e.g., "15:30:00"
// Filter by minimum level
processor.filterByLevel(LogLevel.Warn), // Only allow Warn, Error, Fatal
// Filter based on a custom predicate
processor.filterBy((event) => event.get("user") === "admin"),
// Add source/logger name
processor.addSource("MyApp"),
// Add computer ID or label
processor.addComputerId(),
processor.addComputerLabel(),
// Add static fields to all events
processor.addStaticFields({ env: "production", version: "1.2.3" }),
// Transform a specific field's value
processor.transformField("user_id", (id) => `user_${id}`),
// Remove sensitive fields
processor.removeFields(["password", "token"]),
],
// ... other config
});
Custom Processors
A custom processor is a function that takes a LogEvent and returns a LogEvent or undefined (to drop the event).
import { LogEvent } from "@/lib/ccStructLog";
// Add a unique request ID to all log events
const addRequestId = (event: LogEvent): LogEvent => {
event.set("requestId", `req_${Math.random().toString(36).substr(2, 9)}`);
return event;
};
// Sanitize sensitive information
const sanitizePasswords = (event: LogEvent): LogEvent => {
if (event.has("password")) {
event.set("password", "[REDACTED]");
}
return event;
};
Renderers
Renderers convert the final LogEvent object into a string.
Built-in Renderers
import { textRenderer, jsonRenderer } from "@/lib/ccStructLog";
// textRenderer: Human-readable, colored output for the console.
// Example: [15:30:45] [INFO] Message key=value
// jsonRenderer: Machine-readable JSON output.
// Example: {"level":2,"message":"Message","key":"value","timestamp":"15:30:45"}
Streams
Streams handle the final output destination. You can use multiple streams to send logs to different places.
Built-in Streams
import {
ConsoleStream,
FileStream,
BufferStream,
NullStream,
ConditionalStream,
LogLevel,
DAY,
} from "@/lib/ccStructLog";
// File stream with daily rotation
const fileStream = new FileStream("app.log", DAY);
// Buffer stream (useful for testing or UI display)
const bufferStream = new BufferStream(100); // Keep last 100 messages
// Conditional stream (only send errors to a separate file)
const errorStream = new ConditionalStream(
new FileStream("errors.log"),
(message, event) => (event.get("level") as LogLevel) >= LogLevel.Error
);
File Rotation
FileStream supports automatic file rotation based on time intervals. The rotation interval is specified in seconds as the second argument to the constructor.
import { FileStream, HOUR, DAY, WEEK } from "@/lib/ccStructLog";
// Rotate every hour
const hourlyLog = new FileStream("app_hourly.log", HOUR);
// Rotate daily (recommended for most applications)
const dailyLog = new FileStream("app_daily.log", DAY);
// Rotate weekly
const weeklyLog = new FileStream("app_weekly.log", WEEK);
// No rotation (pass 0 or undefined)
const permanentLog = new FileStream("permanent.log", 0);
Best Practices
-
Use Structured Context: Always provide relevant context as key-value pairs.
// Good logger.info("User action completed", { userId: 123, action: "purchase" }); // Less useful logger.info("User 123 purchased an item"); -
Choose Appropriate Levels:
debug: For developers to diagnose issues.info: Normal application behavior.warn: Potentially harmful situations that don't break functionality.error: Errors that affect a single operation but not the whole app.fatal: Critical errors that require the application to shut down.
-
Use a
source: Identify which component generated the log usingprocessor.addSource.import { Logger, processor } from "@/lib/ccStructLog"; const logger = new Logger({ processors: [processor.addSource("UserService")], // ... }); -
Sanitize Sensitive Data: Use a processor to remove passwords, API keys, etc.
import { Logger, processor } from "@/lib/ccStructLog"; const secureLogger = new Logger({ processors: [ processor.removeFields(["password", "token"]) ], //... }); -
Proper Cleanup: Close loggers during application shutdown to ensure file streams are saved.
// At application shutdown logger.close();
Examples
See src/logExample/main.ts for comprehensive usage examples including:
- Basic logging patterns
- Custom processor chains
- Multiple output streams with different formats
- Error handling strategies
API Reference
For complete API documentation, refer to the TypeScript definitions in each module:
src/lib/ccStructLog/types.ts- Core interfaces and typessrc/lib/ccStructLog/Logger.ts- Main Logger classsrc/lib/ccStructLog/processors.ts- Built-in processorssrc/lib/ccStructLog/renderers.ts- Built-in rendererssrc/lib/ccStructLog/streams.ts- Built-in streamssrc/lib/ccStructLog/index.ts- Convenience functions and exports