Skip to main content
Home

latest

Logi is the simplest JSON logging "framework" for TypeScript.

This package works with Cloudflare Workers, Node.js, Deno, Bun, Browsers
This package works with Cloudflare Workers
This package works with Node.js
This package works with Deno
This package works with Bun
This package works with Browsers
JSR Score
100%
Published
3 months ago (0.3.0)

Logi

Logi is the simplest JSON logging "framework" for TypeScript.

Usage

import { logi } from '@soapbox/logi';

logi({ level: 'info', ns: 'myapp.startup', started: true });
logi({ level: 'debug', ns: 'myapp.sql', sql: 'SELECT * FROM users LIMIT $1', parameters: [20] });
logi({ level: 'warn', ns: 'myapp.config', message: 'missing TRUSTED_PROXIES, rate limiting will be disabled' });
logi({ level: 'error', ns: 'myapp.http', status: 500, client_ip: '192.168.1.2', message: 'Something went wrong!' });

Why?

Many people think they need a "production ready" logging framework with all sorts of methods and configuration options, and that no such thing exists. Much developer time and resources have been wasted creating fancy loggers that are essentially glorified wrappers of console.log.

Take pino, for example. This logger has nearly 2 thousand commits, 11 runtime dependencies, a "team" page on their website, and a "long term support" release schedule, all to essentially call console.log.

Logi contains about 6 lines of runtime code, 5 types, and 0 dependencies. It is easy to understand how it works instantly, and you are encouraged to copy-paste it instead of including it as a dependency if you prefer.

This is because Logi is not a technology solution, but rather a set of decisions that have been made about how TypeScript logging should work, and about separation of responsibilities.

Code

There are a few different versions of Logi, depicted below.

Simplest Version

const logi = (log: LogiLog): void => {
  console.log(JSON.stringify(log));
};

export interface LogiLog {
  level: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'critical';
  ns: string;
  [k: string]: LogiValue | undefined;
}

export interface LogiHandler {
  (log: LogiLog): void;
}

export type LogiValue = JsonValue | { toJSON(): JsonValue };

type JsonValue =
  | { [key: string]: JsonValue | undefined }
  | JsonValue[]
  | string
  | number
  | boolean
  | null;

Full Source

The is the full version of Logi provided by this repo, with comments removed for clarify.

export const logi: Logi = (log: LogiLog): void => {
  logi.handler(log);
};

logi.handler = (log): void => {
  console.log(JSON.stringify(log));
};

export interface Logi extends LogiHandler {
  handler: LogiHandler;
}

export interface LogiLog {
  level: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'critical';
  ns: string;
  [k: string]: LogiValue | undefined;
}

export interface LogiHandler {
  (log: LogiLog): void;
}

export type LogiValue = JsonValue | { toJSON(): JsonValue };

type JsonValue =
  | { [key: string]: JsonValue | undefined }
  | JsonValue[]
  | string
  | number
  | boolean
  | null;

This version defines an official LogiLog type, with level and ns properties.

It also has an extra logi.handler property that lets library users globally configure Logi.

For a fully commented version with TSDocs, see: logi.ts or jsr:@soapbox/logi.

Required Fields

Logi accepts a single log object, with 2 required fields:

  • level: one of trace, debug, info, warn, error, fatal, or critical
  • ns: logger namespace separated by dots (reverse DNS-ish)

Additional custom fields may also be added to any log, including nested objects, as long as all values are JSON-compatible or include a .toJSON() method.

Custom Handler

By overwriting logi.handler, any custom behavior can be implemented.

Include a timestamp on all logs

logi.handler = (log: LogiLog): void => {
  log.ts = Date.now() / 1000;
};

Filter out logs by level

logi.handler = (log: LogiLog): void => {
  if (log.level === 'trace') {
    return;
  }
};

Use JSON in production and plaintext in development

logi.handler = (log: LogiLog): void => {
  if (env === 'production') {
    console.log(JSON.stringify(log));
  }
  if (env === 'development') {
    const { level, ns, message = '', ...rest } = log;
    console.log(level.toUpperCase(), ns, message, ...rest);
  }
};

With this approach, it's possible to filter logs, format logs differently, and send logs to different backends under a variety of circumstances.

Expensive Logs

If the log itself is computationally expensive, and you might filter it out in the handler based on a certain condition anyway, you can wrap the call in the same conditional:

if (shouldLogStats) {
  logi({ level: 'debug', ns: 'myapp.stats', stats: calculateStats() });
}

