npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@logtape/logtape

v0.4.0

Published

Simple logging library with zero dependencies for Deno/Node.js/Bun/browsers

Downloads

2,009

Readme

LogTape

JSR npm GitHub Actions Codecov

[!NOTE] LogTape is still in the early stage of development. The API is not stable yet. Please be careful when using it in production.

LogTape is a logging library for JavaScript and TypeScript. It provides a simple and flexible logging system that is easy to use and easy to extend. The highlights of LogTape are:

  • Zero dependencies: LogTape has zero dependencies. You can use LogTape without worrying about the dependencies of LogTape.

  • Library support: LogTape is designed to be used in libraries as well as applications. You can use LogTape in libraries to provide logging capabilities to users of the libraries.

  • Runtime diversity: LogTape supports Deno, Node.js, Bun, edge functions, and browsers. You can use LogTape in various environments without changing the code.

  • Structured logging: You can log messages with structured data.

  • Hierarchical categories: LogTape uses a hierarchical category system to manage loggers. You can control the verbosity of log messages by setting the log level of loggers at different levels of the category hierarchy.

  • Template literals: LogTape supports template literals for log messages. You can use template literals to log messages with placeholders and values.

  • Dead simple sinks: You can easily add your own sinks to LogTape.

Currently, LogTape provides only few sinks, but you can easily add your own sinks.

Installation

Deno

deno add @logtape/logtape

Node.js

npm add @logtape/logtape

Bun

bun add @logtape/logtape

Quick start

Set up LogTape in the entry point of your application (if you are composing a library, you should not set up LogTape in the library itself; it is up to the application to set up LogTape):

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

await configure({
  sinks: { console: getConsoleSink() },
  filters: {},
  loggers: [
    { category: "my-app", level: "debug", sinks: ["console"] }
  ]
});

And then you can use LogTape in your application or library:

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

const logger = getLogger(["my-app", "my-module"]);

export function myFunc(value: number): void {
  logger.debug `Hello, ${value}!`;
}

How to log

There are total 5 log levels: debug, info, warning, error, fatal (in the order of verbosity). You can log messages with the following syntax:

logger.debug `This is a debug message with ${value}.`;
logger.info  `This is an info message with ${value}.`;
logger.warn  `This is a warning message with ${value}.`;
logger.error `This is an error message with ${value}.`;
logger.fatal `This is a fatal message with ${value}.`;

You can also log messages with a function call. In this case, log messages are structured data:

logger.debug("This is a debug message with {value}.", { value });
logger.info("This is an info message with {value}.", { value });
logger.warn("This is a warning message with {value}.", { value });
logger.error("This is an error message with {value}.", { value });
logger.fatal("This is a fatal message with {value}.", { value });

Sometimes, values to be logged are expensive to compute. In such cases, you can use a function to defer the computation so that it is only computed when the log message is actually logged:

logger.debug(l => l`This is a debug message with ${computeValue()}.`);
logger.debug("Or you can use a function call: {value}.", () => {
  return { value: computeValue() };
});

