LogTape

repository·main·Indexed 23 days ago

https://github.com/dahlia/logtape

A logging framework that provides adapters to bridge LogTape-enabled libraries with existing logging infrastructures, including pino, winston, log4js, and bunyan. It supports multiple runtimes including Node.js, Bun, and Deno.

Tokens
123.1K
Snippets
343
Records
528
Agent score
78%

What's inside LogTape

  1. Overview of LogTape

    main

    LogTape is a zero-dependency logging library for JavaScript and TypeScript designed with a library-first philosophy. It is built to be unobtrusive: libraries can log without configuration, while applications maintain full control over how those logs are handled.

    Key features include:

    • Universal Runtime Support: Works in Deno, Node.js, Bun, browsers, and edge functions.
    • Structured Logging: Support for logging messages with structured data.
    • Hierarchical Categories: Manage log verbosity using a hierarchical category system.
    • Template Literals: Use template literals for log messages with placeholders.
    • Data Redaction: Built-in capabilities to redact sensitive information via patterns or specific fields.
    • Extensible Sinks: Easily add custom sinks for log output.
    • Framework Integrations: First-class support for web frameworks (Express, Fastify, Hono, Koa, GraphQL Yoga) and ORMs (Drizzle ORM) for automatic logging of HTTP requests, GraphQL operations, and database queries.
  2. Overview of LogTape features

    main

    LogTape is a zero-dependency logging library designed for both libraries and applications. Key features include:

    • Library Support: Libraries can use LogTape to provide logging without requiring configuration from the end-user.
    • Runtime Diversity: Works across Deno, Node.js, Bun, browsers, and edge functions.
    • Structured Logging: Support for logging messages with structured data.
    • Hierarchical Categories: Manage loggers and verbosity via a hierarchical category system.
    • Template Literals: Use template literals for logging with placeholders.
    • Data Redaction: Built-in pattern-based or field-based redaction of sensitive information.
    • Extensibility: Easy to add custom sinks and includes first-class integrations for frameworks like Express, Fastify, Hono, and Drizzle ORM.
  3. Overview of LogTape syslog sink

    main

    The @logtape/syslog package provides a syslog sink for LogTape that sends log messages to a syslog server following the [RFC 5424] specification.

    Key features include:

    • RFC 5424 compliant: Follows the official syslog protocol specification.
    • Multiple transports: Supports both UDP and TCP protocols.
    • Cross-runtime: Works on Deno, Node.js, and Bun.
    • Non-blocking: Asynchronous message sending with proper cleanup.
    • Configurable: Extensive configuration options for facility, hostname, etc.
    • Structured logging: Optional structured data support.
    • Zero dependencies: No external dependencies.
  4. Use file sinks in LogTape

    main

    The @logtape/file package provides two main types of sinks for writing logs to the filesystem:

    • File sink: A standard sink for writing log records to a file.
    • Rotating file sink: A sink that manages log files by rotating them (e.g., based on size or time) to prevent single files from growing indefinitely.

    For detailed configuration and usage instructions, refer to the official documentation at logtape.org/sinks/file.

  5. What is a filter and how to use it

    main

    A filter is a function that determines whether a log record should be passed to sinks or discarded. A Filter takes a LogRecord and returns a boolean.

    To use filters:

    1. Define filter functions in the filters object of the configure() function.
    2. Assign filter names to specific loggers in the loggers array.

    Example of a custom filter that checks for a property:

    import { configure, type LogRecord } from "@logtape/logtape";
    
    await configure({
      filters: {
        tooSlow(record: LogRecord) {
          return "elapsed" in record.properties
            && typeof record.properties.elapsed === "number"
            && record.properties.elapsed >= 100;
        },
      },
      loggers: [
        {
          category: ["my-app", "database"],
          sinks: ["console"],
          filters: ["tooSlow"],
        }
      ]
    });
    import type { LogRecord } from "@logtape/logtape";
    export type Filter = (record: LogRecord) => boolean;
  6. What is a Sink in LogTape

    main

    A Sink is a destination for log messages. In LogTape, a sink is a function that receives a LogRecord. You can use built-in sinks like console and stream, or implement your own custom sink by providing a function that matches the Sink signature.

    import type { LogRecord } from "@logtape/logtape";
    
    export type Sink = (record: LogRecord) => void;
  7. Overview of Text Formatters

    main

    A text formatter is a function that converts a log record into a string. LogTape provides several built-in sinks that accept text formatters, including:

    • console sink
    • stream sink
    • file sink
    • rotating file sink

    You can also write custom sinks that utilize a text formatter.

  8. Capture dynamic context with `lazy()`

    main

    The lazy() function allows you to defer the evaluation of context values until logging time. This is essential when you need to log values that change over the lifetime of your application (e.g., a user session or a request ID) or when you want to avoid capturing a stale value at the time the logger is initialized via .with().

    When using .with(), LogTape captures the current value of the provided object. By wrapping the value in lazy(() => value), the callback is invoked at the moment the log is actually recorded, ensuring child loggers always see the most up-to-date data.

    import { getLogger, lazy } from "@logtape/logtape";
    
    let currentUser: User | null = null;
    
    // lazy() wraps a function that will be called at logging time
    const rootLogger = getLogger("app").with({
      user: lazy(() => currentUser
        ? { id: currentUser.id, isAdmin: currentUser.isAdmin }
        : null
      )
    });
    
    const featureLogger = rootLogger.getChild("feature");
    
    // No user yet
    featureLogger.info("Initialization");  // user: null
    
    // User data loads
    currentUser = await loadUser();
    
    // Now logs reflect the current user
    featureLogger.info("User action");  // user: { id: 1, isAdmin: true }
    
    // User data changes
    currentUser.isAdmin = false;
    
    // Logs always show the latest value
    featureLogger.info("Another action");  // user: { id: 1, isAdmin: false }
  9. Enable request context and correlation IDs

    main

    Setting context: true adds request-scoped correlation fields. By default, the middleware reads the x-request-id header, generates one if missing, writes it to the response header, and adds requestId to the log record.

    To ensure logs emitted by your route handlers inherit the same requestId, you must configure LogTape with contextLocalStorage using AsyncLocalStorage:

    import { AsyncLocalStorage } from "node:async_hooks";
    import { configure } from "@logtape/logtape";
    
    await configure({
      // ... sinks and loggers ...
      contextLocalStorage: new AsyncLocalStorage(),
    });
    
    app.use(honoLogger({ context: true }));

    Customizing Context

    You can customize request ID headers and include additional fields:

    app.use(honoLogger({
      context: {
        requestId: {
          headerNames: ["x-correlation-id", "x-request-id"],
          responseHeader: "x-request-id",
        },
        include: ["requestId", "method", "path", "userAgent"],
        enrich: (c) => ({ route: c.req.path }),
      },
    }));
    app.use(honoLogger({
      context: {
        requestId: {
          headerNames: ["x-correlation-id", "x-request-id"],
          responseHeader: "x-request-id",
        },
        include: ["requestId", "method", "path", "userAgent"],
        enrich: (c) => ({ route: c.req.path }),
      },
    }));
  10. Understand @logtape/testing-node API compatibility

    main

    The @logtape/testing-node package is designed to be a drop-in replacement for node:test while adding LogTape failure reporting.

    Supported Node.js test features:

    • Shorthand helpers: test.only(), test.skip(), and test.todo().
    • Lifecycle hooks: describe(), before(), after(), beforeEach(), and afterEach() (these are re-exported unchanged from node:test).

    LogTape-specific exports:

    • test: The primary test function (replaces node:test).
    • it(): A wrapped alias for test.
    • createIt(): A function to create custom it instances.
  11. How LogTape's hierarchical category system works

    main

    LogTape uses a hierarchical category system based on arrays of strings (e.g., ["my-app", "my-module"]).

    When a message is logged, it is dispatched to all loggers whose categories are prefixes of the message's category. For example, a message with category ["my-app", "my-module", "my-submodule"] will be dispatched to loggers configured for ["my-app"] and ["my-app", "my-module"].

    This hierarchy allows you to control verbosity by setting different lowestLevel thresholds at various levels of the category tree.

    import { getFileSink } from "@logtape/file";
    import { configure, getConsoleSink } from "@logtape/logtape";
    
    await configure({
      sinks: {
        console: getConsoleSink(),
        file:    getFileSink("app.log"),
      },
      loggers: [
        { category: ["my-app"],              lowestLevel: "info",  sinks: ["file"] },
        { category: ["my-app", "my-module"], lowestLevel: "debug", sinks: ["console"] },
      ],
    })
  12. Configure loggers and handle sink inheritance

    main

    Loggers connect categories to sinks and filters.

    Inheritance Behavior: By default, loggers inherit the sinks that their ancestor categories have enabled for a specific record's level. Each ancestor's lowestLevel remains in effect. To prevent this and only use the sinks explicitly defined for the logger, set parentSinks: "override".

    Constraint: Defining multiple loggers with the same category is disallowed and will throw a ConfigError during configure().

    import { configure } from "@logtape/logtape";
    // ---cut-before---
    await configure({
      // ... sinks and filters configuration
      loggers: [
        {
          category: "my-app",
          lowestLevel: "info",
          sinks: ["console"],
        },
        {
          category: ["my-app", "database"],
          lowestLevel: "debug",
          sinks: ["file"],
          filters: ["noDebug"],
        },
        {
          category: ["my-app", "user-service"],
          lowestLevel: "info",
          sinks: ["file"],
          filters: ["containsUserData"],
        },
      ],
    });