tslog

repository·master·Indexed 23 days ago

https://github.com/fullstack-build/tslog

Extensible TypeScript Logger for Node.js, Deno, Bun, Browser, and React Native. Version 5.1.0 is ESM-only and requires Node.js ≥ 20. It provides structured logging, colorized pretty output, secret masking, and sub-logger inheritance. Features include automatic source-map resolution for error positions, specialized builds for size-critical environments (tslog/slim), and support for LLM/Agent tracing via AsyncLocalStorage correlation.

Tokens
29.8K
Snippets
80
Records
113
Agent score
82%

What's inside tslog

  1. Configure Output Type and Colorization

    master

    tslog defaults to pretty output.

    Behavior by Environment:

    • Interactive TTY: pretty with colors.
    • Piped/Redirected/CI: pretty without ANSI colors.
    • Browser/React Native: pretty using CSS styling.

    Switching to JSON: Structured json output is opt-in. You can enable it by:

    1. Setting type: "json" in the config.
    2. Setting the environment variable TSLOG_TYPE=json.
    3. Using Logger.fromEnv() which reads TSLOG_TYPE.

    Color Control:

    • NO_COLOR: Disables colors (follows no-color.org).
    • FORCE_COLOR: Forces colorized pretty output.
  2. Optimize performance with stack capture settings

    master

    Stack capture is the biggest performance lever in tslog. You can control it via the stack.capture option:

    • "off": Skips code-position capture entirely. Recommended for high-performance production paths.
    • "auto": (Default for pretty) Captures frames only when the rendered template actually requires a code position.
    • "lazy": Captures frames cheaply and defers parsing until the frames are actually read.
    • "full": Captures complete frames for deep debugging.

    Note: type: "json" defaults to "off" to ensure production JSON logging is highly efficient.

  3. Understand the v5 JSON log schema changes

    master

    The structured (type: "json") output shape has changed significantly from v4.

    Key differences:

    • Message: Now under message (configurable via json.messageKey) instead of "0".
    • Level: Now under level (the name) and optionally levelId (the numeric ID).
    • Timestamp: Now under time (ISO string from _logMeta.date).
    • Metadata: Runtime metadata is under _logMeta with a schema version v: 5. Runtime names are now lowercase (e.g., "node", "browser").
    • User Fields: Plain objects passed to log methods are now spread at the top level of the JSON object.
    • Errors: Logged Error objects are placed under the error key (configurable via json.errorKey).

    Example Mapping:

    • log.info("hi", { userId: 42 }) $\rightarrow$ { "message": "hi", "userId": 42, ... }
  4. Replace overwrite hooks with middleware and custom transports

    master

    The overwrite.* object from v4 has been removed. Its functionality is now split into two distinct extension points:

    1. logger.use(middleware): Use this to enrich or rewrite the LogContext before the record is built. You can modify ctx.args, ctx.meta, or drop a log entirely by returning null or false. This replaces overwrite.addMeta, overwrite.toLogObj, and overwrite.addPlaceholders.

    2. Custom Transport with format: Use this to control the final output shape and destination. This replaces overwrite.transportJSON and overwrite.transportFormatted. You can provide a LogFormatter to the format property to define how the record is turned into a string/object before being passed to write.

    Mapping Summary:

    • overwrite.mask(args) $\rightarrow$ middleware or built-in mask group.
    • overwrite.addMeta(...) $\rightarrow$ middleware writing to ctx.meta.
    • overwrite.transportJSON(...) $\rightarrow$ Transport with format: "json".
    // Example: Using middleware to add metadata and filter levels
    const log = new Logger();
    log.use((ctx) => {
      // metadata is attached under _logMeta
      ctx.meta.traceId = getTraceId();
      // drop logs below INFO (level 3)
      return ctx.logLevelId >= 3 ? ctx : null;
    });
    
    // Example: Custom transport with a JSON formatter
    import type { LogFormatter } from "tslog";
    
    const log = new Logger();
    log.attachTransport({
      name: "backend",
      format: "json",
      write: (_record, line) => myBackend.send(line),
    });
  5. Automatic Source-Mapped Error Positions

    master

    On Node, Bun, and Deno (when NODE_ENV !== "production"), tslog automatically resolves file, line, and column numbers through source maps. This ensures that error stacks and _logMeta.path point to your original .ts source files instead of compiled/transpiled output.

    Configuration:

    • Controlled by the environment by default.
    • Force on/off using TSLOG_SOURCE_MAPS=on or TSLOG_SOURCE_MAPS=off.
  6. Choose the right tslog build

    master

    While the main tslog package is designed to work automatically in most environments, specific use cases can benefit from specialized builds. All builds share the same settings language and JSON shape.

    SituationImportPurpose
    DevelopmenttslogZero config: colorized pretty output, code positions, and validation hints.
    ProductiontslogAutomatically uncolors when piped. Opt-in to type: "json" for structured logs.
    Size-criticaltslog/slim~9.8KB gzip. Removes masking, pretty output, and stack capture. Throws if mask or type: "pretty" is used.
    Testingtslog/testingUse createTestLogger() to capture records for assertions without console noise.
    Browser Debuggingtslog/liteThin console wrappers that preserve the caller's file:line in DevTools.
    Human-readable Prod Logstslog CLIPipe NDJSON into the CLI to get pretty rendering: cat logs.json | npx tslog -l warn.
  7. Enable original TypeScript source positions in error stacks

    master

    By default, tslog resolves error stack positions through source maps to show your original .ts file/line/column instead of the compiled .js position. This works automatically with tsc, esbuild, webpack, Rollup, and Turbopack/Next.js.

    In production, this is disabled by default to avoid the cost of source-map parsing. You can force it using the environment variable TSLOG_SOURCE_MAPS=on. This feature is available for Node, Bun, and Deno.

    const log = new Logger();
    
    try {
      riskyCall(); // throws from compiled dist/app.js, which has a sourceMappingURL
    } catch (err) {
      log.error(err); // stack frames report src/app.ts, not dist/app.js
    }
  8. Enable source-mapped error positions

    master

    In Node.js, Bun, and Deno, v5 automatically resolves stack frames through source maps back to your original .ts files (including line/column) when logging an Error.

    • Default behavior: Enabled automatically when NODE_ENV !== "production".
    • Force enable: Set the environment variable TSLOG_SOURCE_MAPS=on.
    • Force disable: Set the environment variable TSLOG_SOURCE_MAPS=off.

    Note: Browsers use native devtools for this, so tslog does not attempt source map resolution in browser environments.

  9. Optimize logging for LLMs and Agents

    master

    tslog provides specific features to support AI agents and LLM applications:

    • llms.txt: The package includes an llms.txt file to provide agents with a condensed API surface.
    • Fields-first calls: Encourages structured, queryable logs for tool calls and traces.
    • isLevelEnabled(level): Use this to guard expensive payload generation (like large prompts or token counts) so they are only computed if the log level is active.
    • Request/Agent Correlation: Use runInContext(ctx, fn) to attach correlation IDs (like requestId) to all logs within a callback. This uses Node's AsyncLocalStorage and propagates automatically on Node, Deno, and Bun.

    Note for Cloudflare Workers: Since AsyncLocalStorage cannot be auto-resolved, you must manually inject it via the contextStorage setting using the nodejs_als or nodejs_compat compatibility flag.

    // Guard expensive payloads
    if (log.isLevelEnabled("DEBUG")) {
      log.debug({ prompt: buildExpensivePrompt() });
    }
    
    // Correlate logs within a context
    await log.runInContext({ requestId: "abc123" }, async () => {
      log.info("handling request"); // _logMeta carries requestId
      await doWork();
    });
    
    // Manual context injection for environments like Cloudflare Workers
    import { AsyncLocalStorage } from "node:async_hooks";
    const log = new Logger({ contextStorage: new AsyncLocalStorage() });
  10. Use interactive objects in the browser with `passObjectsNatively`

    master

    In browser environments, pretty.passObjectsNatively is on by default. This allows non-Error arguments to be passed to the console method by reference. This means DevTools will render objects and arrays as collapsible, clickable trees rather than static strings.

    When to disable it (passObjectsNatively: false):

    1. Log-time snapshots: Native mode is lazy; if an object mutates after the log call, DevTools shows the current state. Setting this to false renders the object to a string immediately, freezing its value at log time.
    2. Text-matchable logs: DevTools filters and automated tools (like test runners) can only match the rendered string. They cannot see inside natively-passed objects.
    new Logger({
      type: "pretty",
      pretty: {
        // on by default in browsers
        levelMethod: { WARN: console.warn, ERROR: console.error, FATAL: console.error },
      },
    });
    
    log.info("user loaded", { id: 42, roles: ["admin"] }); // object is collapsible in DevTools
  11. Use Sub-loggers for Contextual Tracing

    master

    Sub-loggers (created via getSubLogger() or the child() alias) allow you to create specialized loggers that inherit settings from a parent.

    Key Features:

    • Name Accumulation: Names are stored in _logMeta.parentNames, allowing you to trace the hierarchy (e.g., app -> db -> query).
    • Bindings: Use bindings to attach static fields to every log record in the sub-logger. These fields merge down the chain and are masked according to your mask settings.
    • Overriding: You can override the default logObj for a specific child by passing a second argument to getSubLogger().
    const main = new Logger({ name: "app", bindings: { service: "checkout" } });
    const db = main.getSubLogger({ name: "db" });
    
    // Create a child with specific overrides
    const query = db.child({ 
      name: "query", 
      minLevel: "DEBUG", 
      bindings: { pool: "primary" } 
    });
    
    query.info("slow query", { ms: 812 });
    // Output includes: service: "checkout", pool: "primary", and parentNames: ["app", "db"]
  12. Understand Log Levels in tslog

    master

    tslog provides seven default log levels via the LogLevel enum. You can use these levels to categorize logs and control visibility using minLevel.

    Default Levels:

    • SILLY (0): log.silly()
    • TRACE (1): log.trace()
    • DEBUG (2): log.debug()
    • INFO (3): log.info()
    • WARN (4): log.warn()
    • ERROR (5): log.error()
    • FATAL (6): log.fatal()

    To suppress logs below a certain threshold, set minLevel during instantiation or use setMinLevel() at runtime.

    import { Logger, LogLevel } from "tslog";
    
    // Set threshold to WARN (suppresses INFO, DEBUG, etc.)
    const log = new Logger({ minLevel: LogLevel.WARN });
    
    log.info("hidden");
    log.warn("visible");
    
    // Change threshold at runtime
    log.setMinLevel("DEBUG");