try fix for windows, change how logging works allows much more flexibility
All checks were successful
Code quality checks / biome (push) Successful in 9s
All checks were successful
Code quality checks / biome (push) Successful in 9s
This commit is contained in:
parent
15d4978ac5
commit
f313a8a329
6 changed files with 297 additions and 51 deletions
180
README.md
180
README.md
|
@ -1,24 +1,18 @@
|
||||||
# @atums/echo
|
# @atums/echo
|
||||||
|
|
||||||
A minimal, flexible logger for Node with:
|
A minimal, flexible logger for Node
|
||||||
|
|
||||||
- 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
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Console and file logging with level-based filtering
|
- Console and file logging with level-based filtering
|
||||||
|
- Multiple arguments per log call with automatic formatting
|
||||||
- Colored output with ANSI formatting
|
- Colored output with ANSI formatting
|
||||||
- Daily rotated `.jsonl` files with custom naming patterns
|
- Daily rotated `.jsonl` files with custom naming patterns
|
||||||
- Supports runtime configuration merging
|
- Supports runtime configuration merging
|
||||||
- Auto-formatted output using custom patterns
|
- Auto-formatted output using custom patterns
|
||||||
- Includes caller file, line, and column
|
- Includes caller file, line, and column
|
||||||
- Pretty-prints structured objects if enabled
|
- Pretty-prints structured objects if enabled
|
||||||
|
- Safe handling of circular references and complex objects
|
||||||
- Flushes open file streams on exit
|
- Flushes open file streams on exit
|
||||||
- Uses Biome and EditorConfig for formatting and linting
|
- Uses Biome and EditorConfig for formatting and linting
|
||||||
|
|
||||||
|
@ -34,19 +28,74 @@ bun add @atums/echo
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
### Basic Logging
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { echo } from "@atums/echo";
|
import { echo } from "@atums/echo";
|
||||||
|
|
||||||
|
// Single arguments
|
||||||
echo.info("App started");
|
echo.info("App started");
|
||||||
echo.debug({ state: "init", ok: true });
|
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 {
|
try {
|
||||||
throw new Error("Something failed");
|
throw new Error("Something failed");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// Single error
|
||||||
echo.error(err);
|
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
|
## Configuration
|
||||||
|
@ -159,10 +208,10 @@ These tokens are replaced in the log pattern:
|
||||||
| `{file-name}` | Source filename |
|
| `{file-name}` | Source filename |
|
||||||
| `{line}` | Line number in source |
|
| `{line}` | Line number in source |
|
||||||
| `{column}` | Column 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 |
|
| `{id}` | Unique short ID for the log |
|
||||||
| `{tag}` | Custom tag used in `echo.custom()` |
|
| `{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:*}` | ANSI color start (e.g. `{color:red}`) |
|
||||||
| `{color:levelColor}` | Dynamic color based on log level |
|
| `{color:levelColor}` | Dynamic color based on log level |
|
||||||
| `{color:tagColor}` | Color for custom tag |
|
| `{color:tagColor}` | Color for custom tag |
|
||||||
|
@ -177,6 +226,8 @@ You can log arbitrary tagged messages with `echo.custom(tag, context, message)`:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
echo.custom("GET", "/health", { status: 200 });
|
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:
|
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:00.123 [GET] (/health) { status: 200 }
|
||||||
|
2025-05-24 16:22:01.456 [WEBHOOK] (payment_received) { amount: 99.99, userId: "abc123" }
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Output Examples
|
## Output Examples
|
||||||
|
|
||||||
### Console
|
### Console Output
|
||||||
|
|
||||||
|
**Single argument:**
|
||||||
```
|
```
|
||||||
2025-05-24 16:15:00.000 [INFO] (index.ts:3:6) Server started
|
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
|
```json
|
||||||
{
|
{
|
||||||
"timestamp": 1748115300000,
|
"timestamp": 1748115300000,
|
||||||
|
@ -212,30 +280,96 @@ Each line is structured JSON:
|
||||||
"file": "index.ts",
|
"file": "index.ts",
|
||||||
"line": "3",
|
"line": "3",
|
||||||
"column": "6",
|
"column": "6",
|
||||||
"data": "Server started"
|
"data": ["Server started"]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
If an error is logged:
|
**Multiple arguments JSON:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"timestamp": 1748115301000,
|
"timestamp": 1748115301123,
|
||||||
"level": "error",
|
"level": "error",
|
||||||
"id": "qW3eR7tU",
|
"id": "qW3eR7tU",
|
||||||
"file": "index.ts",
|
"file": "index.ts",
|
||||||
"line": "10",
|
"line": "8",
|
||||||
"column": "12",
|
"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": {
|
"data": {
|
||||||
"name": "Error",
|
"context": "/health",
|
||||||
"message": "Something failed",
|
"data": { "status": 200 }
|
||||||
"stack": "Error: Something failed\n at index.ts:10:12"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 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
|
## Development
|
||||||
|
|
||||||
This project uses:
|
This project uses:
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^1.9.4",
|
"@biomejs/biome": "^1.9.4",
|
||||||
"@types/bun": "^1.2.13",
|
"@types/bun": "^1.2.15",
|
||||||
"tsup": "^8.5.0",
|
"tsup": "^8.5.0",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3"
|
||||||
},
|
},
|
||||||
|
|
31
src/index.ts
31
src/index.ts
|
@ -77,7 +77,7 @@ class Echo {
|
||||||
accessSync(dir, constants.W_OK);
|
accessSync(dir, constants.W_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
private log(level: LogLevel, data: unknown): void {
|
private log(level: LogLevel, ...args: unknown[]): void {
|
||||||
if (
|
if (
|
||||||
this.config.silent ||
|
this.config.silent ||
|
||||||
logLevelValues[this.config.level] > logLevelValues[level]
|
logLevelValues[this.config.level] > logLevelValues[level]
|
||||||
|
@ -85,39 +85,38 @@ class Echo {
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const meta = getCallerInfo(this.config);
|
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) {
|
if (this.config.console) {
|
||||||
console[getConsoleMethod(level)](line);
|
console[getConsoleMethod(level)](line);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.config.disableFile && this.fileLogger) {
|
if (!this.config.disableFile && this.fileLogger) {
|
||||||
this.fileLogger.write(level, data, meta);
|
this.fileLogger.write(level, args, meta);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public debug(data: unknown): void {
|
public debug(...args: unknown[]): void {
|
||||||
this.log("debug", data);
|
this.log("debug", ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
public info(data: unknown): void {
|
public info(...args: unknown[]): void {
|
||||||
this.log("info", data);
|
this.log("info", ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
public warn(data: unknown): void {
|
public warn(...args: unknown[]): void {
|
||||||
this.log("warn", data);
|
this.log("warn", ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
public error(data: unknown): void {
|
public error(...args: unknown[]): void {
|
||||||
this.log("error", data);
|
this.log("error", ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
public fatal(data: unknown): void {
|
public fatal(...args: unknown[]): void {
|
||||||
this.log("fatal", data);
|
this.log("fatal", ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
public trace(data: unknown): void {
|
public trace(...args: unknown[]): void {
|
||||||
this.log("trace", data);
|
this.log("trace", ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
public custom(tag: string, context: string, data: unknown): void {
|
public custom(tag: string, context: string, data: unknown): void {
|
||||||
|
|
131
src/lib/char.ts
131
src/lib/char.ts
|
@ -1,8 +1,8 @@
|
||||||
import { basename } from "node:path";
|
import { basename } from "node:path";
|
||||||
import { format, inspect } from "node:util";
|
import { format, inspect } from "node:util";
|
||||||
|
import { ansiColors, defaultLevelColor, logLevelValues } from "@lib/config";
|
||||||
import { format as formatDate } from "date-fns-tz";
|
import { format as formatDate } from "date-fns-tz";
|
||||||
|
|
||||||
import { ansiColors, defaultLevelColor, logLevelValues } from "@lib/config";
|
|
||||||
import type {
|
import type {
|
||||||
LogLevel,
|
LogLevel,
|
||||||
LogLevelValue,
|
LogLevelValue,
|
||||||
|
@ -59,6 +59,7 @@ function getCallerInfo(config: Required<LoggerConfig>): {
|
||||||
for (let i = 1; i < lines.length; i++) {
|
for (let i = 1; i < lines.length; i++) {
|
||||||
const line = lines[i].trim();
|
const line = lines[i].trim();
|
||||||
|
|
||||||
|
// try file:// URLs first (works on all platforms)
|
||||||
const fileURLMatch = line.match(
|
const fileURLMatch = line.match(
|
||||||
/at\s+(?:.*\()?file:\/\/(.*):(\d+):(\d+)\)?/,
|
/at\s+(?:.*\()?file:\/\/(.*):(\d+):(\d+)\)?/,
|
||||||
);
|
);
|
||||||
|
@ -79,16 +80,16 @@ function getCallerInfo(config: Required<LoggerConfig>): {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawMatch = line.match(/at\s+(?:.*\()?(.+):(\d+):(\d+)\)?/);
|
const pathMatch = parseStackTracePath(line);
|
||||||
if (rawMatch) {
|
if (pathMatch) {
|
||||||
const [_, fullPath, lineNumber, columnNumber] = rawMatch;
|
const { fullPath, lineNumber, columnNumber } = pathMatch;
|
||||||
const isInternal =
|
const isInternal =
|
||||||
fullPath.includes("atums.echo") || fullPath.includes("@atums/echo");
|
fullPath.includes("atums.echo") || fullPath.includes("@atums/echo");
|
||||||
|
|
||||||
if (isInternal) continue;
|
if (isInternal) continue;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: id,
|
id,
|
||||||
fileName: basename(fullPath),
|
fileName: basename(fullPath),
|
||||||
line: lineNumber,
|
line: lineNumber,
|
||||||
column: columnNumber,
|
column: columnNumber,
|
||||||
|
@ -101,6 +102,59 @@ function getCallerInfo(config: Required<LoggerConfig>): {
|
||||||
return fallback;
|
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 {
|
function generateShortId(length = 8): string {
|
||||||
const chars =
|
const chars =
|
||||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
"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;
|
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;
|
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(
|
function processPattern(
|
||||||
pattern: string,
|
pattern: string,
|
||||||
tokens: PatternTokens,
|
tokens: PatternTokens,
|
||||||
|
@ -231,15 +335,23 @@ function processPattern(
|
||||||
return processed;
|
return processed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatMultipleData(
|
||||||
|
dataArray: unknown[],
|
||||||
|
config: Required<LoggerConfig>,
|
||||||
|
): string {
|
||||||
|
return dataArray.map((item) => formatData(item, config)).join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
function parsePattern(ctx: PatternContext): string {
|
function parsePattern(ctx: PatternContext): string {
|
||||||
const { level, data, config } = ctx;
|
const { level, data, config } = ctx;
|
||||||
|
|
||||||
const { id, fileName, line, column, timestamp, prettyTimestamp } =
|
const { id, fileName, line, column, timestamp, prettyTimestamp } =
|
||||||
getCallerInfo(config);
|
getCallerInfo(config);
|
||||||
|
|
||||||
const resolvedData: string = formatData(data, config);
|
const resolvedData: string = Array.isArray(data)
|
||||||
const numericLevel: LogLevelValue = logLevelValues[level];
|
? formatMultipleData(data, config)
|
||||||
|
: formatData(data, config);
|
||||||
|
|
||||||
|
const numericLevel: LogLevelValue = logLevelValues[level];
|
||||||
const tokens: PatternTokens = {
|
const tokens: PatternTokens = {
|
||||||
timestamp,
|
timestamp,
|
||||||
prettyTimestamp,
|
prettyTimestamp,
|
||||||
|
@ -251,7 +363,6 @@ function parsePattern(ctx: PatternContext): string {
|
||||||
data: resolvedData,
|
data: resolvedData,
|
||||||
id,
|
id,
|
||||||
};
|
};
|
||||||
|
|
||||||
return processPattern(config.pattern, tokens, config, level);
|
return processPattern(config.pattern, tokens, config, level);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { readFileSync } from "node:fs";
|
import { readFileSync } from "node:fs";
|
||||||
import { resolve } from "node:path";
|
import { resolve } from "node:path";
|
||||||
|
|
||||||
import type { LogLevel, LoggerConfig } from "@types";
|
import type { LogLevel, LoggerConfig } from "@types";
|
||||||
|
|
||||||
const logLevelValues = {
|
const logLevelValues = {
|
||||||
|
|
|
@ -8,9 +8,10 @@ import {
|
||||||
} from "node:fs";
|
} from "node:fs";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { serializeLogData } from "@lib/char";
|
import { serializeLogData } from "@lib/char";
|
||||||
import type { LogLevel, LoggerConfig } from "@types";
|
|
||||||
import { format } from "date-fns-tz";
|
import { format } from "date-fns-tz";
|
||||||
|
|
||||||
|
import type { LogLevel, LoggerConfig } from "@types";
|
||||||
|
|
||||||
class FileLogger {
|
class FileLogger {
|
||||||
private stream: WriteStream | null = null;
|
private stream: WriteStream | null = null;
|
||||||
private filePath = "";
|
private filePath = "";
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue