Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • dev
  • main
  • v1.0.2
  • v1.0.3
  • v1.0.4
  • v1.0.5
  • v1.0.6
  • v1.0.7
  • v1.0.8
  • v2.0.0
10 results

Target

Select target project
  • atums/echo
1 result
Select Git revision
  • dev
  • main
  • v1.0.2
  • v1.0.3
  • v1.0.4
  • v1.0.5
  • v1.0.6
  • v1.0.7
  • v1.0.8
  • v2.0.0
10 results
Show changes
Commits on Source (0)
    name: Code quality checks
    on:
    push:
    pull_request:
    jobs:
    biome:
    runs-on: docker
    steps:
    - name: Checkout
    uses: actions/checkout@v4
    - name: Install Bun
    run: |
    curl -fsSL https://bun.sh/install | bash
    export BUN_INSTALL="$HOME/.bun"
    echo "$BUN_INSTALL/bin" >> $GITHUB_PATH
    - name: Install Dependencies
    run: bun install
    - name: Run Biome with verbose output
    run: bunx biome ci . --verbose
    stages:
    - quality
    biome:
    image:
    name: ghcr.io/biomejs/biome:latest
    entrypoint: [""]
    stage: quality
    script:
    - biome ci --verbose
    - biome ci --reporter=gitlab --colors=off > /tmp/code-quality.json
    - cp /tmp/code-quality.json code-quality.json
    artifacts:
    reports:
    codequality: code-quality.json
    paths:
    - code-quality.json
    rules:
    - if: $CI_COMMIT_BRANCH
    - if: $CI_MERGE_REQUEST_ID
    # @atums/echo
    A minimal, flexible logger for Node with console and file output, ANSI colors, and daily rotation.
    A minimal, flexible logger for Node with:
    - Colored console output
    - Daily `.jsonl` file logging
    - Configurable output patterns
    - Structured logs with caller metadata
    - Fully typed config with environment/file/constructor override
    ---
    ## Features
    - Console and file logging with level-based filtering
    - Colored output with ANSI formatting
    - Daily rotated `.jsonl` files
    - Supports runtime configuration merging
    - Auto-formatted output using custom patterns
    - Includes caller file, line, and column
    - Pretty-prints structured objects if enabled
    - Flushes open file streams on exit
    - Uses Biome and EditorConfig for formatting and linting
    ---
    ## Installation
    ......@@ -8,72 +30,229 @@ A minimal, flexible logger for Node with console and file output, ANSI colors, a
    bun add @atums/echo
    ```
    ## Quick Start
    ---
    ## Usage
    ```ts
    import { echo } from "@atums/echo";
    // Basic logging
    echo.info("App started");
    echo.error("Something failed:", error, { userId: 123 });
    echo.debug({ state: "init", ok: true });
    try {
    throw new Error("Something failed");
    } catch (err) {
    echo.error(err);
    }
    ```
    ---
    ## Configuration
    Logger config can be defined in three ways:
    1. JSON file (e.g. `logger.json`)
    2. Environment variables
    3. Constructor override
    Priority (highest to lowest):
    ```
    constructor > environment > logger.json > defaults
    ```
    ---
    ### logger.json example
    ```json
    {
    "directory": "logs",
    "level": "debug",
    "disableFile": false,
    "rotate": true,
    "maxFiles": 3,
    "console": true,
    "consoleColor": true,
    "dateFormat": "yyyy-MM-dd HH:mm:ss.SSS",
    "timezone": "local",
    "silent": false,
    "pattern": "{color:gray}{timestamp}{reset} {color:levelColor}[{level-name}]{reset} {color:gray}({reset}{file-name}:{line}:{column}{color:gray}){reset} {data}",
    "levelColor": {
    "debug": "blue",
    "info": "green",
    "warn": "yellow",
    "error": "red",
    "fatal": "red"
    },
    "customPattern": "{color:gray}{pretty-timestamp}{reset} {color:tagColor}[{tag}]{reset} {color:contextColor}({context}){reset} {data}",
    "customColors": {
    "GET": "red",
    "POST": "blue",
    "PUT": "yellow",
    "DELETE": "red",
    "PATCH": "cyan",
    "HEAD": "magenta",
    "OPTIONS": "white",
    "TRACE": "gray"
    },
    "prettyPrint": true
    }
    ```
    ---
    ### Supported Environment Variables
    | Variable | Description |
    |------------------------|-----------------------------------------------|
    | `LOG_LEVEL` | Log level (`debug`, `info`, etc.) |
    | `LOG_LEVEL_COLOR` | Comma-separated list of `TAG:color` pairs |
    | `LOG_DIRECTORY` | Log directory path (default: `logs`) |
    | `LOG_DISABLE_FILE` | Disable file output (`true` or `false`) |
    | `LOG_ROTATE` | Enable daily rotation |
    | `LOG_MAX_FILES` | Max rotated files to keep |
    | `LOG_CONSOLE` | Enable console output |
    | `LOG_CONSOLE_COLOR` | Enable ANSI color in console output |
    | `LOG_DATE_FORMAT` | Date format for display timestamp |
    | `LOG_TIMEZONE` | Timezone (`local` or IANA string) |
    | `LOG_SILENT` | Completely disable output |
    | `LOG_PATTERN` | Custom log format for console |
    | `LOG_PRETTY_PRINT` | Pretty-print objects in console output |
    | `LOG_CUSTOM_PATTERN` | Pattern used for `echo.custom()` logs |
    | `LOG_CUSTOM_COLORS` | Comma-separated list of `TAG:color` pairs |
    ---
    ## Pattern Tokens
    These tokens are replaced in the log pattern:
    | Token | Description |
    |----------------------|-------------------------------------------------|
    | `{timestamp}` | ISO timestamp string |
    | `{pretty-timestamp}` | Formatted display timestamp |
    | `{level-name}` | Uppercase log level (e.g. DEBUG) |
    | `{level}` | Numeric log level |
    | `{file-name}` | Source filename |
    | `{line}` | Line number in source |
    | `{column}` | Column number in source |
    | `{data}` | Formatted log data (message/object) |
    | `{id}` | Unique short ID for the log |
    | `{tag}` | Custom tag used 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 |
    | `{color:contextColor}`| Color for custom context |
    | `{reset}` | Resets console color |
    ---
    // Multiple arguments
    echo.warn("Rate limit:", 429, { endpoint: "/api/users" });
    ## Custom Log Entries
    // Custom tagged logs
    echo.custom("GET", "/health", { status: 200, duration: "15ms" });
    You can log arbitrary tagged messages with `echo.custom(tag, context, message)`:
    ```ts
    echo.custom("GET", "/health", { status: 200 });
    ```
    ## Log Levels
    The output format is controlled by:
    `trace` | `debug` | `info` | `warn` | `error` | `fatal`
    - `customPattern` — e.g. `{pretty-timestamp} [GET] (/health) { status: 200 }`
    - `customColors` — define colors for tags like `"GET": "green"`
    ## Output
    ### Example output
    **Console:**
    ```
    2025-05-24 16:15:00.000 [INFO] (app.ts:3:6) Server started
    2025-05-24 16:22:00.123 [GET] (/health) { status: 200 }
    ```
    **File (`logs/2025-05-24.jsonl`):**
    ---
    ## Output Examples
    ### Console
    ```
    2025-05-24 16:15:00.000 [INFO] (index.ts:3:6) Server started
    ```
    ### File (`logs/2025-05-24.jsonl`)
    Each line is structured JSON:
    ```json
    {
    "timestamp": 1748115300000,
    "level": "info",
    "file": "app.ts",
    "id": "aB4cD9xZ",
    "file": "index.ts",
    "line": "3",
    "data": ["Server started"]
    "column": "6",
    "data": "Server started"
    }
    ```
    ## Configuration
    Configure via `logger.json`, environment variables, or constructor. See - [Configuration Guide](https://heliopolis.live/atums/echo/-/blob/main/configuration.md) for full details.
    If an error is logged:
    **Quick setup:**
    ```json
    {
    "level": "debug",
    "directory": "logs",
    "console": true,
    "consoleColor": true
    "timestamp": 1748115301000,
    "level": "error",
    "id": "qW3eR7tU",
    "file": "index.ts",
    "line": "10",
    "column": "12",
    "data": {
    "name": "Error",
    "message": "Something failed",
    "stack": "Error: Something failed\n at index.ts:10:12"
    }
    }
    ```
    ## Documentation
    ---
    - - [Configuration Guide](https://heliopolis.live/atums/echo/-/blob/main/configuration.md) - Complete config options, environment variables, and patterns
    ## Development
    ## Features
    This project uses:
    - Multiple arguments per log call
    - Colored console output with ANSI codes
    - Daily rotated `.jsonl` files
    - Safe circular reference handling
    - Error object serialization
    - Custom log patterns and colors
    - Caller location tracking
    - TypeScript
    - Bun runtime
    - Biome for formatting/linting
    - JSONL for structured file output
    - `date-fns-tz` for timezone support
    ---
    ## Images
    <details>
    <summary>Logger preview (pretty)</summary>
    ![Logger preview](https://git.creations.works/atums/echo/media/branch/main/demo/image.png)
    </details>
    <details>
    <summary>Logger preview (no pretty)</summary>
    ![Logger preview no-pretty](https://git.creations.works/atums/echo/media/branch/main/demo/image-no-pretty.png)
    </details>
    ---
    ## License
    [BSD 3-Clause](LICENSE)
    BSD 3-Clause [License](License)
    {
    "$schema": "https://biomejs.dev/schemas/2.0.6/schema.json",
    "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
    "vcs": {
    "enabled": true,
    "clientKind": "git",
    ......@@ -7,14 +7,16 @@
    },
    "files": {
    "ignoreUnknown": true,
    "includes": ["**", "!**/dist"]
    "ignore": ["dist", "types"]
    },
    "formatter": {
    "enabled": true,
    "indentStyle": "tab",
    "lineEnding": "lf"
    },
    "assist": { "actions": { "source": { "organizeImports": "on" } } },
    "organizeImports": {
    "enabled": true
    },
    "css": {
    "formatter": {
    "indentStyle": "tab",
    ......@@ -27,32 +29,21 @@
    "recommended": true,
    "correctness": {
    "noUnusedImports": "error",
    "useJsxKeyInIterable": "off",
    "noUnusedVariables": "error"
    },
    "suspicious": {
    "noVar": "error"
    },
    "style": {
    "useConst": "error",
    "noParameterAssign": "error",
    "useAsConstAssertion": "error",
    "useDefaultParameterLast": "error",
    "useEnumInitializers": "error",
    "useSelfClosingElements": "error",
    "useSingleVarDeclarator": "error",
    "noUnusedTemplateLiteral": "error",
    "useNumberNamespace": "error",
    "noInferrableTypes": "error",
    "noUselessElse": "error"
    "noVar": "error"
    }
    }
    },
    "includes": ["**", "!**/types"]
    },
    "javascript": {
    "formatter": {
    "quoteStyle": "double",
    "indentStyle": "tab",
    "lineEnding": "lf",
    "jsxQuoteStyle": "double",
    "semicolons": "always"
    }
    }
    ......
    # Configuration Guide
    Configuration priority (highest to lowest):
    ```
    constructor > environment variables > logger.json > defaults
    ```
    ## Complete Configuration Example
    **logger.json:**
    ```json
    {
    "directory": "logs",
    "level": "debug",
    "disableFile": false,
    "rotate": true,
    "maxFiles": 3,
    "fileNameFormat": "yyyy-MM-dd",
    "subDirectory": null,
    "console": true,
    "consoleColor": true,
    "dateFormat": "yyyy-MM-dd HH:mm:ss.SSS",
    "timezone": "local",
    "silent": false,
    "pattern": "{color:gray}{timestamp}{reset} {color:levelColor}[{level-name}]{reset} {color:gray}({reset}{file-name}:{line}:{column}{color:gray}){reset} {data}",
    "customPattern": "{color:gray}{pretty-timestamp}{reset} {color:tagColor}[{tag}]{reset} {color:contextColor}({context}){reset} {data}",
    "levelColor": {
    "debug": "blue",
    "info": "green",
    "warn": "yellow",
    "error": "red",
    "fatal": "red"
    },
    "tagColors": {
    "GET": "green",
    "POST": "blue",
    "PUT": "yellow",
    "DELETE": "red",
    "PATCH": "cyan"
    },
    "prettyPrint": true
    }
    ```
    ## All Configuration Options
    | Option | Type | Environment Variable | Default | Description |
    |--------|------|---------------------|---------|-------------|
    | `level` | string | `LOG_LEVEL` | `info` | Log level filtering |
    | `directory` | string | `LOG_DIRECTORY` | `logs` | Directory for log files |
    | `disableFile` | boolean | `LOG_DISABLE_FILE` | `false` | Disable file output |
    | `rotate` | boolean | `LOG_ROTATE` | `true` | Enable daily file rotation |
    | `maxFiles` | number | `LOG_MAX_FILES` | `7` | Max rotated files to keep |
    | `fileNameFormat` | string | `LOG_FILE_NAME_FORMAT` | `yyyy-MM-dd` | File name date format |
    | `subDirectory` | string | `LOG_SUB_DIRECTORY` | `null` | Subdirectory for logs |
    | `console` | boolean | `LOG_CONSOLE` | `true` | Enable console output |
    | `consoleColor` | boolean | `LOG_CONSOLE_COLOR` | `true` | Enable ANSI colors |
    | `dateFormat` | string | `LOG_DATE_FORMAT` | `yyyy-MM-dd HH:mm:ss.SSS` | Display timestamp format |
    | `timezone` | string | `LOG_TIMEZONE` | `local` | Timezone (local or IANA) |
    | `silent` | boolean | `LOG_SILENT` | `false` | Disable all output |
    | `pattern` | string | `LOG_PATTERN` | See below | Console log format |
    | `customPattern` | string | `LOG_CUSTOM_PATTERN` | See below | Format for `echo.custom()` |
    | `prettyPrint` | boolean | `LOG_PRETTY_PRINT` | `true` | Pretty-print objects |
    | `levelColor` | object | `LOG_LEVEL_COLOR` | See above | Level-to-color mapping |
    | `tagColors` | object | `LOG_TAG_COLORS` | See above | Tag-to-color mapping |
    ### Environment Variable Format
    **.env file:**
    ```env
    LOG_LEVEL=debug
    LOG_DIRECTORY=./app-logs
    LOG_CONSOLE_COLOR=true
    # Color mappings (comma-separated key:value pairs)
    LOG_LEVEL_COLOR=debug:blue,info:green,warn:yellow,error:red
    LOG_TAG_COLORS=GET:green,POST:blue,DELETE:red
    ```
    ## Pattern Tokens
    | Token | Output Example | Description |
    |-------|----------------|-------------|
    | `{timestamp}` | `2025-05-24T16:15:00.000Z` | ISO timestamp |
    | `{pretty-timestamp}` | `2025-05-24 16:15:00.000` | Formatted timestamp |
    | `{level-name}` | `INFO` | Uppercase log level |
    | `{level}` | `2` | Numeric log level |
    | `{file-name}` | `app.ts` | Source filename |
    | `{line}` | `42` | Line number |
    | `{column}` | `12` | Column number |
    | `{data}` | `Server started` | Log message/data |
    | `{id}` | `aB4cD9xZ` | Unique short ID |
    | `{tag}` | `GET` | Custom tag (for `echo.custom()`) |
    | `{context}` | `/health` | Custom context |
    | `{color:red}` | *starts red* | ANSI color start |
    | `{color:levelColor}` | *dynamic* | Color based on log level |
    | `{color:tagColor}` | *dynamic* | Color based on tag |
    | `{reset}` | *resets color* | Reset ANSI colors |
    ### Pattern Examples
    **Default:**
    ```
    {color:gray}{timestamp}{reset} {color:levelColor}[{level-name}]{reset} {color:gray}({reset}{file-name}:{line}:{column}{color:gray}){reset} {data}
    ```
    `2025-05-24T16:15:00.000Z [INFO] (app.ts:3:6) Server started`
    **Simple:**
    ```
    {pretty-timestamp} [{level-name}] {data}
    ```
    `2025-05-24 16:15:00.000 [INFO] Server started`
    **Minimal:**
    ```
    [{level-name}] {data}
    ```
    `[INFO] Server started`
    ## File Naming
    | Format | Output |
    |--------|--------|
    | `yyyy-MM-dd` | `2025-06-03.jsonl` |
    | `yyyy-MM-dd_HH-mm` | `2025-06-03_18-30.jsonl` |
    | `yyyyMMdd` | `20250603.jsonl` |
    ## Constructor Override
    ```ts
    import { Echo } from "@atums/echo";
    const logger = new Echo({
    level: "debug",
    directory: "./custom-logs",
    consoleColor: false,
    pattern: "[{level-name}] {data}"
    });
    ```
    ## Available Colors
    `black` | `red` | `green` | `yellow` | `blue` | `magenta` | `cyan` | `white` | `gray`
    Plus: `bright*`, `bold*`, `bg*` variants
    demo/image-no-pretty.png

    17 KiB

    demo/image.png

    32.5 KiB

    {
    "name": "@atums/echo",
    "version": "2.0.0",
    "version": "1.0.2",
    "description": "A minimal, flexible logger",
    "private": false,
    "type": "module",
    "main": "./dist/index.cjs",
    "module": "./dist/index.js",
    "main": "./dist/index.js",
    "module": "./dist/index.mjs",
    "types": "./dist/index.d.ts",
    "typesVersions": {
    "*": {
    "lib/char": ["./dist/lib/char.d.ts"],
    "lib/config": ["./dist/lib/config.d.ts"]
    }
    },
    "exports": {
    ".": {
    "import": {
    "types": "./dist/index.d.ts",
    "default": "./dist/index.js"
    "import": "./dist/index.js",
    "require": "./dist/index.js"
    },
    "require": {
    "types": "./dist/index.d.ts",
    "default": "./dist/index.cjs"
    }
    "./lib/char": {
    "import": "./dist/lib/char.js",
    "require": "./dist/lib/char.js"
    },
    "./lib/config": {
    "import": "./dist/lib/config.js",
    "require": "./dist/lib/config.js"
    }
    },
    "scripts": {
    "dev": "bun run --watch build",
    "build": "rm -rf dist && tsup && rm -f dist/*.d.cts dist/**/*.d.cts",
    "dev": "bun run build --watch",
    "build": "rm -rf dist && tsup src/index.ts --dts --out-dir dist --format esm,cjs",
    "lint": "bunx biome check",
    "lint:fix": "bunx biome check --fix",
    "cleanup": "rm -rf logs node_modules bun.lock"
    },
    "license": "BSD-3-Clause",
    "devDependencies": {
    "@biomejs/biome": "^2.0.6",
    "@types/bun": "latest",
    "tsup": "latest"
    "@biomejs/biome": "^1.9.4",
    "@types/bun": "^1.2.13",
    "tsup": "^8.5.0",
    "typescript": "^5.8.3"
    },
    "files": [
    "dist",
    "README.md",
    "LICENSE",
    "configuration.md"
    ],
    "files": ["dist", "README.md", "LICENSE"],
    "repository": {
    "type": "git",
    "url": "https://heliopolis.live/atums/echo"
    },
    "keywords": [
    "logger",
    "logging",
    "typescript",
    "nodejs",
    "structured"
    ],
    "author": "creations.works",
    "homepage": "https://heliopolis.live/atums/echo",
    "bugs": {
    "url": "https://heliopolis.live/atums/echo/issues"
    "url": "https://git.creations.works/atums/echo"
    },
    "dependencies": {
    "date-fns-tz": "latest"
    "date-fns-tz": "^3.2.0"
    }
    }
    import {
    accessSync,
    constants,
    type Stats,
    accessSync,
    existsSync,
    mkdirSync,
    type Stats,
    statSync,
    } from "node:fs";
    import { resolve } from "node:path";
    import { format, inspect } from "node:util";
    import { getCallerInfo, getTimestamp, parsePattern } from "@lib/char";
    import {
    formatData,
    getCallerInfo,
    getConsoleMethod,
    getTimestamp,
    parsePattern,
    processPattern,
    } from "#lib/char";
    import {
    ansiColors,
    defaultConfig,
    loadEnvConfig,
    loadLoggerConfig,
    logLevelValues,
    validateAndSanitizeConfig,
    } from "#lib/config";
    import { FileLogger } from "#lib/file";
    import type { LoggerConfig, LogLevel, PatternTokens } from "#types";
    } from "@lib/config";
    import { FileLogger } from "@lib/file";
    import type { LogLevel, LoggerConfig } from "@types";
    class Echo {
    private readonly directory: string;
    ......@@ -42,18 +35,12 @@ class Echo {
    const envConfig: LoggerConfig = loadEnvConfig();
    const mergedConfig = {
    this.config = {
    ...defaultConfig,
    ...fileConfig,
    ...envConfig,
    ...overrideConfig,
    };
    const finalConfig = validateAndSanitizeConfig(
    mergedConfig,
    "merged configuration",
    );
    this.config = finalConfig as Required<LoggerConfig>;
    this.directory = resolve(this.config.directory);
    ......@@ -77,7 +64,7 @@ class Echo {
    accessSync(dir, constants.W_OK);
    }
    private log(level: LogLevel, ...args: unknown[]): void {
    private log(level: LogLevel, data: unknown): void {
    if (
    this.config.silent ||
    logLevelValues[this.config.level] > logLevelValues[level]
    ......@@ -85,59 +72,80 @@ class Echo {
    return;
    const meta = getCallerInfo(this.config);
    const line = parsePattern({ level, data: args, config: this.config });
    const line = parsePattern({ level, data, config: this.config });
    if (this.config.console) {
    console[getConsoleMethod(level)](line);
    console[level === "error" ? "error" : level === "warn" ? "warn" : "log"](
    line,
    );
    }
    if (!this.config.disableFile && this.fileLogger) {
    this.fileLogger.write(level, args, meta);
    this.fileLogger.write(level, data, meta);
    }
    }
    public debug(...args: unknown[]): void {
    this.log("debug", ...args);
    public debug(data: unknown): void {
    this.log("debug", data);
    }
    public info(...args: unknown[]): void {
    this.log("info", ...args);
    public info(data: unknown): void {
    this.log("info", data);
    }
    public warn(...args: unknown[]): void {
    this.log("warn", ...args);
    public warn(data: unknown): void {
    this.log("warn", data);
    }
    public error(...args: unknown[]): void {
    this.log("error", ...args);
    public error(data: unknown): void {
    this.log("error", data);
    }
    public fatal(...args: unknown[]): void {
    this.log("fatal", ...args);
    public fatal(data: unknown): void {
    this.log("fatal", data);
    }
    public trace(...args: unknown[]): void {
    this.log("trace", ...args);
    public trace(data: unknown): void {
    this.log("trace", data);
    }
    public custom(tag: string, context: string, data: unknown): void {
    if (this.config.silent) return;
    const timestamps = getTimestamp(this.config);
    const resolvedData = formatData(data, this.config);
    const normalizedTag = tag.toUpperCase();
    const tagColor = this.config.consoleColor
    ? (ansiColors[this.config.customColors?.[normalizedTag] ?? "green"] ?? "")
    : "";
    const contextColor = this.config.consoleColor ? ansiColors.cyan : "";
    const gray = this.config.consoleColor ? ansiColors.gray : "";
    const reset = this.config.consoleColor ? ansiColors.reset : "";
    const resolvedData =
    this.config.prettyPrint && typeof data === "object" && data !== null
    ? inspect(data, {
    depth: null,
    colors: this.config.consoleColor,
    breakLength: 1,
    compact: false,
    })
    : format(data);
    const pattern =
    this.config.customPattern ??
    "{color:gray}{pretty-timestamp}{reset} {color:tagColor}[{tag}]{reset} {color:contextColor}({context}){reset} {data}";
    const tokens: PatternTokens = {
    timestamp: timestamps.timestamp,
    prettyTimestamp: timestamps.prettyTimestamp,
    tag,
    context,
    data: resolvedData,
    };
    const line = processPattern(pattern, tokens, this.config, undefined, tag);
    const line = pattern
    .replace(/{timestamp}/g, timestamps.timestamp)
    .replace(/{pretty-timestamp}/g, timestamps.prettyTimestamp)
    .replace(/{tag}/g, tag)
    .replace(/{context}/g, context)
    .replace(/{data}/g, resolvedData)
    .replace(/{color:gray}/g, gray)
    .replace(/{color:tagColor}/g, tagColor)
    .replace(/{color:contextColor}/g, contextColor)
    .replace(/{reset}/g, reset);
    if (this.config.console) {
    console.log(line);
    ......@@ -160,3 +168,4 @@ function createLogger(config?: string | LoggerConfig): Echo {
    const echo = new Echo();
    export { echo, Echo, createLogger };
    export type { LoggerConfig, LogLevel } from "@types";
    import { basename } from "node:path";
    import { format, inspect } from "node:util";
    import { format as formatDate } from "date-fns-tz";
    import { ansiColors, defaultLevelColor, logLevelValues } from "#lib/config";
    import { ansiColors, defaultLevelColor, logLevelValues } from "@lib/config";
    import type {
    LoggerConfig,
    LogLevel,
    LogLevelValue,
    LoggerConfig,
    PatternContext,
    PatternTokens,
    } from "#types";
    } from "@types";
    function getTimestamp(config: Required<LoggerConfig>): {
    prettyTimestamp: string;
    ......@@ -41,6 +40,7 @@ function getCallerInfo(config: Required<LoggerConfig>): {
    prettyTimestamp: string;
    } {
    const id = generateShortId();
    const timestampInfo = getTimestamp(config);
    const fallback = {
    id,
    ......@@ -52,26 +52,25 @@ function getCallerInfo(config: Required<LoggerConfig>): {
    };
    const stack = new Error().stack;
    if (!stack) return fallback;
    if (!stack) {
    return fallback;
    }
    const lines = stack.split("\n");
    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+)\)?/,
    );
    if (fileURLMatch) {
    const [_, fullPath, lineNumber, columnNumber] = fileURLMatch;
    const isInternal =
    fullPath.includes("atums.echo") || fullPath.includes("@atums/echo");
    if (isInternal) continue;
    const fullPath = fileURLMatch[1];
    const lineNumber = fileURLMatch[2];
    const columnNumber = fileURLMatch[3];
    return {
    id,
    id: id,
    fileName: basename(fullPath),
    line: lineNumber,
    column: columnNumber,
    ......@@ -80,16 +79,22 @@ function getCallerInfo(config: Required<LoggerConfig>): {
    };
    }
    const pathMatch = parseStackTracePath(line);
    if (pathMatch) {
    const { fullPath, lineNumber, columnNumber } = pathMatch;
    const isInternal =
    fullPath.includes("atums.echo") || fullPath.includes("@atums/echo");
    const rawMatch = line.match(/at\s+(?:.*\()?(.+):(\d+):(\d+)\)?/);
    if (rawMatch) {
    const fullPath = rawMatch[1];
    const lineNumber = rawMatch[2];
    const columnNumber = rawMatch[3];
    if (isInternal) continue;
    if (
    fullPath.includes("/logger/") ||
    fullPath.includes("/src/index.ts") ||
    fullPath.includes("/src/lib/")
    ) {
    continue;
    }
    return {
    id,
    id: id,
    fileName: basename(fullPath),
    line: lineNumber,
    column: columnNumber,
    ......@@ -102,59 +107,6 @@ 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";
    ......@@ -168,212 +120,53 @@ function generateShortId(length = 8): string {
    return id;
    }
    function formatData(data: unknown, config: Required<LoggerConfig>): string {
    return config.prettyPrint && typeof data === "object" && data !== null
    ? inspect(data, {
    depth: null,
    colors: config.consoleColor,
    breakLength: 1,
    compact: false,
    })
    : format(data);
    }
    function getConsoleMethod(level: LogLevel): "log" | "warn" | "error" {
    if (level === "error" || level === "fatal") return "error";
    if (level === "warn") return "warn";
    return "log";
    }
    function resolveColor(
    colorKey: string,
    function replaceColorTokens(
    input: string,
    level: LogLevel,
    config: Required<LoggerConfig>,
    level?: LogLevel,
    tag?: string,
    ): string {
    return input
    .replace(/{color:(\w+)}/g, (_, colorKey) => {
    if (!config.consoleColor) return "";
    if (colorKey === "levelColor" && level) {
    if (colorKey === "levelColor") {
    const colorForLevel =
    config.levelColor?.[level] ?? defaultLevelColor[level];
    return ansiColors[colorForLevel ?? ""] ?? "";
    }
    if (colorKey === "tagColor" && tag) {
    const normalizedTag = tag.toUpperCase();
    return ansiColors[config.tagColors?.[normalizedTag] ?? "green"] ?? "";
    }
    if (colorKey === "contextColor") {
    return ansiColors.cyan ?? "";
    }
    return ansiColors[colorKey] ?? "";
    }
    function serializeLogData(data: unknown): unknown {
    if (data instanceof Error) {
    return {
    name: data.name,
    message: data.message,
    stack: data.stack,
    };
    }
    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,
    config: Required<LoggerConfig>,
    level?: LogLevel,
    tag?: string,
    ): string {
    let processed = pattern;
    if (tokens.timestamp) {
    processed = processed.replace(/{timestamp}/g, tokens.timestamp);
    }
    if (tokens.prettyTimestamp) {
    processed = processed.replace(
    /{pretty-timestamp}/g,
    tokens.prettyTimestamp,
    );
    }
    if (tokens.levelName) {
    processed = processed.replace(/{level-name}/g, tokens.levelName);
    }
    if (tokens.level) {
    processed = processed.replace(/{level}/g, tokens.level);
    }
    if (tokens.fileName) {
    processed = processed.replace(/{file-name}/g, tokens.fileName);
    }
    if (tokens.line) {
    processed = processed.replace(/{line}/g, tokens.line);
    }
    if (tokens.column) {
    processed = processed.replace(/{column}/g, tokens.column);
    }
    if (tokens.data) {
    processed = processed.replace(/{data}/g, tokens.data);
    }
    if (tokens.id) {
    processed = processed.replace(/{id}/g, tokens.id);
    }
    if (tokens.tag) {
    processed = processed.replace(/{tag}/g, tokens.tag);
    }
    if (tokens.context) {
    processed = processed.replace(/{context}/g, tokens.context);
    }
    processed = processed.replace(/{color:(\w+)}/g, (_, colorKey) => {
    return resolveColor(colorKey, config, level, tag);
    });
    processed = processed.replace(
    /{reset}/g,
    config.consoleColor ? ansiColors.reset : "",
    );
    return processed;
    }
    function formatMultipleData(
    dataArray: unknown[],
    config: Required<LoggerConfig>,
    ): string {
    return dataArray.map((item) => formatData(item, config)).join(" ");
    })
    .replace(/{reset}/g, config.consoleColor ? ansiColors.reset : "");
    }
    function parsePattern(ctx: PatternContext): string {
    const { level, data, config } = ctx;
    const { id, fileName, line, column, timestamp, prettyTimestamp } =
    getCallerInfo(config);
    const resolvedData: string = Array.isArray(data)
    ? formatMultipleData(data, config)
    : formatData(data, config);
    const resolvedData: string =
    config.prettyPrint && typeof data === "object" && data !== null
    ? inspect(data, {
    depth: null,
    colors: config.consoleColor,
    breakLength: 1,
    compact: false,
    })
    : format(data);
    const numericLevel: LogLevelValue = logLevelValues[level];
    const tokens: PatternTokens = {
    timestamp,
    prettyTimestamp,
    levelName: level.toUpperCase(),
    level: String(numericLevel),
    fileName,
    line,
    column,
    data: resolvedData,
    id,
    };
    return processPattern(config.pattern, tokens, config, level);
    const final = config.pattern
    .replace(/{timestamp}/g, timestamp)
    .replace(/{pretty-timestamp}/g, prettyTimestamp)
    .replace(/{level-name}/g, level.toUpperCase())
    .replace(/{level}/g, String(numericLevel))
    .replace(/{file-name}/g, fileName)
    .replace(/{line}/g, line)
    .replace(/{data}/g, resolvedData)
    .replace(/{id}/g, id)
    .replace(/{column}/g, column);
    return replaceColorTokens(final, level, config);
    }
    export {
    parsePattern,
    getCallerInfo,
    getTimestamp,
    generateShortId,
    formatData,
    getConsoleMethod,
    resolveColor,
    serializeLogData,
    processPattern,
    };
    export { parsePattern, getCallerInfo, getTimestamp, generateShortId };
    import { readFileSync } from "node:fs";
    import { resolve } from "node:path";
    import type { LoggerConfig, LogLevel } from "#types";
    import type { LogLevel, LoggerConfig } from "@types";
    const logLevelValues = {
    trace: 10,
    ......@@ -11,7 +10,7 @@ const logLevelValues = {
    error: 50,
    fatal: 60,
    silent: 70,
    } as const;
    };
    const defaultLevelColor: Record<LogLevel, keyof typeof ansiColors> = {
    trace: "cyan",
    ......@@ -25,97 +24,18 @@ const defaultLevelColor: Record<LogLevel, keyof typeof ansiColors> = {
    const ansiColors: Record<string, string> = {
    reset: "\x1b[0m",
    // text styles
    dim: "\x1b[2m",
    bright: "\x1b[1m",
    bold: "\x1b[1m",
    italic: "\x1b[3m",
    underline: "\x1b[4m",
    inverse: "\x1b[7m",
    hidden: "\x1b[8m",
    strikethrough: "\x1b[9m",
    // regular colors (30-37)
    black: "\x1b[30m",
    red: "\x1b[31m",
    green: "\x1b[32m",
    yellow: "\x1b[33m",
    blue: "\x1b[34m",
    magenta: "\x1b[35m",
    purple: "\x1b[35m", // alias for magenta
    cyan: "\x1b[36m",
    white: "\x1b[37m",
    // bold colors (1;30-37)
    boldBlack: "\x1b[1;30m",
    boldRed: "\x1b[1;31m",
    boldGreen: "\x1b[1;32m",
    boldYellow: "\x1b[1;33m",
    boldBlue: "\x1b[1;34m",
    boldMagenta: "\x1b[1;35m",
    boldPurple: "\x1b[1;35m", // alias for bold magenta
    boldCyan: "\x1b[1;36m",
    boldWhite: "\x1b[1;37m",
    // underline colors (4;30-37)
    underlineBlack: "\x1b[4;30m",
    underlineRed: "\x1b[4;31m",
    underlineGreen: "\x1b[4;32m",
    underlineYellow: "\x1b[4;33m",
    underlineBlue: "\x1b[4;34m",
    underlineMagenta: "\x1b[4;35m",
    underlinePurple: "\x1b[4;35m", // alias for underline magenta
    underlineCyan: "\x1b[4;36m",
    underlineWhite: "\x1b[4;37m",
    // background colors (40-47)
    bgBlack: "\x1b[40m",
    bgRed: "\x1b[41m",
    bgGreen: "\x1b[42m",
    bgYellow: "\x1b[43m",
    bgBlue: "\x1b[44m",
    bgMagenta: "\x1b[45m",
    bgPurple: "\x1b[45m", // alias for bg magenta
    bgCyan: "\x1b[46m",
    bgWhite: "\x1b[47m",
    // high intensity colors (90-97)
    gray: "\x1b[90m",
    brightBlack: "\x1b[90m", // alias for gray
    brightRed: "\x1b[91m",
    brightGreen: "\x1b[92m",
    brightYellow: "\x1b[93m",
    brightBlue: "\x1b[94m",
    brightMagenta: "\x1b[95m",
    brightPurple: "\x1b[95m", // alias for bright magenta
    brightCyan: "\x1b[96m",
    brightWhite: "\x1b[97m",
    // bold high intensity colors (1;90-97)
    boldBrightBlack: "\x1b[1;90m",
    boldGray: "\x1b[1;90m", // alias for bold bright black
    boldBrightRed: "\x1b[1;91m",
    boldBrightGreen: "\x1b[1;92m",
    boldBrightYellow: "\x1b[1;93m",
    boldBrightBlue: "\x1b[1;94m",
    boldBrightMagenta: "\x1b[1;95m",
    boldBrightPurple: "\x1b[1;95m", // alias for bold bright magenta
    boldBrightCyan: "\x1b[1;96m",
    boldBrightWhite: "\x1b[1;97m",
    // high intensity background colors (100-107)
    bgBrightBlack: "\x1b[100m",
    bgGray: "\x1b[100m", // alias for bg bright black
    bgBrightRed: "\x1b[101m",
    bgBrightGreen: "\x1b[102m",
    bgBrightYellow: "\x1b[103m",
    bgBrightBlue: "\x1b[104m",
    bgBrightMagenta: "\x1b[105m",
    bgBrightPurple: "\x1b[105m", // alias for bg bright magenta
    bgBrightCyan: "\x1b[106m",
    bgBrightWhite: "\x1b[107m",
    } as const;
    };
    const defaultConfig: Required<LoggerConfig> = {
    directory: "logs",
    ......@@ -123,9 +43,7 @@ const defaultConfig: Required<LoggerConfig> = {
    disableFile: false,
    rotate: true,
    maxFiles: null,
    fileNameFormat: "yyyy-MM-dd",
    subDirectory: null,
    maxFiles: 3,
    console: true,
    consoleColor: true,
    ......@@ -147,136 +65,19 @@ const defaultConfig: Required<LoggerConfig> = {
    fatal: "red",
    },
    tagColors: {},
    customColors: {},
    customPattern:
    "{color:gray}{pretty-timestamp}{reset} {color:tagColor}[{tag}]{reset} {color:contextColor}({context}){reset} {data}",
    prettyPrint: true,
    };
    function isValidLogLevel(level: string): level is LogLevel {
    return level in logLevelValues;
    }
    function isValidColor(color: string): color is keyof typeof ansiColors {
    return color in ansiColors;
    }
    function parseNumericEnv(
    value: string | undefined,
    min = 0,
    ): number | undefined {
    if (!value) return undefined;
    const parsed = Number.parseInt(value, 10);
    if (Number.isNaN(parsed) || parsed < min) {
    return undefined;
    }
    return parsed;
    }
    function parseBooleanEnv(value: string | undefined): boolean | undefined {
    if (!value) return undefined;
    return value.toLowerCase() === "true";
    }
    function validateAndSanitizeConfig(
    config: LoggerConfig,
    source: string,
    ): LoggerConfig {
    const sanitized = { ...config };
    const warnings: string[] = [];
    if (sanitized.level && !isValidLogLevel(sanitized.level)) {
    warnings.push(
    `Invalid log level "${sanitized.level}" in ${source}, using default "info"`,
    );
    sanitized.level = "info";
    }
    if (sanitized.maxFiles !== undefined && sanitized.maxFiles !== null) {
    if (
    typeof sanitized.maxFiles !== "number" ||
    sanitized.maxFiles < 1 ||
    !Number.isInteger(sanitized.maxFiles)
    ) {
    warnings.push(
    `Invalid maxFiles value "${sanitized.maxFiles}" in ${source}, setting to null`,
    );
    sanitized.maxFiles = null;
    }
    }
    if (sanitized.levelColor) {
    const validLevelColors: Partial<Record<LogLevel, keyof typeof ansiColors>> =
    {};
    for (const [level, color] of Object.entries(sanitized.levelColor)) {
    if (!isValidLogLevel(level)) {
    warnings.push(
    `Invalid log level "${level}" in levelColor from ${source}, skipping`,
    );
    continue;
    }
    if (!isValidColor(color)) {
    warnings.push(
    `Invalid color "${color}" for level "${level}" in ${source}, using default`,
    );
    validLevelColors[level as LogLevel] =
    defaultLevelColor[level as LogLevel];
    } else {
    validLevelColors[level as LogLevel] = color;
    }
    }
    sanitized.levelColor = validLevelColors;
    }
    if (sanitized.tagColors) {
    const validtagColors: Record<string, keyof typeof ansiColors> = {};
    for (const [tag, color] of Object.entries(sanitized.tagColors)) {
    if (!isValidColor(color)) {
    warnings.push(
    `Invalid color "${color}" for tag "${tag}" in ${source}, skipping`,
    );
    continue;
    }
    validtagColors[tag] = color;
    }
    sanitized.tagColors = validtagColors;
    }
    if (warnings.length > 0) {
    console.warn(
    `[@atums/echo] Configuration warnings:\n ${warnings.join("\n ")}`,
    );
    }
    return sanitized;
    }
    function loadLoggerConfig(configPath = "logger.json"): LoggerConfig {
    try {
    const fullPath: string = resolve(process.cwd(), configPath);
    const raw: string = readFileSync(fullPath, "utf-8");
    const parsed = JSON.parse(raw);
    if (typeof parsed !== "object" || parsed === null) {
    console.warn(`[@atums/echo] Invalid config file format: ${configPath}`);
    return {};
    }
    return validateAndSanitizeConfig(parsed, `config file "${configPath}"`);
    } catch (error) {
    if (error instanceof Error && !error.message.includes("ENOENT")) {
    console.warn(
    `[@atums/echo] Failed to load config file ${configPath}:`,
    error.message,
    );
    }
    return JSON.parse(raw);
    } catch {
    return {};
    }
    }
    ......@@ -284,101 +85,50 @@ function loadLoggerConfig(configPath = "logger.json"): LoggerConfig {
    function loadEnvConfig(): LoggerConfig {
    const config: LoggerConfig = {};
    if (process.env.LOG_LEVEL && isValidLogLevel(process.env.LOG_LEVEL)) {
    config.level = process.env.LOG_LEVEL;
    }
    if (process.env.LOG_DIRECTORY) {
    config.directory = process.env.LOG_DIRECTORY;
    }
    config.disableFile = parseBooleanEnv(process.env.LOG_DISABLE_FILE);
    config.rotate = parseBooleanEnv(process.env.LOG_ROTATE);
    config.console = parseBooleanEnv(process.env.LOG_CONSOLE);
    config.consoleColor = parseBooleanEnv(process.env.LOG_CONSOLE_COLOR);
    config.silent = parseBooleanEnv(process.env.LOG_SILENT);
    config.prettyPrint = parseBooleanEnv(process.env.LOG_PRETTY_PRINT);
    const maxFiles = parseNumericEnv(process.env.LOG_MAX_FILES, 1);
    if (maxFiles !== undefined) {
    config.maxFiles = maxFiles;
    }
    if (process.env.LOG_FILE_NAME_FORMAT) {
    config.fileNameFormat = process.env.LOG_FILE_NAME_FORMAT;
    }
    if (process.env.LOG_SUB_DIRECTORY) {
    config.subDirectory = process.env.LOG_SUB_DIRECTORY;
    }
    if (process.env.LOG_DATE_FORMAT) {
    if (process.env.LOG_LEVEL) config.level = process.env.LOG_LEVEL as LogLevel;
    if (process.env.LOG_DIRECTORY) config.directory = process.env.LOG_DIRECTORY;
    if (process.env.LOG_DISABLE_FILE)
    config.disableFile = process.env.LOG_DISABLE_FILE === "true";
    if (process.env.LOG_ROTATE) config.rotate = process.env.LOG_ROTATE === "true";
    if (process.env.LOG_MAX_FILES)
    config.maxFiles = Number.parseInt(process.env.LOG_MAX_FILES, 10);
    if (process.env.LOG_CONSOLE)
    config.console = process.env.LOG_CONSOLE === "true";
    if (process.env.LOG_CONSOLE_COLOR)
    config.consoleColor = process.env.LOG_CONSOLE_COLOR === "true";
    if (process.env.LOG_DATE_FORMAT)
    config.dateFormat = process.env.LOG_DATE_FORMAT;
    }
    if (process.env.LOG_TIMEZONE) {
    config.timezone = process.env.LOG_TIMEZONE;
    }
    if (process.env.LOG_PATTERN) {
    config.pattern = process.env.LOG_PATTERN;
    }
    if (process.env.LOG_CUSTOM_PATTERN) {
    config.customPattern = process.env.LOG_CUSTOM_PATTERN;
    }
    if (process.env.LOG_TIMEZONE) config.timezone = process.env.LOG_TIMEZONE;
    if (process.env.LOG_SILENT) config.silent = process.env.LOG_SILENT === "true";
    if (process.env.LOG_PATTERN) config.pattern = process.env.LOG_PATTERN;
    if (process.env.LOG_PRETTY_PRINT)
    config.prettyPrint = process.env.LOG_PRETTY_PRINT === "true";
    if (process.env.LOG_LEVEL_COLOR) {
    const colors = process.env.LOG_LEVEL_COLOR.split(",");
    const levelColor: Partial<Record<LogLevel, keyof typeof ansiColors>> = {};
    for (const colorPair of colors) {
    const [level, colorName] = colorPair.split(":");
    if (
    level &&
    colorName &&
    isValidLogLevel(level) &&
    isValidColor(colorName)
    ) {
    levelColor[level] = colorName;
    } else {
    console.warn(`[@atums/echo] Invalid level color pair: ${colorPair}`);
    }
    }
    if (Object.keys(levelColor).length > 0) {
    config.levelColor = levelColor;
    }
    for (const color of colors) {
    const [level, colorName] = color.split(":");
    if (logLevelValues[level as LogLevel] !== undefined) {
    config.levelColor = {
    ...config.levelColor,
    [level as LogLevel]: colorName as keyof typeof ansiColors,
    };
    }
    if (process.env.LOG_TAG_COLORS) {
    const colors = process.env.LOG_TAG_COLORS.split(",");
    const tagColors: Record<string, keyof typeof ansiColors> = {};
    for (const colorPair of colors) {
    const [tag, colorName] = colorPair.split(":");
    if (tag && colorName && isValidColor(colorName)) {
    tagColors[tag] = colorName;
    } else {
    console.warn(`[@atums/echo] Invalid custom color pair: ${colorPair}`);
    }
    }
    if (Object.keys(tagColors).length > 0) {
    config.tagColors = tagColors;
    if (process.env.LOG_CUSTOM_COLORS) {
    const colors = process.env.LOG_CUSTOM_COLORS.split(",");
    for (const color of colors) {
    const [tag, colorName] = color.split(":");
    config.customColors = {
    ...config.customColors,
    [tag]: colorName as keyof typeof ansiColors,
    };
    }
    }
    const sanitizedConfig = validateAndSanitizeConfig(
    config,
    "environment variables",
    );
    return Object.fromEntries(
    Object.entries(sanitizedConfig).filter(([_, value]) => value !== undefined),
    ) as LoggerConfig;
    return config;
    }
    export {
    ......@@ -388,7 +138,4 @@ export {
    loadEnvConfig,
    logLevelValues,
    ansiColors,
    validateAndSanitizeConfig,
    isValidLogLevel,
    isValidColor,
    };
    import {
    type WriteStream,
    createWriteStream,
    existsSync,
    mkdirSync,
    readdirSync,
    unlinkSync,
    type WriteStream,
    } from "node:fs";
    import { join } from "node:path";
    import type { LogLevel, LoggerConfig } from "@types";
    import { format } from "date-fns-tz";
    import { serializeLogData } from "#lib/char";
    import type { LoggerConfig, LogLevel } from "#types";
    class FileLogger {
    private stream: WriteStream | null = null;
    private filePath = "";
    private date = "";
    private fileNameFormat = "yyyy-MM-dd";
    private logDirectory: string;
    constructor(private readonly config: Required<LoggerConfig>) {
    this.logDirectory = this.config.subDirectory
    ? join(this.config.directory, this.config.subDirectory)
    : this.config.directory;
    mkdirSync(this.logDirectory, { recursive: true });
    if (this.config.fileNameFormat) {
    try {
    format(new Date(), this.config.fileNameFormat, {
    timeZone: this.config.timezone,
    });
    this.fileNameFormat = this.config.fileNameFormat;
    } catch (error) {
    throw new Error(
    `[@atums/echo] Invalid fileNameFormat: ${this.config.fileNameFormat}. Error: ${error instanceof Error ? error.message : "Unknown error"}`,
    );
    }
    if (!existsSync(this.config.directory)) {
    mkdirSync(this.config.directory, { recursive: true });
    }
    }
    private getLogFilePath(dateStr: string): string {
    const fileName = `${dateStr}.jsonl`;
    return join(this.logDirectory, fileName);
    return join(this.config.directory, `${dateStr}.jsonl`);
    }
    private resetStream(path: string): void {
    ......@@ -50,32 +31,13 @@ class FileLogger {
    this.filePath = path;
    }
    private generateFileRegex(): RegExp {
    const pattern = this.fileNameFormat
    .replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
    .replace(/\\y\\y\\y\\y/g, "\\d{4}")
    .replace(/\\M\\M/g, "\\d{2}")
    .replace(/\\d\\d/g, "\\d{2}")
    .replace(/\\H\\H/g, "\\d{2}")
    .replace(/\\m\\m/g, "\\d{2}")
    .replace(/\\s\\s/g, "\\d{2}")
    .replace(/\\S\\S\\S/g, "\\d{3}");
    return new RegExp(`^${pattern}\\.jsonl$`);
    }
    private pruneOldLogs(): void {
    if (this.config.maxFiles === null) {
    return;
    }
    if (this.config.maxFiles < 1) {
    if (this.config.maxFiles && this.config.maxFiles < 1) {
    throw new Error("[@atums/echo] maxFiles must be >= 1 if set.");
    }
    const fileRegex = this.generateFileRegex();
    const files = readdirSync(this.logDirectory)
    .filter((file) => fileRegex.test(file))
    const files = readdirSync(this.config.directory)
    .filter((file) => /^\d{4}-\d{2}-\d{2}\.jsonl$/.test(file))
    .sort();
    const excess = files.slice(
    ......@@ -85,18 +47,11 @@ class FileLogger {
    for (const file of excess) {
    try {
    unlinkSync(join(this.logDirectory, file));
    unlinkSync(join(this.config.directory, file));
    } catch {}
    }
    }
    private getFilePath(dateStr?: string): string {
    if (this.config.rotate && dateStr) {
    return this.getLogFilePath(dateStr);
    }
    return join(this.logDirectory, "log.jsonl");
    }
    public write(
    level: LogLevel | string,
    data: unknown,
    ......@@ -112,34 +67,45 @@ class FileLogger {
    if (this.config.disableFile) return;
    const now = new Date();
    const line = `${JSON.stringify({
    timestamp: new Date(meta.timestamp).getTime(),
    level,
    id: meta.id,
    file: meta.fileName,
    line: meta.line,
    column: meta.column,
    data: serializeLogData(data),
    })}\n`;
    let path: string;
    const dateStr = this.config.rotate
    ? format(now, this.fileNameFormat, { timeZone: this.config.timezone })
    : undefined;
    const needsRotation = this.config.rotate && this.date !== dateStr;
    path = this.getFilePath(dateStr);
    if (this.config.rotate) {
    const dateStr = format(now, "yyyy-MM-dd", {
    timeZone: this.config.timezone,
    });
    path = this.getLogFilePath(dateStr);
    if (!this.stream || needsRotation || this.filePath !== path) {
    if (this.config.rotate && dateStr) {
    if (!this.stream || this.filePath !== path || this.date !== dateStr) {
    this.date = dateStr;
    }
    this.resetStream(path);
    if (needsRotation) {
    this.pruneOldLogs();
    }
    } else {
    path = join(this.config.directory, "log.jsonl");
    if (!this.stream || this.filePath !== path) {
    this.resetStream(path);
    }
    }
    const line = `${JSON.stringify({
    timestamp: new Date(meta.timestamp).getTime(),
    level,
    id: meta.id,
    file: meta.fileName,
    line: meta.line,
    column: meta.column,
    data:
    data instanceof Error
    ? {
    name: data.name,
    message: data.message,
    stack: data.stack,
    }
    : typeof data === "string" || typeof data === "number"
    ? data
    : data,
    })}\n`;
    try {
    this.stream?.write(line);
    ......
    ......@@ -2,10 +2,12 @@
    "compilerOptions": {
    "baseUrl": "./",
    "paths": {
    "#types": ["types/index.ts"],
    "#lib/*": ["src/lib/*"]
    "@/*": ["src/*"],
    "@types": ["types/index.ts"],
    "@types/*": ["types/*"],
    "@lib/*": ["src/lib/*"]
    },
    "typeRoots": ["./node_modules/@types"],
    "typeRoots": ["./types", "./node_modules/@types"],
    "lib": ["ESNext", "DOM"],
    "target": "ESNext",
    "module": "ESNext",
    ......
    import { defineConfig } from "tsup";
    export default defineConfig({
    entry: ["src/index.ts"],
    format: ["esm", "cjs"],
    dts: {
    resolve: true,
    compilerOptions: {
    module: "ESNext",
    moduleResolution: "bundler",
    },
    },
    outDir: "dist",
    minify: true,
    splitting: false,
    sourcemap: false,
    clean: true,
    treeshake: true,
    target: "es2024",
    define: {
    "process.env.NODE_ENV": '"production"',
    },
    outExtension({ format }) {
    return {
    js: format === "cjs" ? ".cjs" : ".js",
    dts: ".d.ts",
    };
    },
    });
    import type { ansiColors, logLevelValues } from "#lib/config";
    import { ansiColors, logLevelValues } from "@lib/config";
    type LogLevelValue = (typeof logLevelValues)[keyof typeof logLevelValues];
    type LogLevelValue = typeof logLevelValues[keyof typeof logLevelValues];
    type LogLevel = keyof typeof logLevelValues;
    type LoggerConfig = {
    ......@@ -9,9 +9,7 @@ type LoggerConfig = {
    disableFile?: boolean;
    rotate?: boolean;
    maxFiles?: number | null;
    fileNameFormat?: string;
    subDirectory?: string | null;
    maxFiles?: number;
    console?: boolean;
    consoleColor?: boolean;
    ......@@ -25,35 +23,15 @@ type LoggerConfig = {
    levelColor?: Partial<Record<LogLevel, keyof typeof ansiColors>>;
    customPattern?: string;
    tagColors?: Record<string, keyof typeof ansiColors>;
    customColors?: Record<string, keyof typeof ansiColors>;
    prettyPrint?: boolean;
    };
    type PatternContext = {
    interface PatternContext {
    level: LogLevel;
    data: unknown;
    config: Required<LoggerConfig>;
    };
    interface PatternTokens {
    timestamp?: string;
    prettyTimestamp?: string;
    levelName?: string;
    level?: string;
    fileName?: string;
    line?: string;
    column?: string;
    data?: string;
    id?: string;
    tag?: string;
    context?: string;
    }
    export type {
    LogLevel,
    LogLevelValue,
    LoggerConfig,
    PatternContext,
    PatternTokens,
    };
    export type { LogLevel, LogLevelValue, LoggerConfig, PatternContext };