When using the function call, the way to log single curly braces { is to double the brace {{:

logger.debug("This logs {{single}} curly braces.");

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", "my-module"] and ["my-app"].

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

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

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

await configure({
  sinks: {
    console: getConsoleSink(),
  },
  filters: {},
  loggers: [
    { category: ["my-app"], level: "info", sinks: ["console"] },
    { category: ["my-app", "my-module"], level: "debug", sinks: ["console"] },
  ],
})

Sinks

A sink is a destination of log messages. LogTape currently provides a few sinks: console and stream. However, you can easily add your own sinks. The signature of a sink is:

export type Sink = (record: LogRecord) => void;

Here's a simple example of a sink that writes log messages to console:

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

await configure({
  sinks: {
    console(record) {
      console.log(record.message);
    }
  },
  // Omitted for brevity
});

Console sink

Of course, you don't have to implement your own console sink because LogTape provides a console sink:

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

await configure({
  sinks: {
    console: getConsoleSink(),
  },
  // Omitted for brevity
});

See also getConsoleSink() function and ConsoleSinkOptions interface in the API reference for more details.

Stream sink

Another built-in sink is a stream sink. It writes log messages to a WritableStream. Here's an example of a stream sink that writes log messages to the standard error:

// Deno:
await configure({
  sinks: {
    stream: getStreamSink(Deno.stderr.writable),
  },
  // Omitted for brevity
});
// Node.js:
import stream from "node:stream";

await configure({
  sinks: {
    stream: getStreamSink(stream.Writable.toWeb(process.stderr)),
  },
  // Omitted for brevity
});

[!NOTE] Here we use WritableStream from the Web Streams API. If you are using Node.js, you cannot directly pass process.stderr to getStreamSink because process.stderr is not a WritableStream but a Writable, which is a Node.js stream. You can use Writable.toWeb() method to convert a Node.js stream to a WritableStream.

See also getStreamSink() function and StreamSinkOptions interface in the API reference for more details.

File sink

[!NOTE] File sink is unavailable in the browser environment.

LogTape provides a file sink as well. Here's an example of a file sink that writes log messages to a file:

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

await configure({
  sinks: {
    file: getFileSink("my-app.log"),
  },
  // Omitted for brevity
});

See also getFileSink() function and FileSinkOptions interface in the API reference for more details.

[!NOTE] On Deno, you need to have the --allow-write flag and the --unstable-fs flag to use the file sink.

Rotating file sink

[!NOTE] Rotating file sink is unavailable in the browser environment.

A rotating file sink is a file sink that rotates log files. It creates a new log file when the current log file reaches a certain size. Here's an example of a rotating file sink that writes log messages to a file:

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

await configure({
  sinks: {
    file: getRotatingFileSink("my-app.log", {
      maxFileSize: 1024 * 1024,  // 1 MiB
      maxFiles: 5,
    }),
  },
  // Omitted for brevity
});

Rotated log files are named with a suffix like .1, .2, .3, and so on.

For more details, see getRotatingFileSink() function and RotatingFileSinkOptions interface in the API reference.

[!NOTE] On Deno, you need to have the --allow-write flag and the --unstable-fs flag to use the rotating file sink.

Text formatter

A stream sink and a file sink write log messages in a plain text format. You can customize the format by providing a text formatter. The type of a text formatter is:

export type TextFormatter = (record: LogRecord) => string;

Here's an example of a text formatter that writes log messages in a JSON Lines format:

await configure({
  sinks: {
    stream: getFileSink("log.jsonl", {
      formatter(log) {
        return JSON.stringify(log) + "\n",
      }
    }),
  },
  // Omitted for brevity
})

[!TIP] If you want to monitor log messages formatted in JSON Lines in real-time readably, you can utilize the tail and jq commands:

tail -f log.jsonl | jq .

Disposable sink

[!TIP] If you are unfamiliar with the concept of disposables, see also the proposal of ECMAScript Explicit Resource Management.

A disposable sink is a sink that can be disposed of. They are automatically disposed of when the configuration is reset or the program exits. The type of a disposable sink is: Sink & Disposable. You can create a disposable sink by defining a [Symbol.dispose] method:

const disposableSink: Sink & Disposable = (record: LogRecord) => {
  console.log(record.message);
};
disposableSink[Symbol.dispose] = () => {
  console.log("Disposed!");
};

A sink can be asynchronously disposed of as well. The type of an asynchronous disposable sink is: Sink & AsyncDisposable. You can create an asynchronous disposable sink by defining a [Symbol.asyncDispose] method:

const asyncDisposableSink: Sink & AsyncDisposable = (record: LogRecord) => {
  console.log(record.message);
};
asyncDisposableSink[Symbol.asyncDispose] = async () => {
  console.log("Disposed!");
};

Explicit disposal

You can explicitly dispose of a sink by calling the dispose() method. It is useful when you want to flush the buffer of a sink without blocking returning a response in edge functions. Here's an example of using the dispose() with ctx.waitUntil() in Cloudflare Workers:

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

export default {
  async fetch(request, env, ctx) {
    await configure({ /* ... */ });
    // ...
    ctx.waitUntil(dispose());
  }
}

Filters

A filter is a function that filters log messages. A filter takes a log record and returns a boolean value. If the filter returns true, the log record is passed to the sinks; otherwise, the log record is discarded. Its signature is:

export type Filter = (record: LogRecord) => boolean;

For example, the following filter discards log messages whose property elapsed is less than 100 milliseconds:

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

await configure({
  // Omitted for brevity
  filters: {
    tooSlow(record: LogRecord) {
      return "elapsed" in record.properties && record.properties.elapsed >= 100;
    },
  },
  loggers: [
    {
      category: ["my-app", "database"],
      level: "debug",
      sinks: ["console"],
      filters: ["tooSlow"],
    }
  ]
});

Level filter

LogTape provides a built-in level filter. You can use the level filter to filter log messages by their log levels. The level filter factory takes a LogLevel string and returns a level filter. For example, the following level filter discards log messages whose log level is less than info:

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

await configure({
  filters: {
    infoOrHigher: getLevelFilter("info");
  },
  // Omitted for brevity
});

Sink filter

A sink filter is a filter that is applied to a specific sink. You can add a sink filter to a sink by decorating the sink with withFilter():

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

await configure({
  sinks: {
    filteredConsole: withFilter(
      getConsoleSink(),
      log => "elapsed" in log.properties && log.properties.elapsed >= 100,
    ),
  },
  // Omitted for brevity
});

The filteredConsoleSink only logs messages whose property elapsed is greater than or equal to 100 milliseconds to the console.

[!TIP] The withFilter() function can take a LogLevel string as the second argument. In this case, the log messages whose log level is less than the specified log level are discarded.

Testing

Here are some tips for testing your application or library with LogTape.

Reset configuration

You can reset the configuration of LogTape to its initial state. This is useful when you want to reset the configuration between tests. For example, the following code shows how to reset the configuration after a test (regardless of whether the test passes or fails) in Deno:

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

Deno.test("my test", async (t) => {
  await t.step("set up", async () => {
    await configure({ /* ... */ });
  });

  await t.step("run test", () => {
    // Run the test
  });

  await t.step("tear down", async () => {
    await reset();
  });
});

Buffer sink

For testing purposes, you may want to collect log messages in memory. Although LogTape does not provide a built-in buffer sink, you can easily implement it:

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

const buffer: LogRecord[] = [];

await configure({
  sinks: {
    buffer: buffer.push.bind(buffer),
  },
  // Omitted for brevity
});