Skip to content

Categories

LogTape uses a hierarchical category system to manage loggers. A category is a list of strings. For example, ["my-app", "my-module"] is a category.

When you log a message, it is dispatched to all loggers whose categories are prefixes of the category of the logger. For example, if you log a message with the category ["my-app", "my-module", "my-submodule"], it is dispatched to loggers whose categories are ["my-app"] and ["my-app", "my-module"].

This behavior allows you to control the verbosity of log messages by setting the lowestLevel of loggers at different levels of the category hierarchy.

Here's an example of setting log levels for different categories:

import {  } from "@logtape/file";
import { ,  } from "@logtape/logtape";

await ({
  : {
    : (),
    :    ("app.log"),
  },
  : [
    { : ["my-app"],              : "info",  : ["file"] },
    { : ["my-app", "my-module"], : "debug", : ["console"] },
  ],
})

With this configuration, a "debug" record from ["my-app", "my-module"] goes only to the console sink. An "info" record goes to both the console and file sinks because it also meets the parent logger's "info" threshold.

Sink inheritance and overriding

When you configure a logger, you can specify multiple sinks for the logger. By default, the logger combines its own sinks with the sinks that its parent would use for each log record. A parent sink is inherited only when the record's level meets the parent logger's lowestLevel. Inheritance does not copy the parent's sink identifiers into the child logger's configuration.

For example, the following configuration sets up two sinks, a and b, and configures the child logger to accept a lower level than its parent:

import { type LogRecord, ,  } from "@logtape/logtape";

const : LogRecord[] = [];
const : LogRecord[] = [];

await ({
  : {
    : ..(),
    : ..(),
  },
  : [
    { : ["my-app"], : "info", : ["a"] },
    {
      : ["my-app", "my-module"],
      : "debug",
      : ["b"],
    },
  ],
});

(["my-app", "my-module"]).("details");
// a = []
// b = [{ message: "details", ... }]

(["my-app", "my-module"]).("ready");
// a = [{ message: "ready", ... }]
// b = [{ message: "details", ... }, { message: "ready", ... }]

The "debug" record reaches only sink b: the child logger accepts it, but the parent logger does not. The "info" record reaches both sinks.

Inherited and local sink lists are concatenated without removing duplicates. If the same sink is configured on both a child and an enabled parent, that sink receives the record twice.

You can override the sinks inherited from the parent loggers by specifying parentSinks: "override" in the logger configuration. This is useful when you want to replace the inherited sinks with a different set of sinks:

import { type LogRecord, ,  } from "@logtape/logtape";

const : LogRecord[] = [];
const : LogRecord[] = [];

await ({
  : {
    : ..(),
    : ..(),
  },
  : [
    { : ["my-app"], : ["a"] },
    {
      : ["my-app", "my-module"],
      : ["b"],
      : "override", 
    },
  ],
});

(["my-app"]).("foo");
// a = [{ message: "foo", ... }]
// b = []

(["my-app", "my-module"]).("bar");
// a = [{ message: "foo", ... }]
// b = [{ message: "bar", ... }]

Root logger

The root logger is a special logger that acts as the parent of all other loggers in the hierarchical category system. It is represented by an empty array [] as its category. This logger is particularly useful for catch-all logging configurations where you want to capture logs from all categories without knowing the specific categories in advance.

import { ,  } from "@logtape/logtape";

await ({
  : {
    : (),
  },
  : [
    // Root logger catches all log messages
    { : [], : ["console"], : "info" },
  ],
});

The root logger will capture logs from all categories in your application and any libraries you're using. This is perfect for scenarios where you want to ensure you don't miss any logs without needing to know all the specific categories beforehand.

You can also combine the root logger with more specific loggers:

import {  } from "@logtape/file";
import { ,  } from "@logtape/logtape";

await ({
  : {
    : (),
    : ("app.log"),
  },
  : [
    // Write all categories to the file at info level
    { : [], : ["file"], : "info" },
    // More verbose logging for your specific app
    { : ["my-app"], : ["console"], : "debug" },
  ],
});

