refactor(logging): restructure exports and consolidate processors

This commit is contained in:
2025-11-21 14:22:46 +08:00
parent 6d5cf11f2b
commit 3287661318
4 changed files with 216 additions and 382 deletions

View File

@@ -7,150 +7,14 @@
*/ */
// Re-export all core types and classes // Re-export all core types and classes
export { export * from "./types";
LogLevel, export * from "./Logger";
LogEvent,
Processor,
Renderer,
Stream,
LoggerOptions,
ILogger,
} from "./types";
export { Logger } from "./Logger";
// Re-export all processors // Re-export all processors
export { export * from "./processors";
addTimestamp,
addFormattedTimestamp,
addFullTimestamp,
filterByLevel,
addSource,
addComputerId,
addComputerLabel,
filterBy,
transformField,
removeFields,
addStaticFields,
} from "./processors";
// Re-export all renderers // Re-export all renderers
export { jsonRenderer, textRenderer } from "./renderers"; export * from "./renderers";
// Re-export all streams // Re-export all streams
export { export * from "./streams";
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,
});
}

View File

@@ -8,213 +8,196 @@
import { LogEvent, Processor, LogLevel } from "./types"; import { LogEvent, Processor, LogLevel } from "./types";
/** export namespace processor {
* Adds a timestamp to the log event. /**
* * Configuration options for the timestamp processor.
* This processor adds the current time as a structured timestamp object */
* using CC:Tweaked's os.date() function. The timestamp includes year, interface TimestampConfig {
* month, day, hour, minute, and second components. /**
* * The format string takes the same formats as C's strftime function.
* Performance note: os.date() is relatively expensive, so this should */
* typically be placed early in the processor chain and used only once. format?: string;
* }
* @param event - The log event to process
* @returns The event with timestamp added /**
*/ * Adds a timestamp to each log event.
export function addTimestamp(): Processor { *
return (event) => { * This processor adds a "time" field to each log event with the current
const timestamp = os.date("!*t") as LuaDate; * timestamp. The timestamp format can be customized using the `format`
event.set("timestamp", timestamp); * option.
return event; *
}; * @param config - Configuration options for the timestamp processor.
} * @returns A processor function that adds a timestamp to each log event.
*/
/** export function addTimestamp(config: TimestampConfig = {}): Processor {
* Adds a human-readable timestamp string to the log event. return (event) => {
* let time: string;
* This processor adds a formatted timestamp string that's easier to read if (config.format === undefined) {
* in log output. The format is "HH:MM:SS" in UTC time. time = os.date("%F %T") as string;
* } else {
* @param event - The log event to process time = os.date(config.format) as string;
* @returns The event with formatted timestamp added }
*/
export function addFormattedTimestamp(): Processor { event.set("timestamp", time);
return (event) => { 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; /**
}; * 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
* Adds a full ISO-like timestamp string to the log event. * this processor can be useful for dynamic filtering or when you need
* * different levels for different streams.
* This processor adds a complete timestamp in YYYY-MM-DD HH:MM:SS format *
* which is useful for file logging and structured output. * @param minLevel - The minimum log level to allow through
* * @returns A processor function that filters by level
* @param event - The log event to process */
* @returns The event with full timestamp added export function filterByLevel(minLevel: LogLevel): Processor {
*/ return (event) => {
export function addFullTimestamp(): Processor { const eventLevel = event.get("level") as LogLevel | undefined;
return (event) => { if (eventLevel === undefined) {
const timestamp = os.date("!*t") as LuaDate; return event; // Pass through if no level is set
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; if (eventLevel !== undefined && eventLevel < minLevel) {
}; return undefined; // Drop the log event
} }
/** 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 * Adds a logger name/source to the log event.
* different levels for different streams. *
* * This processor is useful when you have multiple loggers in your application
* @param minLevel - The minimum log level to allow through * and want to identify which component generated each log entry.
* @returns A processor function that filters by level *
*/ * @param name - The name/source to add to log events
export function filterByLevel(minLevel: LogLevel): Processor { * @returns A processor function that adds the source name
return (event) => { */
const eventLevel = event.get("level") as LogLevel | undefined; export function addSource(name: string): Processor {
if (eventLevel === undefined) { return (event) => {
return event; // Pass through if no level is set event.set("source", name);
} return event;
};
if (eventLevel !== undefined && eventLevel < minLevel) { }
return undefined; // Drop the log event
} /**
* Adds the current computer ID to the log event.
return 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
* Adds a logger name/source to the log event. * @returns The event with computer ID added
* */
* This processor is useful when you have multiple loggers in your application export function addComputerId(): Processor {
* and want to identify which component generated each log entry. return (event) => {
* event.set("computer_id", os.getComputerID());
* @param name - The name/source to add to log events return event;
* @returns A processor function that adds the source name };
*/ }
export function addSource(name: string): Processor {
return (event) => { /**
event.set("source", name); * Adds the current computer label to the log event.
return 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
* Adds the current computer ID to the log event. * @returns The event with computer label added (if available)
* */
* In CC:Tweaked environments, this can help identify which computer export function addComputerLabel(): Processor {
* generated the log when logs are aggregated from multiple sources. return (event) => {
* const label = os.getComputerLabel();
* @param event - The log event to process if (label !== undefined && label !== null) {
* @returns The event with computer ID added event.set("computer_label", label);
*/ }
export function addComputerId(): Processor { return event;
return (event) => { };
event.set("computer_id", os.getComputerID()); }
return event;
}; /**
} * Filters out events that match a specific condition.
*
/** * This is a generic processor that allows you to filter events based on
* Adds the current computer label to the log event. * any custom condition. The predicate function should return true to keep
* * the event and false to drop it.
* 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 predicate - Function that returns true to keep the event
* * @returns A processor function that filters based on the predicate
* @param event - The log event to process */
* @returns The event with computer label added (if available) export function filterBy(
*/ predicate: (event: LogEvent) => boolean,
export function addComputerLabel(): Processor { ): Processor {
return (event) => { return (event) => {
const label = os.getComputerLabel(); return predicate(event) ? event : undefined;
if (label !== undefined && label !== null) { };
event.set("computer_label", label); }
}
return event; /**
}; * Transforms a specific field in the log event.
} *
* This processor allows you to modify the value of a specific field
/** * using a transformation function.
* Filters out events that match a specific condition. *
* * @param fieldName - The name of the field to transform
* This is a generic processor that allows you to filter events based on * @param transformer - Function to transform the field value
* any custom condition. The predicate function should return true to keep * @returns A processor function that transforms the specified field
* the event and false to drop it. */
* export function transformField(
* @param predicate - Function that returns true to keep the event fieldName: string,
* @returns A processor function that filters based on the predicate transformer: (value: unknown) => unknown,
*/ ): Processor {
export function filterBy(predicate: (event: LogEvent) => boolean): Processor { return (event) => {
return (event) => { if (event.has(fieldName)) {
return predicate(event) ? event : undefined; const currentValue = event.get(fieldName);
}; const newValue = transformer(currentValue);
} event.set(fieldName, newValue);
}
/** return event;
* Transforms a specific field in the log event. };
* }
* This processor allows you to modify the value of a specific field
* using a transformation function. /**
* * Removes specified fields from the log event.
* @param fieldName - The name of the field to transform *
* @param transformer - Function to transform the field value * This processor can be used to strip sensitive or unnecessary information
* @returns A processor function that transforms the specified field * from log events before they are rendered and output.
*/ *
export function transformField( * @param fieldNames - Array of field names to remove
fieldName: string, * @returns A processor function that removes the specified fields
transformer: (value: unknown) => unknown, */
): Processor { export function removeFields(fieldNames: string[]): Processor {
return (event) => { return (event) => {
if (event.has(fieldName)) { for (const fieldName of fieldNames) {
const currentValue = event.get(fieldName); event.delete(fieldName);
const newValue = transformer(currentValue); }
event.set(fieldName, newValue); return event;
} };
return event; }
};
} /**
* Adds static fields to every log event.
/** *
* Removes specified fields from the log event. * This processor adds the same set of fields to every log event that
* * passes through it. Useful for adding application name, version,
* This processor can be used to strip sensitive or unnecessary information * environment, etc.
* from log events before they are rendered and output. *
* * @param fields - Object containing the static fields to add
* @param fieldNames - Array of field names to remove * @returns A processor function that adds the static fields
* @returns A processor function that removes the specified fields */
*/ export function addStaticFields(
export function removeFields(fieldNames: string[]): Processor { fields: Record<string, unknown>,
return (event) => { ): Processor {
for (const fieldName of fieldNames) { return (event) => {
event.delete(fieldName); for (const [key, value] of Object.entries(fields)) {
} event.set(key, value);
return event; }
}; 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;
};
} }

View File

@@ -6,7 +6,7 @@
* different output formats (JSON, console-friendly, etc.). * different output formats (JSON, console-friendly, etc.).
*/ */
import { Renderer } from "./types"; import { LogLevel, Renderer } from "./types";
/** /**
* Renders log events as JSON strings. * Renders log events as JSON strings.
@@ -43,44 +43,30 @@ export const jsonRenderer: Renderer = (event) => {
* timestamp, level, message, and additional context fields formatted * timestamp, level, message, and additional context fields formatted
* in a readable way. * in a readable way.
* *
* Format: [HH:MM:SS] [LEVEL] message { key=value, key2=value2 } * Format: [YYYY-MM-DD HH:MM:SS] [LEVEL] message key=value, key2=value2
* *
* @param event - The log event to render * @param event - The log event to render
* @returns Human-readable string representation * @returns Human-readable string representation
*/ */
export const textRenderer: Renderer = (event) => { export const textRenderer: Renderer = (event) => {
// Extract core components // Extract core components
const timestamp = event.get("timestamp") as LuaDate | undefined; const timeStr = event.get("timestamp") as string | undefined;
const timeStr = event.get("time") as string | undefined; const level: string | undefined = LogLevel[event.get("level") as LogLevel];
const level = (event.get("level") as string)?.toUpperCase() ?? "UNKNOWN";
const message = (event.get("message") as string) ?? ""; 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 // Start building the output
let output = `[${timestampStr}] [${level}] ${message}`; let output = `[${timeStr}] [${level}] ${message} \t`;
// Add context fields (excluding the core fields we already used) // Add context fields (excluding the core fields we already used)
const contextFields: string[] = []; const contextFields: string[] = [];
for (const [key, value] of event.entries()) { for (const [key, value] of event.entries()) {
if ( if (key !== "timestamp" && key !== "level" && key !== "message") {
key !== "timestamp" &&
key !== "time" &&
key !== "level" &&
key !== "message"
) {
contextFields.push(`${key}=${tostring(value)}`); contextFields.push(`${key}=${tostring(value)}`);
} }
} }
if (contextFields.length > 0) { if (contextFields.length > 0) {
output += ` { ${contextFields.join(", ")} }`; output += contextFields.join(", ");
} }
return output; return output;

View File

@@ -6,7 +6,7 @@
* implements the Stream interface and handles its own output logic. * implements the Stream interface and handles its own output logic.
*/ */
import { Stream, LogEvent } from "./types"; import { LogLevel, Stream, LogEvent } from "./types";
/** /**
* Console stream that outputs to the CC:Tweaked terminal. * Console stream that outputs to the CC:Tweaked terminal.
@@ -16,14 +16,14 @@ import { Stream, LogEvent } from "./types";
* color after writing each message. * color after writing each message.
*/ */
export class ConsoleStream implements Stream { export class ConsoleStream implements Stream {
private levelColors: Map<string, number> = new Map([ private levelColors: { [key: string]: number } = {
["trace", colors.lightGray], Trace: colors.lightGray,
["debug", colors.gray], Debug: colors.gray,
["info", colors.white], Info: colors.green,
["warn", colors.orange], Warn: colors.orange,
["error", colors.red], Error: colors.red,
["fatal", colors.red], Fatal: colors.red,
]); };
/** /**
* Write a formatted log message to the terminal. * Write a formatted log message to the terminal.
@@ -32,8 +32,9 @@ export class ConsoleStream implements Stream {
* @param event - The original log event for context (used for level-based coloring) * @param event - The original log event for context (used for level-based coloring)
*/ */
public write(message: string, event: LogEvent): void { public write(message: string, event: LogEvent): void {
const level = event.get("level") as string | undefined; const level: string | undefined =
const color = level ? this.levelColors.get(level) : undefined; LogLevel[event.get("level") as LogLevel];
const color = level !== undefined ? this.levelColors[level] : undefined;
if (color !== undefined) { if (color !== undefined) {
const originalColor = term.getTextColor(); const originalColor = term.getTextColor();