try fix for windows, change how logging works allows much more flexibility
All checks were successful
Code quality checks / biome (push) Successful in 9s

This commit is contained in:
creations 2025-06-11 18:24:48 -04:00
parent 15d4978ac5
commit f313a8a329
Signed by: creations
GPG key ID: 8F553AA4320FC711
6 changed files with 297 additions and 51 deletions

178
README.md
View file

@ -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,7 +208,7 @@ 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()` |
@ -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",
"data": {
"line": "8",
"column": "6",
"data": [
"Database error:",
{
"name": "Error",
"message": "Something failed",
"stack": "Error: Something failed\n at index.ts:10:12"
"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": {
"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 <ref *1> { 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:

View file

@ -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"
},

View file

@ -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 {

View file

@ -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<LoggerConfig>): {
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<LoggerConfig>): {
};
}
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<LoggerConfig>): {
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<string, unknown> = {};
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<LoggerConfig>,
): 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);
}

View file

@ -1,5 +1,6 @@
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import type { LogLevel, LoggerConfig } from "@types";
const logLevelValues = {

View file

@ -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 = "";