JSON-first Logging

Instead of methods like log.info(), log.error(), etc, Logi has you include the full JSON log (with a required level property) in the log itself.

This makes the input match the output, giving it simplicity and clarity. No property merging is necessary. TypeScript enforces the log shape.

Since the input must always be a single JSON object, the full benefits of JSON logging are available to custom handlers, so logs can be easily formatted however you want in different environments.

Console Logging

Generally, logs should be made directly to the console, not to a file or external service. This is not to say you should not store logs elsewhere, just that it's usually the job of the operating system, not the runtime, to send the logs where they need to go.

That being said, there are a few options for runtimes, ranging from good to hacky:

  • In Deno, the OpenTelemetry integration can be used to forward logs from the console elsewhere.
  • Using a Logi custom handler, it's possible to send logs elsewhere.
  • The console global can be stubbed at runtime to send logs elsewhere.

Modern Linux systems typically use systemd, which captures console output and can be configured to write to another file of your choosing. Programs like Promtail can be installed on the host to forward logs elsewhere. Logging to the console and then dealing with it on the OS is usually the correct solution.

Don't let a sysadmin at your company get away with not implementing proper logging, and forcing you, the developer, to deal with it. It's their job.

Colors

By default, Logi does not output logs with colors for increased compatibility. To enable colors, use a custom handler:

import { logi } from '@soapbox/logi';
import { bgRed, blue, gray, green, magenta, red, white, yellow } from '@std/fmt/colors';

export const colorHandler: LogiHandler = (log: LogiLog): void => {
  const data = JSON.stringify(log);

  switch (log.level) {
    case 'trace':
      return console.log(gray(data));
    case 'debug':
      return console.log(blue(data));
    case 'info':
      return console.log(green(data));
    case 'warn':
      return console.log(yellow(data));
    case 'error':
    case 'fatal':
      return console.log(red(data));
    case 'critical':
      return console.log(bgRed(white(data)));
    default:
      return console.log(data);
  }
};

logi.handler = colorHandler;

Alternatively, don't do that, and just add an alias for jq to your .bashrc instead:

alias logcolor="jq -Rr '
  fromjson? |
  if .level == \"trace\" then \"\u001b[90m\(. | tostring)\u001b[0m\"     # Gray
  elif .level == \"debug\" then \"\u001b[34m\(. | tostring)\u001b[0m\"   # Blue
  elif .level == \"info\" then \"\u001b[32m\(. | tostring)\u001b[0m\"    # Green
  elif .level == \"warn\" then \"\u001b[33m\(. | tostring)\u001b[0m\"    # Yellow
  elif .level == \"error\" then \"\u001b[31m\(. | tostring)\u001b[0m\"   # Red
  elif .level == \"fatal\" then \"\u001b[35m\(. | tostring)\u001b[0m\"   # Magenta
  elif .level == \"critical\" then \"\u001b[41m\u001b[97m\(. | tostring)\u001b[0m\" # Bright Red Background, White Text
  else \"\(. | tostring)\" # Default, no color
  end
'"

Then you can pipe commands to logcolor, eg:

deno task dev | logcolor

That's the power of JSON logs!

Logi

Prometheus is the god of fire, Loki is known as the "Nordic Prometheus", and "logi" means "fire" in Old Norse.

Logi is designed with Loki in mind, and its level property matches the values Loki detects automatically.

Final Note

It took me longer to write this README than to develop this software. That should show how overengineered logging solutions have become.

License

This is free and unencumbered software released into the public domain.

New Ticket: Report package

Please provide a reason for reporting this package. We will review your report and take appropriate action.

Please review the JSR usage policy before submitting a report.

Add Package

deno add jsr:@soapbox/logi

Import symbol

import * as logi from "@soapbox/logi";
or

Import directly with a jsr specifier

import * as logi from "jsr:@soapbox/logi";

Add Package

pnpm i jsr:@soapbox/logi
or (using pnpm 10.8 or older)
pnpm dlx jsr add @soapbox/logi

Import symbol

import * as logi from "@soapbox/logi";

Add Package

yarn add jsr:@soapbox/logi
or (using Yarn 4.8 or older)
yarn dlx jsr add @soapbox/logi

Import symbol

import * as logi from "@soapbox/logi";

Add Package

npx jsr add @soapbox/logi

Import symbol

import * as logi from "@soapbox/logi";

Add Package

bunx jsr add @soapbox/logi

Import symbol

import * as logi from "@soapbox/logi";