Library / SDK
loglayer/loglayer avatar
loglayer/loglayer

LogLayer: one logging API over Pino, console, files and OpenTelemetry

A modern logging library for Javascript / Typescript that routes logs to various logging libraries, cloud providers, files, StatsD, and OpenTelemetry while providing a fluent API for specifying log messages, metadata and errors.

548 stars12 forksTypeScriptMIT

At a glance

What is it?
LogLayer is a TypeScript logging library that routes entries to whichever backend you choose, with a fluent API for context, metadata and errors, a plugin system for redaction, and mixins for wide events. It runs on Node.js, Bun, Deno and in the browser, and ships as a pnpm and turbo monorepo.
Who is it for?
LogLayer suits a TypeScript codebase that has outgrown console.log and wants structured logging without committing to a vendor's logger, since the transport layer is swappable and the same call sites work against console, Pino or a cloud provider.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 28 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem: log calls you cannot change later

LogLayer describes itself as a unified logger that routes logs to various logging libraries, cloud providers, files and OpenTelemetry, while providing a fluent API for specifying log messages, metadata and errors. It supports browser, Node.js, Bun and Deno.

The problem it addresses is one of lock-in. A codebase that calls pino.info directly, or console.log, has baked a choice into every call site. Swapping the backend later means either accepting two loggers, or rewriting every call. LogLayer puts a thin layer in between so that the call site stays the same while the transport changes underneath.

The README's example makes the point directly, with a comment that says you can also start out with a console logger and swap to another later. That is the whole pitch, and the rest of the library is the machinery for making the swap safe rather than lossy.

The project is MIT licensed, written in TypeScript, and the homepage is loglayer.dev with the full documentation there. There is also a Go version at go.loglayer.dev, which tells you the author intends this to be a family of libraries rather than a single package, and the repository is run by Theo Gravity and Disaresta.

The topic list is broad, covering bun, deno, electron, nextjs, nodejs, datadog, opentelemetry, pino, statsd, file-rotation and structured-logging, which is a reasonable map of the backends a JavaScript application might want to write to.

The fluent API: context, metadata, prefix and errors

The core call chain is where most of the design decisions live. Here is the README's example in full:

javascript
// Example using the Pino logging library with LogLayer
// You can also start out with a console logger and swap to another later!
import { LogLayer } from 'loglayer';
import { pino } from 'pino';
import { PinoTransport } from '@loglayer/transport-pino';
import { redactionPlugin } from '@loglayer/plugin-redaction';

const log = new LogLayer({
  // Multiple loggers can also be used at the same time. 
  transport: new PinoTransport({
    logger: pino()
  }),
  // Plugins modify log data before it's shipped to your logging library.
  plugins: [
    redactionPlugin({
      paths: ['password'],
      censor: '[REDACTED]',
    }),
  ],
  // Put context data in a specific field (default is flattened)
  contextFieldName: 'context',
  // Put metadata in a specific field (default is flattened)
  metadataFieldName: 'metadata',
})

// persisted data that is always included in logs
log.withContext({
   path: "/",
   reqId: "1234"
})

log.withPrefix("[my-app]")
  .withError(new Error('test'))
  // data that is included for this log entry only
  .withMetadata({ some: 'data', password: 'my-pass' })
  // Non-object data only (numbers, booleans, and strings only)
  // this can be omitted to just log an object / error
  // by using .errorOnly() / .metadataOnly() instead of withError() / withMetadata()
  .info('my message')

Three separate data channels run through it, and keeping them distinct is the useful idea here. withContext sets data that persists for every subsequent log entry, which in the example is a request path and a request id. withMetadata attaches data to one entry only. withPrefix adds a string prefix to the message.

The contextFieldName and metadataFieldName options control whether those two channels are nested under named fields or flattened into the top level of the log record, and the defaults are flattened. Nesting them, as the example does, means a logger that already has a field called context will not collide with yours, which is a real problem when several libraries write to the same record.

The errorOnly and metadataOnly variants exist for the case where you want to log only the error or only the metadata without a message, which matters when the error object already carries the message.