In this configuration, the file sink receives "info" and higher records from every category. The console sink also receives records from the ["my-app"] category, including "debug" records.

Child loggers

You can get a child logger from a parent logger by calling getChild():

const  = (["my-app"]);
const  = .("my-module");
// equivalent: const childLogger = getLogger(["my-app", "my-module"]);

The getChild() method can take an array of strings as well:

const  = (["my-app"]);
const  = .(["my-module", "foo"]);
// equivalent: const childLogger = getLogger(["my-app", "my-module", "foo"]);

Meta logger

The meta logger is a special logger in LogTape designed to handle internal logging within the LogTape system itself. It serves as a mechanism for LogTape to report its own operational status, errors, and important events. This is particularly useful for debugging issues with LogTape configuration or for monitoring the health of your logging system.

It is logged to the category ["logtape", "meta"], and it is automatically enabled when you call configure() without specifying the meta logger. To disable the meta logger, you can set the sinks property of the meta logger to an empty array.

To hide the startup notice from the meta logger, set its lowestLevel to "warning" or higher.

NOTE

On sink errors, the meta logger is used to log the error messages, but these messages are not logged to the same sink that caused the error. This is to prevent infinite loops of error messages when a sink error is caused by the sink itself.

TIP

Consider using a separate sink for the meta logger. This ensures that if there's an issue with your main sink, you can still receive meta logs about the issue:

import { ,  } from "@logtape/logtape";
import {  } from "./your-main-sink.ts";

await ({
  : {
    : (),
    : (),
  },
  : {},
  : [
    { : ["logtape", "meta"], : ["console"] },
    { : ["your-app"], : ["main"] },
  ],
});

Category prefix

Category prefix is available since LogTape 1.3.0.

When building layered library architectures (core libraries → SDKs → applications), you may want logs from internal libraries to appear under your SDK's category. The withCategoryPrefix() function allows you to prepend a category prefix to all log records within a callback context.

Settings

CAUTION

In order to use withCategoryPrefix(), your JavaScript runtime must support context-local states (like Node.js's node:async_hooks module). If your JavaScript runtime doesn't support context-local states, LogTape will silently ignore the category prefix.

As of November 2025, Node.js, Deno, and Bun support this feature. Web browsers don't support it yet.

See also TC39 Async Context proposal for web browsers.

To enable withCategoryPrefix(), you need to set a contextLocalStorage option in the configure() function. In Node.js, Deno, and Bun, you can use AsyncLocalStorage from the node:async_hooks module:

import {  } from "node:async_hooks";
import {  } from "@logtape/logtape";

await ({
  // ... other settings ...
  : new (),
});

NOTE

Without the contextLocalStorage option, withCategoryPrefix() will not prepend any prefix and will log a warning to the meta logger (["logtape", "meta"]).

Basic usage

import { ,  } from "@logtape/logtape";
import {  } from "core-library";

export function () {
  return (["my-sdk"], () => {
    // Any logs from core-library within this context
    // will have ["my-sdk"] prepended to their category
    return ();
  });
}

If coreLibraryFunction() logs with getLogger(["core-library"]), the final category will become ["my-sdk", "core-library"].

You can also pass a string instead of an array:

import {  } from "@logtape/logtape";

("my-sdk", () => {
  // Equivalent to withCategoryPrefix(["my-sdk"], () => { ... })
});

Nesting

Category prefixes can be nested and accumulate:

import { ,  } from "@logtape/logtape";

(["app"], () => {
  (["sdk-1"], () => {
    (["core-lib"]).("Hello");
    // Category: ["app", "sdk-1", "core-lib"]
  });
});

Combining with implicit contexts

withCategoryPrefix() works seamlessly with withContext():

import { , ,  } from "@logtape/logtape";

(["my-sdk"], () => {
  ({ : "abc-123" }, () => {
    (["internal"]).("Processing request: {requestId}");
    // Category: ["my-sdk", "internal"]
    // Properties include: { requestId: "abc-123" }
  });
});

Released under the MIT License.