diff --git a/.gitignore b/.gitignore index 61d6e1b..ad8baba 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ build/ reference/ src/**/*.md -QWEN.md +.ai/ # Devenv .devenv* diff --git a/.justfile b/.justfile index f73aa69..82eec11 100644 --- a/.justfile +++ b/.justfile @@ -13,7 +13,7 @@ build-accesscontrol: build-test: pnpm tstl -p ./targets/tsconfig.test.json -build-example: build-tuiExample build-cliExample +build-example: build-tuiExample build-cliExample build-logExample build-tuiExample: pnpm tstl -p ./targets/tsconfig.tuiExample.json @@ -21,6 +21,9 @@ build-tuiExample: build-cliExample: pnpm tstl -p ./targets/tsconfig.cliExample.json +build-logExample: + pnpm tstl -p ./targets/tsconfig.logExample.json + sync: rsync --delete -r "./build/" "{{ sync-path }}" diff --git a/README.md b/README.md index 3cbd319..5ec7afb 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ A declarative, reactive TUI (Terminal User Interface) framework inspired by [Sol - **Component-Based:** Structure your UI into reusable components. See `src/tuiExample/main.ts` for a demo. ### 4. ccCLI Framework -A lightweight, functional-style framework for building command-line interfaces (CLIs) within CC:Tweaked. It supports nested commands, arguments, options, and automatic help generation. See the [ccCLI Documentation](docs/ccCLI.md) for more details. +A lightweight, functional-style framework for building command-line interfaces (CLIs) within CC:Tweaked. It supports nested commands, arguments, options, and automatic help generation. See the [ccCLI Documentation](./docs/ccCLI.md) for more details. - **Declarative API:** Define commands, arguments, and options using a simple, object-based structure. - **Nested Commands:** Organize complex applications with subcommands (e.g., `mycli command subcommand`). @@ -40,8 +40,8 @@ A lightweight, functional-style framework for building command-line interfaces ( - **Type-Safe:** Built with TypeScript for robust development. ### 5. Core Libraries -- **`ChatManager`:** A powerful manager for `chatBox` peripherals that handles message queuing, cooldowns, and asynchronous sending/receiving. See the [ChatManager Documentation](docs/ChatManager.md) for more details. -- **`ccLog`:** A robust logging library with automatic, time-based log file rotation. +- **`ChatManager`:** A powerful manager for `chatBox` peripherals that handles message queuing, cooldowns, and asynchronous sending/receiving. See the [ChatManager Documentation](./docs/ChatManager.md) for more details. +- **`ccStructLog`:** A modern, structured logging library inspired by Python's `structlog`. It provides a flexible, extensible framework based on processors, renderers, and streams, designed for CC:Tweaked. See the [ccStructLog Documentation](./docs/ccStructLog.md) for more details. - **`PeripheralManager`:** A utility for easily finding and requiring peripherals by name or type. - **`CraftManager`:** A library for parsing and executing crafting recipes from Create mod packages. @@ -152,4 +152,4 @@ tuiExample ## License -This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. +This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for details. diff --git a/docs/ccStructLog.md b/docs/ccStructLog.md new file mode 100644 index 0000000..b1916a4 --- /dev/null +++ b/docs/ccStructLog.md @@ -0,0 +1,281 @@ +# 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 + +```typescript +import { createDevLogger } from "@/lib/ccStructLog"; + +// Create a development logger +const logger = createDevLogger(); + +// 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 }); +``` + +## Core Concepts + +### Log Levels + +```typescript +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 + +1. **Capture**: User calls `logger.info("message", {key: "value"})`. +2. **Package**: A `LogEvent` object (`Map`) is created with the message, context, and metadata. +3. **Process**: The event is passed through a chain of processors (e.g., to add a timestamp, filter by level). +4. **Render**: The final event is converted to a string by a renderer (e.g., `textRenderer`, `jsonRenderer`). +5. **Output**: The string is sent to one or more streams (e.g., console, file). + +## Pre-configured Loggers + +### Development Logger +Optimized for development and debugging with human-readable console output. + +```typescript +import { createDevLogger, LogLevel } from "@/lib/ccStructLog"; + +const logger = createDevLogger({ + source: "MyApp", + includeComputerId: true, +}); + +logger.debug("This is a debug message."); +``` + +### Production Logger +Optimized for production with JSON-formatted file output and daily rotation. + +```typescript +import { createProdLogger, DAY } from "@/lib/ccStructLog"; + +const logger = createProdLogger("app.log", { + source: "MyApp", + rotationInterval: DAY, // Rotate daily + includeConsole: false, // Don't log to console +}); + +logger.info("Application is running in production."); +``` + +## Custom Configuration + +You can create a logger with a completely custom setup. + +```typescript +import { + Logger, + LogLevel, + addFullTimestamp, + addComputerId, + addSource, + jsonRenderer, + FileStream, + ConsoleStream, + HOUR, +} from "@/lib/ccStructLog"; + +const logger = new Logger({ + processors: [ + addFullTimestamp(), + addComputerId(), + 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. + +### Built-in Processors +```typescript +import { + addTimestamp, // Add structured timestamp + addFormattedTimestamp, // Add HH:MM:SS string + addFullTimestamp, // Add YYYY-MM-DD HH:MM:SS string + filterByLevel, // Filter by minimum level + filterBy, // Filter based on a custom predicate + addSource, // Add source/logger name + addComputerId, // Add computer ID + addComputerLabel, // Add computer label + addStaticFields, // Add static fields to all events + transformField, // Transform a specific field's value + removeFields, // Remove sensitive fields +} from "@/lib/ccStructLog"; + +// Usage example +const logger = new Logger({ + processors: [ + addTimestamp(), + addSource("MyApp"), + filterByLevel(LogLevel.Warn), // Only allow Warn, Error, Fatal + 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). + +```typescript +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 +```typescript +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":"info","message":"Message","key":"value","timestamp":"..."} +``` + +## Streams + +Streams handle the final output destination. You can use multiple streams to send logs to different places. + +### Built-in Streams +```typescript +import { + ConsoleStream, // Output to CC:Tweaked terminal with colors + FileStream, // Output to file with rotation support + BufferStream, // Store in an in-memory buffer + NullStream, // Discard all output +} from "@/lib/ccStructLog"; +import { ConditionalStream } from "@/lib/ccStructLog/streams"; // Note direct import + +// 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. + +```typescript +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 +const permanentLog = new FileStream("permanent.log", 0); +``` + +## Best Practices + +1. **Use Structured Context**: Always provide relevant context as key-value pairs. + ```typescript + // Good + logger.info("User action completed", { userId: 123, action: "purchase" }); + + // Less useful + logger.info("User 123 purchased an item"); + ``` + +2. **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. + +3. **Use a `source`**: Identify which component generated the log. + ```typescript + const logger = createDevLogger({ source: "UserService" }); + ``` + +4. **Sanitize Sensitive Data**: Use a processor to remove passwords, API keys, etc. + ```typescript + const secureLogger = new Logger({ + processors: [ removeFields(["password", "token"]) ], + //... + }); + ``` + +5. **Proper Cleanup**: Close loggers during application shutdown to ensure file streams are saved. + ```typescript + // 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 types +- `src/lib/ccStructLog/Logger.ts` - Main Logger class +- `src/lib/ccStructLog/processors.ts` - Built-in processors +- `src/lib/ccStructLog/renderers.ts` - Built-in renderers +- `src/lib/ccStructLog/streams.ts` - Built-in streams +- `src/lib/ccStructLog/index.ts` - Convenience functions and exports diff --git a/src/accesscontrol/logviewer.ts b/src/accesscontrol/logviewer.ts deleted file mode 100644 index 520c087..0000000 --- a/src/accesscontrol/logviewer.ts +++ /dev/null @@ -1,110 +0,0 @@ -/** - * Access Control Log Viewer - * Simple log viewer that allows launching the TUI with 'c' key - */ - -import { launchAccessControlTUI } from "./tui"; - -const args = [...$vararg]; - -function displayLog(filepath: string, lines = 20) { - const [file] = io.open(filepath, "r"); - if (!file) { - print(`Failed to open log file: ${filepath}`); - return; - } - - const content = file.read("*a"); - file.close(); - - if (content === null || content === undefined || content === "") { - print("Log file is empty"); - return; - } - - const logLines = content.split("\n"); - const startIndex = Math.max(0, logLines.length - lines); - const displayLines = logLines.slice(startIndex); - - term.clear(); - term.setCursorPos(1, 1); - - print("=== Access Control Log Viewer ==="); - print("Press 'c' to open configuration TUI, 'q' to quit, 'r' to refresh"); - print("=========================================="); - print(""); - - for (const line of displayLines) { - if (line.trim() !== "") { - print(line); - } - } - - print(""); - print("=========================================="); - print(`Showing last ${displayLines.length} lines of ${filepath}`); -} - -function main(args: string[]) { - const logFilepath = args[0] || `${shell.dir()}/accesscontrol.log`; - const lines = args[1] ? parseInt(args[1]) : 20; - - if (isNaN(lines) || lines <= 0) { - print("Usage: logviewer [logfile] [lines]"); - print(" logfile - Path to log file (default: accesscontrol.log)"); - print(" lines - Number of lines to display (default: 20)"); - return; - } - - let running = true; - - // Initial display - displayLog(logFilepath, lines); - - while (running) { - const [eventType, key] = os.pullEvent(); - - if (eventType === "key") { - if (key === keys.c) { - // Launch TUI - print("Launching Access Control TUI..."); - try { - launchAccessControlTUI(); - // Refresh display after TUI closes - displayLog(logFilepath, lines); - } catch (error) { - if (error === "TUI_CLOSE" || error === "Terminated") { - displayLog(logFilepath, lines); - } else { - print(`TUI error: ${String(error)}`); - os.sleep(2); - displayLog(logFilepath, lines); - } - } - } else if (key === keys.q) { - // Quit - running = false; - } else if (key === keys.r) { - // Refresh - displayLog(logFilepath, lines); - } - } else if (eventType === "terminate") { - running = false; - } - } - - term.clear(); - term.setCursorPos(1, 1); - print("Log viewer closed."); -} - -try { - main(args); -} catch (error) { - if (error === "Terminated") { - print("Log viewer terminated by user."); - } else { - print("Error in log viewer:"); - printError(error); - } -} diff --git a/src/lib/ccLog.ts b/src/lib/ccLog.ts deleted file mode 100644 index 078ecef..0000000 --- a/src/lib/ccLog.ts +++ /dev/null @@ -1,183 +0,0 @@ -export enum LogLevel { - Debug = 0, - Info = 1, - Warn = 2, - Error = 3, -} - -// Define time interval constants in seconds -export const SECOND = 1; -export const MINUTE = 60 * SECOND; -export const HOUR = 60 * MINUTE; -export const DAY = 24 * HOUR; -export const WEEK = 7 * DAY; - -export interface CCLogInitConfig { - printTerminal?: boolean; - logInterval?: number; - outputMinLevel?: LogLevel; -} - -export class CCLog { - private fp: LuaFile | undefined; - private filename?: string; - private logInterval: number; - private printTerminal: boolean; - private outputMinLevel: LogLevel; - private startTime: number; - private currentTimePeriod: string; - - constructor(filename?: string, config?: CCLogInitConfig) { - term.clear(); - term.setCursorPos(1, 1); - - this.logInterval = config?.logInterval ?? DAY; - this.printTerminal = config?.printTerminal ?? true; - this.outputMinLevel = config?.outputMinLevel ?? LogLevel.Debug; - - this.startTime = os.time(os.date("*t")); - this.currentTimePeriod = this.getTimePeriodString(this.startTime); - - if (filename != undefined && filename.length != 0) { - this.filename = filename; - const filepath = this.generateFilePath(filename, this.currentTimePeriod); - const [file, error] = io.open(filepath, fs.exists(filepath) ? "a" : "w+"); - if (file != undefined) { - this.fp = file; - } else { - throw Error(error); - } - } - } - - /** - * Generates a time period string based on the interval - * For DAY interval: YYYY-MM-DD - * For HOUR interval: YYYY-MM-DD-HH - * For MINUTE interval: YYYY-MM-DD-HH-MM - * For SECOND interval: YYYY-MM-DD-HH-MM-SS - */ - private getTimePeriodString(time: number): string { - const periodStart = Math.floor(time / this.logInterval) * this.logInterval; - const d = os.date("*t", periodStart); - - if (this.logInterval >= DAY) { - return `${d.year}-${string.format("%02d", d.month)}-${string.format("%02d", d.day)}`; - } else if (this.logInterval >= HOUR) { - return `${d.year}-${string.format("%02d", d.month)}-${string.format("%02d", d.day)}_${string.format("%02d", d.hour)}`; - } else if (this.logInterval >= MINUTE) { - return `${d.year}-${string.format("%02d", d.month)}-${string.format("%02d", d.day)}_${string.format("%02d", d.hour)}-${string.format("%02d", d.min)}`; - } - return `${d.year}-${string.format("%02d", d.month)}-${string.format("%02d", d.day)}_${string.format("%02d", d.hour)}-${string.format("%02d", d.min)}-${string.format("%02d", d.sec)}`; - } - - private generateFilePath(baseFilename: string, timePeriod: string): string { - const scriptDir = shell.dir() ?? ""; - - const [filenameWithoutExt, extension] = baseFilename.includes(".") - ? baseFilename.split(".") - : [baseFilename, "log"]; - - return fs.combine( - scriptDir, - `${filenameWithoutExt}_${timePeriod}.${extension}`, - ); - } - - private checkAndRotateLogFile() { - if (this.filename != undefined && this.filename.length != 0) { - const currentTime = os.time(os.date("*t")); - const currentTimePeriod = this.getTimePeriodString(currentTime); - - // If we're in a new time period, rotate the log file - if (currentTimePeriod !== this.currentTimePeriod) { - // Close current file if open - if (this.fp) { - this.fp.close(); - this.fp = undefined; - } - - // Update the current time period - this.currentTimePeriod = currentTimePeriod; - - // Open new log file for the new time period - const filepath = this.generateFilePath( - this.filename, - this.currentTimePeriod, - ); - const [file, error] = io.open( - filepath, - fs.exists(filepath) ? "a" : "w+", - ); - if (file != undefined) { - this.fp = file; - } else { - throw Error(error); - } - } - } - } - - private getFormatMsg(msg: string, level: LogLevel): string { - const date = os.date("*t"); - return `[ ${date.year}/${String(date.month).padStart(2, "0")}/${String(date.day).padStart(2, "0")} ${String(date.hour).padStart(2, "0")}:${String(date.min).padStart(2, "0")}:${String(date.sec).padStart(2, "0")} ${LogLevel[level]} ] : ${msg}`; - } - - public writeLine(msg: string, color?: Color) { - // Check if we need to rotate the log file - this.checkAndRotateLogFile(); - - if (this.printTerminal) { - let originalColor: Color = 0; - if (color != undefined) { - originalColor = term.getTextColor(); - term.setTextColor(color); - } - print(msg); - - if (color != undefined) { - term.setTextColor(originalColor); - } - } - - // Log - if (this.fp != undefined) { - this.fp.write(msg + "\r\n"); - } - } - - public debug(msg: string) { - if (LogLevel.Debug >= this.outputMinLevel) - this.writeLine(this.getFormatMsg(msg, LogLevel.Debug), colors.gray); - } - - public info(msg: string) { - if (LogLevel.Info >= this.outputMinLevel) - this.writeLine(this.getFormatMsg(msg, LogLevel.Info), colors.green); - } - - public warn(msg: string) { - if (LogLevel.Warn >= this.outputMinLevel) - this.writeLine(this.getFormatMsg(msg, LogLevel.Warn), colors.orange); - } - - public error(msg: string) { - if (LogLevel.Error >= this.outputMinLevel) - this.writeLine(this.getFormatMsg(msg, LogLevel.Error), colors.red); - } - - public setInTerminal(value: boolean) { - this.printTerminal = value; - } - - public setLogLevel(value: LogLevel) { - this.outputMinLevel = value; - } - - public close() { - if (this.fp !== undefined) { - this.fp.close(); - this.fp = undefined; - } - } -} diff --git a/src/lib/ccStructLog/Logger.ts b/src/lib/ccStructLog/Logger.ts new file mode 100644 index 0000000..e0fab33 --- /dev/null +++ b/src/lib/ccStructLog/Logger.ts @@ -0,0 +1,165 @@ +/** + * Main Logger class implementation. + * This is the primary entry point for users to interact with the logging system. + */ + +import { LogLevel, LoggerOptions, LogEvent, ILogger } from "./types"; + +/** + * The main Logger class that orchestrates the logging pipeline. + * + * This class takes log messages, creates LogEvent objects, processes them through + * a chain of processors, renders them to strings, and outputs them via streams. + */ +export class Logger implements ILogger { + private options: LoggerOptions; + + /** + * Create a new Logger instance. + * + * @param options - Configuration options for the logger + */ + constructor(options: Partial) { + this.options = { + processors: options.processors ?? [], + renderer: options.renderer ?? this.defaultRenderer, + streams: options.streams ?? [], + }; + } + + /** + * Default renderer that returns an empty string. + * Used as fallback when no renderer is provided. + */ + private defaultRenderer = (): string => ""; + + /** + * Main logging method that handles the complete logging pipeline. + * + * @param level - The log level + * @param message - The log message + * @param context - Additional context data as key-value pairs + */ + public log( + level: LogLevel, + message: string, + context: Record = {}, + ): void { + // 1. Create initial LogEvent with core fields + let event: LogEvent | undefined = new Map([ + ["level", level], + ["message", message], + ...Object.entries(context), + ]); + + // 2. Process through the processor chain + for (const processor of this.options.processors) { + if (event === undefined) { + break; // Event was dropped by a processor + } + event = processor(event); + } + + // 3. Render and output if event wasn't dropped + if (event !== undefined) { + const finalEvent = event; + const output = this.options.renderer(finalEvent); + + // Send to all configured streams + for (const stream of this.options.streams) { + stream.write(output, finalEvent); + } + } + } + + /** + * Log a trace message. + * Typically used for very detailed diagnostic information. + * + * @param message - The log message + * @param context - Additional context data + */ + public trace(message: string, context?: Record): void { + this.log(LogLevel.Trace, message, context); + } + + /** + * Log a debug message. + * Used for diagnostic information useful during development. + * + * @param message - The log message + * @param context - Additional context data + */ + public debug(message: string, context?: Record): void { + this.log(LogLevel.Debug, message, context); + } + + /** + * Log an info message. + * Used for general informational messages about application flow. + * + * @param message - The log message + * @param context - Additional context data + */ + public info(message: string, context?: Record): void { + this.log(LogLevel.Info, message, context); + } + + /** + * Log a warning message. + * Used for potentially harmful situations that don't stop execution. + * + * @param message - The log message + * @param context - Additional context data + */ + public warn(message: string, context?: Record): void { + this.log(LogLevel.Warn, message, context); + } + + /** + * Log an error message. + * Used for error events that might allow the application to continue. + * + * @param message - The log message + * @param context - Additional context data + */ + public error(message: string, context?: Record): void { + this.log(LogLevel.Error, message, context); + } + + /** + * Log a fatal message. + * Used for very severe error events that might cause termination. + * + * @param message - The log message + * @param context - Additional context data + */ + public fatal(message: string, context?: Record): void { + this.log(LogLevel.Fatal, message, context); + } + + /** + * Update the logger's configuration. + * Useful for dynamically changing logging behavior at runtime. + * + * @param options - New configuration options to merge with existing ones + */ + public configure(options: Partial): void { + this.options = { + ...this.options, + ...options, + }; + } + + /** + * Close all streams and clean up resources. + * Should be called when the logger is no longer needed. + */ + public close(): void { + for (const stream of this.options.streams) { + if (stream.close) { + stream.close(); + } + } + } +} diff --git a/src/lib/ccStructLog/index.ts b/src/lib/ccStructLog/index.ts new file mode 100644 index 0000000..148121b --- /dev/null +++ b/src/lib/ccStructLog/index.ts @@ -0,0 +1,156 @@ +/** + * Main entry point for the ccStructLog library. + * + * This module provides convenient factory functions and pre-configured + * logger instances for common use cases. It exports all the core components + * while providing easy-to-use defaults for typical logging scenarios. + */ + +// Re-export all core types and classes +export { + LogLevel, + LogEvent, + Processor, + Renderer, + Stream, + LoggerOptions, + ILogger, +} from "./types"; +export { Logger } from "./Logger"; + +// Re-export all processors +export { + addTimestamp, + addFormattedTimestamp, + addFullTimestamp, + filterByLevel, + addSource, + addComputerId, + addComputerLabel, + filterBy, + transformField, + removeFields, + addStaticFields, +} from "./processors"; + +// Re-export all renderers +export { jsonRenderer, textRenderer } from "./renderers"; + +// Re-export all streams +export { + ConsoleStream, + FileStream, + BufferStream, + NullStream, + SECOND, + MINUTE, + HOUR, + DAY, + WEEK, +} from "./streams"; + +import { Logger } from "./Logger"; +import { LogLevel, LogEvent } from "./types"; +import { + addFormattedTimestamp, + addFullTimestamp, + addComputerId, +} from "./processors"; +import { textRenderer, jsonRenderer } from "./renderers"; +import { ConsoleStream, FileStream, DAY } from "./streams"; + +/** + * Create a development logger with console output and colored formatting. + * + * This logger is optimized for development and debugging, with: + * - Debug level and above + * - Formatted timestamps + * - Computer ID tracking + * - Human-readable console output with colors + * + * @param options - Optional configuration to override defaults + * @returns A configured Logger instance for development + */ +export function createDevLogger( + options: { + level?: LogLevel; + source?: string; + includeComputerId?: boolean; + } = {}, +): Logger { + const processors = [addFormattedTimestamp()]; + + if (options.includeComputerId !== false) { + processors.push(addComputerId()); + } + + if (options.source) { + processors.push((event: LogEvent) => { + event.set("source", options.source); + return event; + }); + } + + return new Logger({ + processors, + renderer: textRenderer, + streams: [new ConsoleStream()], + }); +} + +/** + * Create a production logger with file output and JSON formatting. + * + * This logger is optimized for production environments, with: + * - Info level and above + * - Full timestamps + * - Computer ID and label tracking + * - JSON output for machine processing + * - Daily file rotation + * + * @param filename - Base filename for log files + * @param options - Optional configuration to override defaults + * @returns A configured Logger instance for production + */ +export function createProdLogger( + filename: string, + options: { + level?: LogLevel; + source?: string; + rotationInterval?: number; + includeConsole?: boolean; + } = {}, +): Logger { + const processors = [ + addFullTimestamp(), + addComputerId(), + (event: LogEvent) => { + const label = os.getComputerLabel(); + if (label) { + event.set("computer_label", label); + } + return event; + }, + ]; + + if (options.source) { + processors.push((event: LogEvent) => { + event.set("source", options.source); + return event; + }); + } + + const streams: Array = [ + new FileStream(filename, options.rotationInterval ?? DAY), + ]; + + if (options.includeConsole) { + streams.push(new ConsoleStream()); + } + + return new Logger({ + processors, + renderer: jsonRenderer, + streams, + }); +} diff --git a/src/lib/ccStructLog/processors.ts b/src/lib/ccStructLog/processors.ts new file mode 100644 index 0000000..9c0bd8f --- /dev/null +++ b/src/lib/ccStructLog/processors.ts @@ -0,0 +1,220 @@ +/** + * Standard processors for the ccStructLog library. + * + * Processors are functions that can modify, enrich, or filter log events + * as they flow through the logging pipeline. Each processor receives a + * LogEvent and can return a modified LogEvent or undefined to drop the log. + */ + +import { LogEvent, Processor, LogLevel } from "./types"; + +/** + * Adds a timestamp to the log event. + * + * This processor adds the current time as a structured timestamp object + * using CC:Tweaked's os.date() function. The timestamp includes year, + * month, day, hour, minute, and second components. + * + * Performance note: os.date() is relatively expensive, so this should + * typically be placed early in the processor chain and used only once. + * + * @param event - The log event to process + * @returns The event with timestamp added + */ +export function addTimestamp(): Processor { + return (event) => { + const timestamp = os.date("!*t") as LuaDate; + event.set("timestamp", timestamp); + return event; + }; +} + +/** + * Adds a human-readable timestamp string to the log event. + * + * This processor adds a formatted timestamp string that's easier to read + * in log output. The format is "HH:MM:SS" in UTC time. + * + * @param event - The log event to process + * @returns The event with formatted timestamp added + */ +export function addFormattedTimestamp(): Processor { + return (event) => { + const timestamp = os.date("!*t") as LuaDate; + const timeStr = `${string.format("%02d", timestamp.hour)}:${string.format("%02d", timestamp.min)}:${string.format("%02d", timestamp.sec)}`; + event.set("time", timeStr); + return event; + }; +} + +/** + * Adds a full ISO-like timestamp string to the log event. + * + * This processor adds a complete timestamp in YYYY-MM-DD HH:MM:SS format + * which is useful for file logging and structured output. + * + * @param event - The log event to process + * @returns The event with full timestamp added + */ +export function addFullTimestamp(): Processor { + return (event) => { + const timestamp = os.date("!*t") as LuaDate; + const fullTimeStr = `${timestamp.year}-${string.format("%02d", timestamp.month)}-${string.format("%02d", timestamp.day)} ${string.format("%02d", timestamp.hour)}:${string.format("%02d", timestamp.min)}:${string.format("%02d", timestamp.sec)}`; + event.set("datetime", fullTimeStr); + return event; + }; +} + +/** + * Filters log events by minimum level. + * + * This processor drops log events that are below the specified minimum level. + * Note: The Logger class already does early filtering for performance, but + * this processor can be useful for dynamic filtering or when you need + * different levels for different streams. + * + * @param minLevel - The minimum log level to allow through + * @returns A processor function that filters by level + */ +export function filterByLevel(minLevel: LogLevel): Processor { + return (event) => { + const eventLevel = event.get("level") as LogLevel | undefined; + if (eventLevel === undefined) { + return event; // Pass through if no level is set + } + + if (eventLevel !== undefined && eventLevel < minLevel) { + return undefined; // Drop the log event + } + + return event; + }; +} + +/** + * Adds a logger name/source to the log event. + * + * This processor is useful when you have multiple loggers in your application + * and want to identify which component generated each log entry. + * + * @param name - The name/source to add to log events + * @returns A processor function that adds the source name + */ +export function addSource(name: string): Processor { + return (event) => { + event.set("source", name); + return event; + }; +} + +/** + * Adds the current computer ID to the log event. + * + * In CC:Tweaked environments, this can help identify which computer + * generated the log when logs are aggregated from multiple sources. + * + * @param event - The log event to process + * @returns The event with computer ID added + */ +export function addComputerId(): Processor { + return (event) => { + event.set("computer_id", os.getComputerID()); + return event; + }; +} + +/** + * Adds the current computer label to the log event. + * + * If the computer has a label set, this adds it to the log event. + * This can be more human-readable than the computer ID. + * + * @param event - The log event to process + * @returns The event with computer label added (if available) + */ +export function addComputerLabel(): Processor { + return (event) => { + const label = os.getComputerLabel(); + if (label !== undefined && label !== null) { + event.set("computer_label", label); + } + return event; + }; +} + +/** + * Filters out events that match a specific condition. + * + * This is a generic processor that allows you to filter events based on + * any custom condition. The predicate function should return true to keep + * the event and false to drop it. + * + * @param predicate - Function that returns true to keep the event + * @returns A processor function that filters based on the predicate + */ +export function filterBy(predicate: (event: LogEvent) => boolean): Processor { + return (event) => { + return predicate(event) ? event : undefined; + }; +} + +/** + * Transforms a specific field in the log event. + * + * This processor allows you to modify the value of a specific field + * using a transformation function. + * + * @param fieldName - The name of the field to transform + * @param transformer - Function to transform the field value + * @returns A processor function that transforms the specified field + */ +export function transformField( + fieldName: string, + transformer: (value: unknown) => unknown, +): Processor { + return (event) => { + if (event.has(fieldName)) { + const currentValue = event.get(fieldName); + const newValue = transformer(currentValue); + event.set(fieldName, newValue); + } + return event; + }; +} + +/** + * Removes specified fields from the log event. + * + * This processor can be used to strip sensitive or unnecessary information + * from log events before they are rendered and output. + * + * @param fieldNames - Array of field names to remove + * @returns A processor function that removes the specified fields + */ +export function removeFields(fieldNames: string[]): Processor { + return (event) => { + for (const fieldName of fieldNames) { + event.delete(fieldName); + } + return event; + }; +} + +/** + * Adds static fields to every log event. + * + * This processor adds the same set of fields to every log event that + * passes through it. Useful for adding application name, version, + * environment, etc. + * + * @param fields - Object containing the static fields to add + * @returns A processor function that adds the static fields + */ +export function addStaticFields(fields: Record): Processor { + return (event) => { + for (const [key, value] of Object.entries(fields)) { + event.set(key, value); + } + return event; + }; +} diff --git a/src/lib/ccStructLog/renderers.ts b/src/lib/ccStructLog/renderers.ts new file mode 100644 index 0000000..aab84b6 --- /dev/null +++ b/src/lib/ccStructLog/renderers.ts @@ -0,0 +1,87 @@ +/** + * Standard renderers for the ccStructLog library. + * + * Renderers are functions that convert processed LogEvent objects into + * their final string representation. Different renderers can produce + * different output formats (JSON, console-friendly, etc.). + */ + +import { Renderer } from "./types"; + +/** + * Renders log events as JSON strings. + * + * This renderer converts the LogEvent Map into a plain object and then + * serializes it as JSON. This format is ideal for structured logging + * and machine processing. + * + * Note: This assumes textutils.serialiseJSON is available (CC:Tweaked). + * Falls back to a simple key=value format if JSON serialization fails. + * + * @param event - The log event to render + * @returns JSON string representation of the event + */ +export const jsonRenderer: Renderer = (event) => { + try { + // Convert Map to plain object for JSON serialization + const obj: Record = {}; + for (const [key, value] of event.entries()) { + obj[key] = value; + } + + // Use CC:Tweaked's JSON serialization if available + return textutils.serialiseJSON(obj); + } catch (error) { + return String(error); + } +}; + +/** + * Renders log events in a human-readable Text format. + * + * This renderer creates output suitable for terminal display, with + * timestamp, level, message, and additional context fields formatted + * in a readable way. + * + * Format: [HH:MM:SS] [LEVEL] message { key=value, key2=value2 } + * + * @param event - The log event to render + * @returns Human-readable string representation + */ +export const textRenderer: Renderer = (event) => { + // Extract core components + const timestamp = event.get("timestamp") as LuaDate | undefined; + const timeStr = event.get("time") as string | undefined; + const level = (event.get("level") as string)?.toUpperCase() ?? "UNKNOWN"; + const message = (event.get("message") as string) ?? ""; + + // Format timestamp + let timestampStr = ""; + if (timeStr) { + timestampStr = timeStr; + } else if (timestamp) { + timestampStr = `${string.format("%02d", timestamp.hour)}:${string.format("%02d", timestamp.min)}:${string.format("%02d", timestamp.sec)}`; + } + + // Start building the output + let output = `[${timestampStr}] [${level}] ${message}`; + + // Add context fields (excluding the core fields we already used) + const contextFields: string[] = []; + for (const [key, value] of event.entries()) { + if ( + key !== "timestamp" && + key !== "time" && + key !== "level" && + key !== "message" + ) { + contextFields.push(`${key}=${tostring(value)}`); + } + } + + if (contextFields.length > 0) { + output += ` { ${contextFields.join(", ")} }`; + } + + return output; +}; diff --git a/src/lib/ccStructLog/streams.ts b/src/lib/ccStructLog/streams.ts new file mode 100644 index 0000000..acc2691 --- /dev/null +++ b/src/lib/ccStructLog/streams.ts @@ -0,0 +1,308 @@ +/** + * Standard output streams for the ccStructLog library. + * + * Streams are responsible for writing the final formatted log messages + * to their destination (console, file, network, etc.). Each stream + * implements the Stream interface and handles its own output logic. + */ + +import { Stream, LogEvent } from "./types"; + +/** + * Console stream that outputs to the CC:Tweaked terminal. + * + * This stream writes log messages to the computer's terminal with + * color coding based on log levels. It preserves the original text + * color after writing each message. + */ +export class ConsoleStream implements Stream { + private levelColors: Map = new Map([ + ["trace", colors.lightGray], + ["debug", colors.gray], + ["info", colors.white], + ["warn", colors.orange], + ["error", colors.red], + ["fatal", colors.red], + ]); + + /** + * Write a formatted log message to the terminal. + * + * @param message - The formatted log message + * @param event - The original log event for context (used for level-based coloring) + */ + public write(message: string, event: LogEvent): void { + const level = event.get("level") as string | undefined; + const color = level ? this.levelColors.get(level) : undefined; + + if (color !== undefined) { + const originalColor = term.getTextColor(); + term.setTextColor(color); + print(message); + term.setTextColor(originalColor); + } else { + print(message); + } + } +} + +/** + * File stream that outputs to a file on disk. + * + * This stream writes log messages to a specified file, creating the file + * if it doesn't exist and appending to it if it does. It handles file + * rotation based on time intervals. + */ +export class FileStream implements Stream { + private fileHandle: LuaFile | undefined; + private filePath: string; + private rotationInterval: number; + private lastRotationTime: number; + private baseFilename: string; + + /** + * Create a new file stream. + * + * @param filePath - Path to the log file + * @param rotationInterval - Time in seconds between file rotations (0 = no rotation) + */ + constructor(filePath: string, rotationInterval: number = 0) { + this.filePath = filePath; + this.rotationInterval = rotationInterval; + this.lastRotationTime = os.time(); + this.baseFilename = filePath; + + this.openFile(); + } + + /** + * Open the log file for writing. + * Creates the file if it doesn't exist, appends if it does. + */ + private openFile(): void { + const actualPath = + this.rotationInterval > 0 + ? this.getRotatedFilename() + : this.filePath; + + const [handle, err] = io.open(actualPath, "a"); + if (handle === undefined) { + printError( + `Failed to open log file ${actualPath}: ${err ?? "Unknown error"}`, + ); + return; + } + this.fileHandle = handle; + } + + /** + * Generate a filename with timestamp for file rotation. + */ + private getRotatedFilename(): string { + const currentTime = os.time(); + const rotationPeriod = + Math.floor(currentTime / this.rotationInterval) * + this.rotationInterval; + const date = os.date("*t", rotationPeriod) as LuaDate; + + const timestamp = `${date.year}-${string.format("%02d", date.month)}-${string.format("%02d", date.day)}_${string.format("%02d", date.hour)}-${string.format("%02d", date.min)}`; + + // Split filename and extension + const splitStrs = this.baseFilename.split("."); + if (splitStrs.length === 1) { + return `${this.baseFilename}_${timestamp}.log`; + } + + const name = splitStrs[0]; + const ext = splitStrs[1]; + return `${name}_${timestamp}.${ext}`; + } + + /** + * Check if file rotation is needed and rotate if necessary. + */ + private checkRotation(): void { + if (this.rotationInterval <= 0) return; + + const currentTime = os.time(); + const currentPeriod = Math.floor(currentTime / this.rotationInterval); + const lastPeriod = Math.floor( + this.lastRotationTime / this.rotationInterval, + ); + + if (currentPeriod > lastPeriod) { + // Time to rotate + this.close(); + this.lastRotationTime = currentTime; + this.openFile(); + } + } + + /** + * Write a formatted log message to the file. + * + * @param message - The formatted log message + * @param event - The original log event (unused in this implementation) + */ + public write(message: string, event: LogEvent): void { + this.checkRotation(); + + if (this.fileHandle) { + this.fileHandle.write(message + "\n"); + this.fileHandle.flush(); + } + } + + /** + * Close the file handle and release resources. + */ + public close(): void { + if (this.fileHandle) { + this.fileHandle.close(); + this.fileHandle = undefined; + } + } +} + +/** + * Buffer stream that collects log messages in memory. + * + * This stream stores log messages in an internal buffer, which can be + * useful for testing, temporary storage, or implementing custom output + * logic that processes multiple messages at once. + */ +export class BufferStream implements Stream { + private buffer: string[] = []; + private maxSize: number; + + /** + * Create a new buffer stream. + * + * @param maxSize - Maximum number of messages to store (0 = unlimited) + */ + constructor(maxSize: number = 0) { + this.maxSize = maxSize; + } + + /** + * Write a formatted log message to the buffer. + * + * @param message - The formatted log message + * @param event - The original log event (unused in this implementation) + */ + public write(message: string, event: LogEvent): void { + this.buffer.push(message); + + // Trim buffer if it exceeds max size + if (this.maxSize > 0 && this.buffer.length > this.maxSize) { + this.buffer.shift(); + } + } + + /** + * Get all buffered messages. + * + * @returns Array of all buffered log messages + */ + public getMessages(): string[] { + return [...this.buffer]; + } + + /** + * Get and clear all buffered messages. + * + * @returns Array of all buffered log messages + */ + public flush(): string[] { + const messages = [...this.buffer]; + this.buffer = []; + return messages; + } + + /** + * Clear the buffer without returning messages. + */ + public clear(): void { + this.buffer = []; + } + + /** + * Get the current number of buffered messages. + * + * @returns Number of messages in the buffer + */ + public size(): number { + return this.buffer.length; + } +} + +/** + * Null stream that discards all log messages. + * + * This stream can be useful for testing or when you want to temporarily + * disable logging output without reconfiguring the entire logger. + */ +export class NullStream implements Stream { + /** + * Discard the log message (do nothing). + * + * @param message - The formatted log message (ignored) + * @param event - The original log event (ignored) + */ + public write(message: string, event: LogEvent): void { + // Intentionally do nothing + } +} + +/** + * Conditional stream that only writes messages meeting certain criteria. + * + * This stream wraps another stream and only forwards messages that + * match the specified condition. + */ +export class ConditionalStream implements Stream { + private targetStream: Stream; + private condition: (message: string, event: LogEvent) => boolean; + + /** + * Create a new conditional stream. + * + * @param targetStream - The stream to write to when condition is met + * @param condition - Function that returns true to allow writing + */ + constructor( + targetStream: Stream, + condition: (message: string, event: LogEvent) => boolean, + ) { + this.targetStream = targetStream; + this.condition = (message, event) => condition(message, event); + } + + /** + * Write a formatted log message if the condition is met. + * + * @param message - The formatted log message + * @param event - The original log event + */ + public write(message: string, event: LogEvent): void { + if (this.condition(message, event)) { + this.targetStream.write(message, event); + } + } + + /** + * Close the target stream. + */ + public close(): void { + if (this.targetStream.close) { + this.targetStream.close(); + } + } +} + +// Time constants for file rotation +export const SECOND = 1; +export const MINUTE = 60 * SECOND; +export const HOUR = 60 * MINUTE; +export const DAY = 24 * HOUR; +export const WEEK = 7 * DAY; diff --git a/src/lib/ccStructLog/types.ts b/src/lib/ccStructLog/types.ts new file mode 100644 index 0000000..c6893c7 --- /dev/null +++ b/src/lib/ccStructLog/types.ts @@ -0,0 +1,107 @@ +/** + * Core types for the ccStructLog library. + * This module defines the fundamental interfaces and types used throughout the logging system. + */ + +/** + * Available log levels in order of severity. + */ +export enum LogLevel { + Trace = 0, + Debug = 1, + Info = 2, + Warn = 3, + Error = 4, + Fatal = 5, +} + +/** + * A log event represented as a key-value map. + * Uses Map to maintain insertion order of keys. + */ +export type LogEvent = Map; + +/** + * A processor function that can modify, filter, or enrich log events. + * + * @param event - The log event to process + * @returns The processed log event, or undefined to drop the log + */ +export type Processor = (event: LogEvent) => LogEvent | undefined; + +/** + * A renderer function that converts a log event to a string representation. + * + * @param event - The final log event after all processing + * @returns The formatted string representation + */ +export type Renderer = (event: LogEvent) => string; + +/** + * Interface for output streams that handle the final log output. + */ +export interface Stream { + /** + * Write a formatted log message to the output destination. + * + * @param message - The formatted log message + * @param event - The original log event for context + */ + write(message: string, event: LogEvent): void; + + /** + * Close the stream and release any resources. + * Optional method for cleanup. + */ + close?(): void; +} + +/** + * Configuration options for creating a Logger instance. + */ +export interface LoggerOptions { + /** Array of processors to apply to log events */ + processors: Processor[]; + + /** Renderer to format the final log output */ + renderer: Renderer; + + /** Array of streams to output the formatted logs */ + streams: Stream[]; +} + +/** + * Interface for the main Logger class. + */ +export interface ILogger { + /** + * Log a message at the specified level. + * + * @param level - The log level + * @param message - The log message + * @param context - Additional context data + */ + log( + level: LogLevel, + message: string, + context?: Record, + ): void; + + /** Log at trace level */ + trace(message: string, context?: Record): void; + + /** Log at debug level */ + debug(message: string, context?: Record): void; + + /** Log at info level */ + info(message: string, context?: Record): void; + + /** Log at warn level */ + warn(message: string, context?: Record): void; + + /** Log at error level */ + error(message: string, context?: Record): void; + + /** Log at fatal level */ + fatal(message: string, context?: Record): void; +} diff --git a/src/lib/ccTime.ts b/src/lib/ccTime.ts index 33b0ff3..b88ddea 100644 --- a/src/lib/ccTime.ts +++ b/src/lib/ccTime.ts @@ -1,25 +1,23 @@ -class ccDate { - private _timestamp: number; +export class ccDate { + private _timestamp: number; - constructor() { - this._timestamp = os.time(os.date("*t")); - } + constructor() { + this._timestamp = os.time(os.date("*t")); + } - public static toDateTable(timestamp: number): LuaDate { - return os.date("*t", timestamp) as LuaDate; - } + public static toDateTable(timestamp: number): LuaDate { + return os.date("*t", timestamp) as LuaDate; + } - public toDateTable(): LuaDate { - return os.date("*t", this._timestamp) as LuaDate; - } + public toDateTable(): LuaDate { + return os.date("*t", this._timestamp) as LuaDate; + } - public static toTimestamp(date: LuaDate): number { - return os.time(date); - } + public static toTimestamp(date: LuaDate): number { + return os.time(date); + } - public toTimestamp(): number { - return this._timestamp; - } + public toTimestamp(): number { + return this._timestamp; + } } - -export { ccDate }; diff --git a/src/logExample/main.ts b/src/logExample/main.ts new file mode 100644 index 0000000..8377849 --- /dev/null +++ b/src/logExample/main.ts @@ -0,0 +1,285 @@ +/** + * Example usage of the ccStructLog library. + * + * This file demonstrates various ways to use the restructured logging system, + * including basic usage, custom configurations, and advanced scenarios. + */ + +import { + Logger, + createDevLogger, + createProdLogger, + + // Processors + addTimestamp, + addFormattedTimestamp, + addFullTimestamp, + addSource, + addComputerId, + addStaticFields, + transformField, + + // Renderers + textRenderer, + jsonRenderer, + + // Streams + ConsoleStream, + FileStream, + BufferStream, + DAY, + HOUR, + LogLevel, +} from "../lib/ccStructLog"; +import { ConditionalStream } from "@/lib/ccStructLog/streams"; + +// ============================================================================= +// Basic Usage Examples +// ============================================================================= + +print("=== Basic Usage Examples ==="); + +// 1. Quick start with pre-configured loggers +const devLog = createDevLogger(); +devLog.info("Application started", { version: "1.0.0", port: 8080 }); +devLog.debug("Debug information", { userId: 123, action: "login" }); +devLog.error("Something went wrong", { + error: "Connection failed", + retries: 3, +}); + +// 2. Production logging to file +const prodLog = createProdLogger("app.log", { + source: "MyApplication", + rotationInterval: DAY, + includeConsole: true, +}); + +prodLog.info("User action", { userId: 456, action: "purchase", amount: 29.99 }); +prodLog.warn("Low disk space", { available: 1024, threshold: 2048 }); + +// ============================================================================= +// Custom Logger Configurations +// ============================================================================= + +print("\n=== Custom Logger Configurations ==="); + +// 4. Custom logger with specific processors and renderer +const customLogger = new Logger({ + processors: [ + addFullTimestamp(), + addComputerId(), + addSource("CustomApp"), + addStaticFields({ + environment: "development", + version: "2.1.0", + }), + ], + renderer: jsonRenderer, + streams: [new ConsoleStream(), new FileStream("custom.log", HOUR)], +}); + +customLogger.info("Custom logger example", { + feature: "user_management", + operation: "create_user", +}); + +// ============================================================================= +// Advanced Processor Examples +// ============================================================================= + +print("\n=== Advanced Processor Examples ==="); + +// 6. Custom processors +const addRequestId = (event: Map) => { + event.set("requestId", `req_${Math.random().toString(36).substr(2, 9)}`); + return event; +}; + +const sanitizePasswords = (event: Map) => { + // Remove sensitive information + if (event.has("password")) { + event.set("password", "[REDACTED]"); + } + if (event.has("token")) { + event.set("token", "[REDACTED]"); + } + return event; +}; + +const secureLogger = new Logger({ + processors: [ + addTimestamp(), + addRequestId, + sanitizePasswords, + transformField("message", (msg) => `[SECURE] ${msg}`), + ], + renderer: jsonRenderer, + streams: [new ConsoleStream()], +}); + +secureLogger.info("User login attempt", { + username: "john_doe", + password: "secret123", + token: "abc123def456", +}); + +// ============================================================================= +// Stream Examples +// ============================================================================= + +print("\n=== Stream Examples ==="); + +// 11. Buffer stream for batch processing +const bufferStream = new BufferStream(100); // Keep last 100 messages +const bufferLogger = new Logger({ + processors: [addFormattedTimestamp()], + renderer: textRenderer, + streams: [ + new ConditionalStream(new ConsoleStream(), (msg, event) => { + if (event.get("level") === LogLevel.Info) { + return false; + } else { + return true; + } + }), + bufferStream, + ], +}); + +// Log several messages +for (let i = 0; i < 5; i++) { + bufferLogger.info(`Buffered info message ${i}`, { iteration: i }); + bufferLogger.warn(`Buffered warn message ${i}`, { iteration: i }); +} + +// Get all buffered messages +const bufferedMessages = bufferStream.getMessages(); +print(`Buffered ${bufferedMessages.length} messages:`); +for (const msg of bufferedMessages) { + print(` ${msg}`); +} + +// 12. Multi-stream with different formats +const multiFormatLogger = new Logger({ + processors: [addFullTimestamp(), addComputerId()], + renderer: (event) => "default", // This won't be used + streams: [ + // Console with human-readable format + { + write: (_, event) => { + const formatted = textRenderer(event); + new ConsoleStream().write(formatted, event); + }, + }, + // File with JSON format + { + write: (_, event) => { + const formatted = jsonRenderer(event); + new FileStream("structured.log").write(formatted, event); + }, + }, + ], +}); + +multiFormatLogger.info("Multi-format message", { + feature: "logging", + test: true, +}); + +// ============================================================================= +// Error Handling and Edge Cases +// ============================================================================= + +print("\n=== Error Handling Examples ==="); + +// 13. Robust error handling +const robustLogger = new Logger({ + processors: [ + addTimestamp(), + // Processor that might fail + (event) => { + try { + // Simulate potential failure + if (Math.random() > 0.8) { + throw new Error("Processor failed"); + } + event.set("processed", true); + return event; + } catch (error) { + // Log processor errors but don't break the chain + printError(`Processor error: ${String(error)}`); + event.set("processor_error", true); + return event; + } + }, + ], + renderer: textRenderer, + streams: [new ConsoleStream()], +}); + +// Log multiple messages to see error handling in action +for (let i = 0; i < 10; i++) { + robustLogger.info(`Message ${i}`, { attempt: i }); +} + +// ============================================================================= +// Cleanup Examples +// ============================================================================= + +print("\n=== Cleanup Examples ==="); + +// 14. Proper cleanup +const fileLogger = new Logger({ + processors: [addTimestamp()], + renderer: jsonRenderer, + streams: [new FileStream("temp.log")], +}); + +fileLogger.info("Temporary log entry"); + +// Clean shutdown - close all streams +fileLogger.close(); + +print("\n=== Examples Complete ==="); +print("Check the generated log files:"); +print("- app.log (daily rotation)"); +print("- custom.log (hourly rotation)"); +print("- all.log (complete log)"); +print("- debug.log (detailed debug info)"); +print("- structured.log (JSON format)"); +print("- temp.log (temporary file, now closed)"); + +// ============================================================================= +// Performance Comparison (commented out to avoid noise) +// ============================================================================= + +/* +print("\n=== Performance Comparison ==="); + +const iterations = 1000; + +// Test simple console logging +const startTime1 = os.clock(); +const simpleLogger = createMinimalLogger(); +for (let i = 0; i < iterations; i++) { + simpleLogger.info(`Simple message ${i}`); +} +const endTime1 = os.clock(); +print(`Simple Console Logger: ${endTime1 - startTime1} seconds`); + +// Test complex processor chain +const startTime2 = os.clock(); +const complexLogger = createDetailedLogger("perf_test.log", { + source: "PerfTest" +}); +for (let i = 0; i < iterations; i++) { + complexLogger.info(`Complex message ${i}`, { + iteration: i, + data: { nested: { value: i * 2 } } + }); +} +complexLogger.close(); +const endTime2 = os.clock(); +print(`Complex Processor Chain: ${endTime2 - startTime2} seconds`); +*/ diff --git a/src/test/testCcLog.ts b/src/test/testCcLog.ts deleted file mode 100644 index 802b096..0000000 --- a/src/test/testCcLog.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { CCLog, MINUTE, HOUR } from "@/lib/ccLog"; - -// Test the new time-based rotation functionality -function testTimeBasedRotation() { - print("Testing time-based log rotation functionality..."); - - // Test with default interval (1 day) - const logger1 = new CCLog("test_log_default.txt"); - logger1.info("This is a test message with default interval (1 day)"); - - // Test with custom interval (1 hour) - const logger2 = new CCLog("test_log_hourly.txt", { logInterval: HOUR }); - logger2.info("This is a test message with 1-hour interval"); - - // Test with custom interval (30 minutes) - const logger3 = new CCLog("test_log_30min.txt", { logInterval: 30 * MINUTE }); - logger3.info("This is a test message with 30-minute interval"); - - logger1.close(); - logger2.close(); - logger3.close(); - - print("Test completed successfully!"); -} - -export { testTimeBasedRotation }; diff --git a/targets/tsconfig.logExample.json b/targets/tsconfig.logExample.json new file mode 100644 index 0000000..d044970 --- /dev/null +++ b/targets/tsconfig.logExample.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://raw.githubusercontent.com/MCJack123/TypeScriptToLua/master/tsconfig-schema.json", + "extends": "../tsconfig.json", + "tstl": { + "luaBundle": "../build/logExample.lua", + "luaBundleEntry": "../src/logExample/main.ts" + }, + "include": ["../src/logExample/*.ts", "../src/lib/ccStructLog/*.ts"] +}