Plugins and transports: the two extension points

The configuration distinguishes two extension mechanisms, and the difference in when they run is the important part.

A transport is the destination. The README notes that multiple loggers can also be used at the same time, so one LogLayer instance can fan out to several backends. Transports are separate npm packages, with the naming convention visible in the example, @loglayer/transport-pino, and the topic list and release list showing the range, including @loglayer/transport-google-cloud-logging and a transport package at version 3.3.0.

A plugin modifies the log data before it is shipped, as the comment in the example says. The redactionPlugin shown takes paths and a censor string, and in the example it redacts a password field before the entry reaches Pino. That ordering matters: redaction inside the logging library means it applies to every transport at once, rather than each backend needing its own redaction configuration. It also means the secret is never written to a log backend, which is the difference between a redaction plugin and a filter that only hides it in one view.

The monorepo structure makes the layering explicit. Under packages/core there are separate packages for the context manager system, the log level manager system, the main LogLayer implementation, the plugin system, the transport system, shared utilities and types, and shared TypeScript configurations. Alongside are packages/context-managers, packages/log-level-managers, packages/transports, packages/mixins, packages/plugins and packages/integrations, the last being framework integrations such as ElysiaJS.

So the core is itself a set of packages with defined roles, and the officially supported implementations live beside them. That is more structure than a typical logging wrapper needs, and it buys the ability to swap the context manager or the level manager without touching the rest.

Wide events and the AsyncLocalStorage mixin

The Wide Events Mixin is the most interesting feature and the least conventional. The README says it enables comprehensive, self-contained log entries that capture an entire operation's context, and links to a page called Why Logging Sucks for background on the problem.

The idea is that instead of scattering a request across a dozen log lines, you accumulate context over the life of an operation and emit one self-contained entry at the end. The example shows the mechanics:

typescript
import { AsyncLocalStorage } from "node:async_hooks";
import { LogLayer, StructuredTransport, useLogLayerMixin } from 'loglayer';
import { createWideEventMixin } from '@loglayer/mixin-wide-events';

const asyncLocalStorage = new AsyncLocalStorage<{ logger: LogLayer }>();

useLogLayerMixin(createWideEventMixin({ asyncContext: asyncLocalStorage }));

const log = new LogLayer({
  transport: new StructuredTransport({ logger: console })
});

asyncLocalStorage.run({ logger: log }, () => {
  log.withWideEvents({ userId: "123" });
  // ... do work ...
  log.withWideEvents({ orderId: "456", duration: 150 });
  log.emitWideEvent({ message: "Order processed" });
  // Emits a single log with all accumulated data in one entry
});

AsyncLocalStorage is the mechanism that makes it work. Because Node's async context propagates across await boundaries and through concurrent tasks, the accumulated data stays attached to the right operation without being threaded through every function signature. The call to useLogLayerMixin registers the mixin globally, and then withWideEvents adds fields while emitWideEvent writes the single consolidated entry.

The StructuredTransport with console as the logger is used here rather than Pino, which is a useful detail: the mixin is transport-agnostic and works with the simplest possible backend, so you can develop against console and ship to a real logger.

Working in the monorepo

The repository is a monorepo using pnpm for package management and turbo for build orchestration. The development sequence is short:

Install the dependencies with pnpm install and the git hooks with pnpx lefthook install, then build all packages with turbo build. Tests run across all packages with:

bash
turbo test

Linting and formatting use Biome, and turbo lint runs it across all packages, with turbo lint:staged running lint only on staged files, which is what the git hooks use. The docs are built with vitepress, and there are two commands for that:

bash
pnpm run docs

to view them locally, and:

bash
pnpm run docs:build

to check the production build.

The root package.json shows the release machinery, which is worth noting because it explains the versioning you see on npm. Changesets handles versioning and publishing, with changeset version and changeset publish as separate scripts, and a publish-packages script that runs turbo run build first. Commitlint is configured with a conventional commit config, and syncpack enforces consistent dependency versions and package names across the workspace, with dedicated lint scripts for semver and mismatches. The engines field requires Node.js 20.16.0 or newer with engineStrict set to true, and the package manager is pinned to [email protected].

