mirror of
https://github.com/SikongJueluo/cc-utils.git
synced 2025-11-29 12:57:50 +08:00
feat(logging): implement structured logging system with ccStructLog
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,7 +5,7 @@ build/
|
||||
reference/
|
||||
src/**/*.md
|
||||
|
||||
QWEN.md
|
||||
.ai/
|
||||
|
||||
# Devenv
|
||||
.devenv*
|
||||
|
||||
@@ -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 }}"
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
281
docs/ccStructLog.md
Normal file
281
docs/ccStructLog.md
Normal file
@@ -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<string, unknown>`) 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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
183
src/lib/ccLog.ts
183
src/lib/ccLog.ts
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
165
src/lib/ccStructLog/Logger.ts
Normal file
165
src/lib/ccStructLog/Logger.ts
Normal file
@@ -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<LoggerOptions>) {
|
||||
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<string, unknown> = {},
|
||||
): void {
|
||||
// 1. Create initial LogEvent with core fields
|
||||
let event: LogEvent | undefined = new Map<string, unknown>([
|
||||
["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<string, unknown>): 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<string, unknown>): 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<string, unknown>): 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<string, unknown>): 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<string, unknown>): 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<string, unknown>): 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<LoggerOptions>): 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
156
src/lib/ccStructLog/index.ts
Normal file
156
src/lib/ccStructLog/index.ts
Normal file
@@ -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<ConsoleStream | FileStream> = [
|
||||
new FileStream(filename, options.rotationInterval ?? DAY),
|
||||
];
|
||||
|
||||
if (options.includeConsole) {
|
||||
streams.push(new ConsoleStream());
|
||||
}
|
||||
|
||||
return new Logger({
|
||||
processors,
|
||||
renderer: jsonRenderer,
|
||||
streams,
|
||||
});
|
||||
}
|
||||
220
src/lib/ccStructLog/processors.ts
Normal file
220
src/lib/ccStructLog/processors.ts
Normal file
@@ -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<string, unknown>): Processor {
|
||||
return (event) => {
|
||||
for (const [key, value] of Object.entries(fields)) {
|
||||
event.set(key, value);
|
||||
}
|
||||
return event;
|
||||
};
|
||||
}
|
||||
87
src/lib/ccStructLog/renderers.ts
Normal file
87
src/lib/ccStructLog/renderers.ts
Normal file
@@ -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<string, unknown> = {};
|
||||
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;
|
||||
};
|
||||
308
src/lib/ccStructLog/streams.ts
Normal file
308
src/lib/ccStructLog/streams.ts
Normal file
@@ -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<string, number> = 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;
|
||||
107
src/lib/ccStructLog/types.ts
Normal file
107
src/lib/ccStructLog/types.ts
Normal file
@@ -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<string, unknown>;
|
||||
|
||||
/**
|
||||
* 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<string, unknown>,
|
||||
): void;
|
||||
|
||||
/** Log at trace level */
|
||||
trace(message: string, context?: Record<string, unknown>): void;
|
||||
|
||||
/** Log at debug level */
|
||||
debug(message: string, context?: Record<string, unknown>): void;
|
||||
|
||||
/** Log at info level */
|
||||
info(message: string, context?: Record<string, unknown>): void;
|
||||
|
||||
/** Log at warn level */
|
||||
warn(message: string, context?: Record<string, unknown>): void;
|
||||
|
||||
/** Log at error level */
|
||||
error(message: string, context?: Record<string, unknown>): void;
|
||||
|
||||
/** Log at fatal level */
|
||||
fatal(message: string, context?: Record<string, unknown>): void;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
class ccDate {
|
||||
export class ccDate {
|
||||
private _timestamp: number;
|
||||
|
||||
constructor() {
|
||||
@@ -21,5 +21,3 @@ class ccDate {
|
||||
return this._timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
export { ccDate };
|
||||
|
||||
285
src/logExample/main.ts
Normal file
285
src/logExample/main.ts
Normal file
@@ -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<string, unknown>) => {
|
||||
event.set("requestId", `req_${Math.random().toString(36).substr(2, 9)}`);
|
||||
return event;
|
||||
};
|
||||
|
||||
const sanitizePasswords = (event: Map<string, unknown>) => {
|
||||
// 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`);
|
||||
*/
|
||||
@@ -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 };
|
||||
9
targets/tsconfig.logExample.json
Normal file
9
targets/tsconfig.logExample.json
Normal file
@@ -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"]
|
||||
}
|
||||
Reference in New Issue
Block a user