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 oftrace
,debug
,info
,warn
,error
,fatal
, orcritical
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.
Add Package
deno add jsr:@soapbox/logi
Import symbol
import * as logi from "@soapbox/logi";
Import directly with a jsr specifier
import * as logi from "jsr:@soapbox/logi";
Add Package
pnpm i jsr:@soapbox/logi
pnpm dlx jsr add @soapbox/logi
Import symbol
import * as logi from "@soapbox/logi";
Add Package
yarn add jsr:@soapbox/logi
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";