The published packages are versioned independently. The newest releases listed are @loglayer/[email protected] from 2026-09-25, [email protected] and @loglayer/[email protected] both from 2026-07-13, so the transport packages track their own versions rather than moving in step with the core.

Where LogLayer is the wrong choice

Three limits are structural. The first is the layer of indirection itself. Every log call now goes through LogLayer, through the plugin chain and out through a transport adapter, and the cost of that is paid on every entry. For a service that logs heavily on a hot path, the wrapper is real overhead against calling the underlying logger directly, and the README's own framing, that you can start with a console logger and swap later, is only a virtue if you actually swap.

The second is the dependency weight of the ecosystem. The core is one package, but a real setup pulls in a transport package, possibly a plugin package, and the framework integration packages on top of that, in a monorepo where packages are released at different times and the version numbers do not line up. A team that values a minimal dependency tree should weigh that.

The third is platform coverage of the features. The library claims browser, Node.js, Bun and Deno support, but the wide events mixin is built on AsyncLocalStorage from node:async_hooks, which is a Node-family API. A browser bundle that wants wide events is on different footing from a Node service, and the README's example imports the mixin from a separate package, so the capability is opt-in rather than universal.

The comparison worth making is with a single logger used directly. Pino on its own has no abstraction layer, no plugin chain and no transport indirection, and it is faster for that reason. The difference in approach is that LogLayer is about being able to change your mind later at the cost of paying a small tax now, and for an application whose logging backend is chosen per environment or per deployment target that trade is often worth it, while for an application that has already settled on one backend it is not.

The last push to the repository was on 2026-09-25, the repository is not archived, and the licence is MIT, so there is no copyleft obligation on code that merely uses it.

Editorial conclusion

LogLayer suits a TypeScript codebase that has outgrown console.log and wants structured logging without committing to a vendor's logger, since the transport layer is swappable and the same call sites work against console, Pino or a cloud provider. It is the wrong tool if you are content with one logger's own API, since adding a wrapper layer costs an indirection and a dependency, and it is the wrong tool for a project on Node.js older than 20.16.0, which the monorepo's engines field requires. Verify first which transports and plugins you actually need, and note that the newest core release listed is [email protected] from 2026-07-13 with the last push on 2026-09-25.

Frequently asked questions

What is LogLayer and what backends does it support?

LogLayer is a unified TypeScript logging library that routes logs to various logging libraries, cloud providers, files, StatsD and OpenTelemetry through a single fluent API for log messages, metadata and errors. It supports browser, Node.js, Bun and Deno.

What is the difference between withContext and withMetadata in LogLayer?

withContext sets data that persists and is included in every later log entry, while withMetadata attaches data to one entry only. The contextFieldName and metadataFieldName options choose whether each is nested under a named field or flattened into the top level, and the default is flattened.

How does the redaction plugin in LogLayer work?

Plugins modify log data before it is shipped to your logging library. The redactionPlugin takes paths and a censor string, for example paths ['password'] with censor '[REDACTED]', and it applies inside LogLayer so the redaction covers every transport at once.

What are wide events in LogLayer?

The Wide Events Mixin enables self-contained log entries that capture an entire operation's context, accumulating fields with withWideEvents and writing one consolidated entry with emitWideEvent. It uses Node's AsyncLocalStorage so the accumulated context follows the operation across async boundaries.

How do I build and test the LogLayer monorepo?

Install dependencies with pnpm install, install the git hooks with pnpx lefthook install, then run turbo build to build all packages and turbo test to run the tests. Linting uses Biome through turbo lint, and the docs are served with pnpm run docs.

Can LogLayer send logs to more than one place at once?

Yes. The configuration comment in the README notes that multiple loggers can also be used at the same time, so a single LogLayer instance can fan out to several transports rather than being limited to one destination.

Official sources

  1. License: MIT
  2. loglayer/loglayer on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/loglayer-loglayer.svg)](https://hysenlabs.com/projects/loglayer-loglayer)

Was this useful?