From f313a8a329b345d647060a47bc34b3ec44c27252 Mon Sep 17 00:00:00 2001 From: creations Date: Wed, 11 Jun 2025 18:24:48 -0400 Subject: [PATCH] try fix for windows, change how logging works allows much more flexibility --- README.md | 180 ++++++++++++++++++++++++++++++++++++++++------ package.json | 2 +- src/index.ts | 31 ++++---- src/lib/char.ts | 131 ++++++++++++++++++++++++++++++--- src/lib/config.ts | 1 + src/lib/file.ts | 3 +- 6 files changed, 297 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 05c9160..f160dfc 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,18 @@ # @atums/echo -A minimal, flexible logger for Node with: - -- Colored console output -- Daily `.jsonl` file logging -- Configurable output patterns and file naming -- Structured logs with caller metadata -- Fully typed config with environment/file/constructor override - ---- +A minimal, flexible logger for Node ## Features - Console and file logging with level-based filtering +- Multiple arguments per log call with automatic formatting - Colored output with ANSI formatting - Daily rotated `.jsonl` files with custom naming patterns - Supports runtime configuration merging - Auto-formatted output using custom patterns - Includes caller file, line, and column - Pretty-prints structured objects if enabled +- Safe handling of circular references and complex objects - Flushes open file streams on exit - Uses Biome and EditorConfig for formatting and linting @@ -34,19 +28,74 @@ bun add @atums/echo ## Usage +### Basic Logging + ```ts import { echo } from "@atums/echo"; +// Single arguments echo.info("App started"); echo.debug({ state: "init", ok: true }); +// Multiple arguments - all joined with spaces in console +echo.info("User login:", { userId: 123, ip: "192.168.1.1" }); +echo.warn("Rate limit exceeded:", 429, { endpoint: "/api/users" }); +echo.error("Database error:", error, { query: "SELECT * FROM users" }); +``` + +### Error Handling + +```ts try { throw new Error("Something failed"); } catch (err) { + // Single error echo.error(err); + + // Error with context + echo.error("Operation failed:", err, { userId: 123, operation: "login" }); + + // Multiple context items + echo.fatal("Critical error:", err, "System shutting down", { timestamp: Date.now() }); } ``` +### Multiple Data Types + +```ts +// Mix any data types +echo.info("Processing:", 42, true, { batch: "A1" }, ["item1", "item2"]); +echo.debug("State:", "active", { connections: 5 }, null, undefined); +echo.warn("Alert:", "High CPU usage:", 95.2, "%", { threshold: 80 }); +``` + +### API Request Logging + +```ts +// Custom tagged logs for HTTP requests +echo.custom("GET", "/api/users", { status: 200, duration: "15ms" }); +echo.custom("POST", "/api/auth", { status: 401, error: "Invalid token" }); + +// Standard logs with request context +echo.info("API Request:", "GET /health", { status: 200, responseTime: "5ms" }); +echo.error("API Error:", "POST /users", 500, { error: "Database timeout" }); +``` + +--- + +## Log Levels + +All log levels support multiple arguments: + +```ts +echo.trace("Trace message:", data1, data2); +echo.debug("Debug info:", object, array, "string"); +echo.info("Information:", value1, value2, value3); +echo.warn("Warning:", message, errorCode, context); +echo.error("Error occurred:", error, additionalData); +echo.fatal("Fatal error:", error, "system", "shutdown"); +``` + --- ## Configuration @@ -159,10 +208,10 @@ These tokens are replaced in the log pattern: | `{file-name}` | Source filename | | `{line}` | Line number in source | | `{column}` | Column number in source | -| `{data}` | Formatted log data (message/object) | +| `{data}` | Formatted log data | | `{id}` | Unique short ID for the log | | `{tag}` | Custom tag used in `echo.custom()` | -| `{context}` | Custom context in `echo.custom()` | +| `{context}` | Custom context in `echo.custom()` | | `{color:*}` | ANSI color start (e.g. `{color:red}`) | | `{color:levelColor}` | Dynamic color based on log level | | `{color:tagColor}` | Color for custom tag | @@ -177,6 +226,8 @@ You can log arbitrary tagged messages with `echo.custom(tag, context, message)`: ```ts echo.custom("GET", "/health", { status: 200 }); +echo.custom("WEBHOOK", "payment_received", { amount: 99.99, userId: "abc123" }); +echo.custom("CRON", "daily_backup", { files: 1420, duration: "2m 15s" }); ``` The output format is controlled by: @@ -188,22 +239,39 @@ The output format is controlled by: ``` 2025-05-24 16:22:00.123 [GET] (/health) { status: 200 } +2025-05-24 16:22:01.456 [WEBHOOK] (payment_received) { amount: 99.99, userId: "abc123" } ``` --- ## Output Examples -### Console +### Console Output +**Single argument:** ``` 2025-05-24 16:15:00.000 [INFO] (index.ts:3:6) Server started ``` -### File (`logs/2025-05-24.jsonl`) +**Multiple arguments:** +``` +2025-05-24 16:15:01.123 [ERROR] (index.ts:8:6) Database error: Error: Connection timeout { + query: 'SELECT * FROM users', + duration: '5s' +} +``` -Each line is structured JSON: +**Mixed data types:** +``` +2025-05-24 16:15:02.456 [WARN] (index.ts:12:6) Rate limit: 429 exceeded for /api/users { + ip: '192.168.1.1', + attempts: 15 +} +``` +### File Output (`logs/2025-05-24.jsonl`) + +**Single argument JSON:** ```json { "timestamp": 1748115300000, @@ -212,30 +280,96 @@ Each line is structured JSON: "file": "index.ts", "line": "3", "column": "6", - "data": "Server started" + "data": ["Server started"] } ``` -If an error is logged: - +**Multiple arguments JSON:** ```json { - "timestamp": 1748115301000, + "timestamp": 1748115301123, "level": "error", "id": "qW3eR7tU", "file": "index.ts", - "line": "10", - "column": "12", + "line": "8", + "column": "6", + "data": [ + "Database error:", + { + "name": "Error", + "message": "Connection timeout", + "stack": "Error: Connection timeout\n at index.ts:8:6" + }, + { + "query": "SELECT * FROM users", + "duration": "5s" + } + ] +} +``` + +**Custom log JSON:** +```json +{ + "timestamp": 1748115302456, + "level": "GET", + "id": "mN8oP2qR", + "file": "index.ts", + "line": "15", + "column": "6", "data": { - "name": "Error", - "message": "Something failed", - "stack": "Error: Something failed\n at index.ts:10:12" + "context": "/health", + "data": { "status": 200 } } } ``` --- +## Advanced Features + +### Circular Reference Handling + +The logger safely handles circular references without crashing: + +```ts +const obj = { name: "test" }; +obj.self = obj; // Creates circular reference + +echo.info("Circular object:", obj); // Works safely +// Console: Shows { name: 'test', self: [Circular *1] } +// File: Stores { "name": "test", "self": "[Circular Reference]" } +``` + +### Error Object Serialization + +Error objects are automatically converted to structured data: + +```ts +const error = new Error("Something failed"); +echo.error("Operation failed:", error, { userId: 123 }); + +// File output includes: +// { +// "name": "Error", +// "message": "Something failed", +// "stack": "Error: Something failed\n at ..." +// } +``` + +### Performance Considerations + +The logger handles rapid logging efficiently: + +```ts +for (let i = 0; i < 1000; i++) { + echo.debug("Processing item:", i, { batch: "A1", progress: i/1000 }); +} +// All logs are processed without blocking +``` + +--- + ## Development This project uses: diff --git a/package.json b/package.json index b489159..4aaebbb 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "license": "BSD-3-Clause", "devDependencies": { "@biomejs/biome": "^1.9.4", - "@types/bun": "^1.2.13", + "@types/bun": "^1.2.15", "tsup": "^8.5.0", "typescript": "^5.8.3" }, diff --git a/src/index.ts b/src/index.ts index 37abdab..597025a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -77,7 +77,7 @@ class Echo { accessSync(dir, constants.W_OK); } - private log(level: LogLevel, data: unknown): void { + private log(level: LogLevel, ...args: unknown[]): void { if ( this.config.silent || logLevelValues[this.config.level] > logLevelValues[level] @@ -85,39 +85,38 @@ class Echo { return; const meta = getCallerInfo(this.config); - const line = parsePattern({ level, data, config: this.config }); + const line = parsePattern({ level, data: args, config: this.config }); if (this.config.console) { console[getConsoleMethod(level)](line); } - if (!this.config.disableFile && this.fileLogger) { - this.fileLogger.write(level, data, meta); + this.fileLogger.write(level, args, meta); } } - public debug(data: unknown): void { - this.log("debug", data); + public debug(...args: unknown[]): void { + this.log("debug", ...args); } - public info(data: unknown): void { - this.log("info", data); + public info(...args: unknown[]): void { + this.log("info", ...args); } - public warn(data: unknown): void { - this.log("warn", data); + public warn(...args: unknown[]): void { + this.log("warn", ...args); } - public error(data: unknown): void { - this.log("error", data); + public error(...args: unknown[]): void { + this.log("error", ...args); } - public fatal(data: unknown): void { - this.log("fatal", data); + public fatal(...args: unknown[]): void { + this.log("fatal", ...args); } - public trace(data: unknown): void { - this.log("trace", data); + public trace(...args: unknown[]): void { + this.log("trace", ...args); } public custom(tag: string, context: string, data: unknown): void { diff --git a/src/lib/char.ts b/src/lib/char.ts index 050b2c7..cd6f474 100644 --- a/src/lib/char.ts +++ b/src/lib/char.ts @@ -1,8 +1,8 @@ import { basename } from "node:path"; import { format, inspect } from "node:util"; +import { ansiColors, defaultLevelColor, logLevelValues } from "@lib/config"; import { format as formatDate } from "date-fns-tz"; -import { ansiColors, defaultLevelColor, logLevelValues } from "@lib/config"; import type { LogLevel, LogLevelValue, @@ -59,6 +59,7 @@ function getCallerInfo(config: Required): { for (let i = 1; i < lines.length; i++) { const line = lines[i].trim(); + // try file:// URLs first (works on all platforms) const fileURLMatch = line.match( /at\s+(?:.*\()?file:\/\/(.*):(\d+):(\d+)\)?/, ); @@ -79,16 +80,16 @@ function getCallerInfo(config: Required): { }; } - const rawMatch = line.match(/at\s+(?:.*\()?(.+):(\d+):(\d+)\)?/); - if (rawMatch) { - const [_, fullPath, lineNumber, columnNumber] = rawMatch; + const pathMatch = parseStackTracePath(line); + if (pathMatch) { + const { fullPath, lineNumber, columnNumber } = pathMatch; const isInternal = fullPath.includes("atums.echo") || fullPath.includes("@atums/echo"); if (isInternal) continue; return { - id: id, + id, fileName: basename(fullPath), line: lineNumber, column: columnNumber, @@ -101,6 +102,59 @@ function getCallerInfo(config: Required): { return fallback; } +function parseStackTracePath(line: string): { + fullPath: string; + lineNumber: string; + columnNumber: string; +} | null { + // remove "at " prefix and trim + const cleanLine = line.replace(/^\s*at\s+/, "").trim(); + + let pathPart: string; + + // extract path from parentheses if present + const parenMatch = cleanLine.match(/\(([^)]+)\)$/); + if (parenMatch) { + pathPart = parenMatch[1]; + } else { + pathPart = cleanLine; + } + + let match: RegExpMatchArray | null = null; + + if (process.platform === "win32") { + // windows-specific parsing + // handle drive letters (C:) vs line numbers (:10:5) + match = pathPart.match( + /^((?:[a-zA-Z]:|\\\\[^\\]+\\[^\\]+|[^:]+)):(\d+):(\d+)$/, + ); + + if (!match) { + // try alternative windows format with forward slashes + match = pathPart.match(/^([a-zA-Z]:[^:]+):(\d+):(\d+)$/); + } + + if (!match) { + // try UNC path format + match = pathPart.match(/^(\\\\[^:]+):(\d+):(\d+)$/); + } + } else { + // unix-like systems (Linux, macOS) + match = pathPart.match(/^([^:]+):(\d+):(\d+)$/); + } + + if (match) { + const [_, fullPath, lineNumber, columnNumber] = match; + return { + fullPath: fullPath.trim(), + lineNumber, + columnNumber, + }; + } + + return null; +} + function generateShortId(length = 8): string { const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; @@ -166,13 +220,63 @@ function serializeLogData(data: unknown): unknown { }; } - if (typeof data === "string" || typeof data === "number") { + if ( + typeof data === "string" || + typeof data === "number" || + typeof data === "boolean" || + data === null || + data === undefined + ) { return data; } + if (typeof data === "object") { + try { + return JSON.parse(JSON.stringify(data)); + } catch (err) { + if ( + (err instanceof TypeError && err.message.includes("circular")) || + (err instanceof Error && err.message.includes("cyclic")) + ) { + return createSafeObjectRepresentation(data); + } + return String(data); + } + } + return data; } +function createSafeObjectRepresentation( + obj: unknown, + seen = new WeakSet(), +): unknown { + if (obj === null || typeof obj !== "object") { + return obj; + } + + if (seen.has(obj as object)) { + return "[Circular Reference]"; + } + + seen.add(obj as object); + + if (Array.isArray(obj)) { + return obj.map((item) => createSafeObjectRepresentation(item, seen)); + } + + const result: Record = {}; + for (const [key, value] of Object.entries(obj)) { + try { + result[key] = createSafeObjectRepresentation(value, seen); + } catch { + result[key] = "[Unserializable]"; + } + } + + return result; +} + function processPattern( pattern: string, tokens: PatternTokens, @@ -231,15 +335,23 @@ function processPattern( return processed; } +function formatMultipleData( + dataArray: unknown[], + config: Required, +): string { + return dataArray.map((item) => formatData(item, config)).join(" "); +} + function parsePattern(ctx: PatternContext): string { const { level, data, config } = ctx; - const { id, fileName, line, column, timestamp, prettyTimestamp } = getCallerInfo(config); - const resolvedData: string = formatData(data, config); - const numericLevel: LogLevelValue = logLevelValues[level]; + const resolvedData: string = Array.isArray(data) + ? formatMultipleData(data, config) + : formatData(data, config); + const numericLevel: LogLevelValue = logLevelValues[level]; const tokens: PatternTokens = { timestamp, prettyTimestamp, @@ -251,7 +363,6 @@ function parsePattern(ctx: PatternContext): string { data: resolvedData, id, }; - return processPattern(config.pattern, tokens, config, level); } diff --git a/src/lib/config.ts b/src/lib/config.ts index e5a9f0f..cdaff77 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -1,5 +1,6 @@ import { readFileSync } from "node:fs"; import { resolve } from "node:path"; + import type { LogLevel, LoggerConfig } from "@types"; const logLevelValues = { diff --git a/src/lib/file.ts b/src/lib/file.ts index 6f1d70c..faec16d 100644 --- a/src/lib/file.ts +++ b/src/lib/file.ts @@ -8,9 +8,10 @@ import { } from "node:fs"; import { join } from "node:path"; import { serializeLogData } from "@lib/char"; -import type { LogLevel, LoggerConfig } from "@types"; import { format } from "date-fns-tz"; +import type { LogLevel, LoggerConfig } from "@types"; + class FileLogger { private stream: WriteStream | null = null; private filePath = "";