Skip to content

annexare/jsonl-logger

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

38 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Monthly Downloads NPM CI

JSONL Logger

Lightweight JSON Lines logger with pluggable formatters (Google Cloud Logging, VictoriaLogs, Elastic/ECS, Datadog, Pino). Modern ESM-only, zero dependencies, Bun-first, works on Node.js and Deno.

Outputs plain text for local development — auto-colored on a TTY, plain when piped or redirected — and structured JSON (JSONL) for production, switched by a single LOG_FORMAT environment variable with no code changes.

Next.js (and other hardcoded plain text logs) become JSON-only logging for systems where it is required.

Install

bun add jsonl-logger
# or
npm install jsonl-logger

Quick Start

import { logger } from 'jsonl-logger'

logger.info('Server started', { port: 3000 })
logger.log('Neutral message', { note: 'no level icon' })
logger.error('Request failed', { path: '/api' }, new Error('timeout'))

Output Modes

The active mode is selected by the LOG_FORMAT environment variable — no code changes required:

  • Plain text (default, LOG_FORMAT unset) — human-readable lines with a timestamp, level label, and inline context; colored when writing to a TTY. Ideal for local development. LOG_LEVEL defaults to debug.
  • Structured JSON (LOG_FORMAT set) — one JSON object per line (JSONL) shaped by the selected formatter. Ideal for production log pipelines. LOG_LEVEL defaults to info. See Formatters.

Plain-text output for the Quick Start example above:

18:42:05 ● Server started {"port":3000}
18:42:05   Neutral message {"note":"no level icon"}
18:42:05 ✖ Request failed {"path":"/api"}
Error: timeout
    at handler (/app/server.ts:12:9)

Color

Color is auto-detected: on when stdout is an interactive TTY, off when output is piped or redirected (e.g. to a log file or in CI). Override via the colors constructor option or the environment — precedence is colors option > FORCE_COLOR > NO_COLOR > TTY detection:

  • colors: true | false — explicit per-logger control, wins over everything below
  • FORCE_COLOR — set (to anything but 0/false) to force color on
  • NO_COLOR — set (to any value) to force color off (no-color.org)
import { Logger } from 'jsonl-logger'

const logger = new Logger({}, { colors: false }) // never colorize, regardless of TTY

Label Styles

In plain-text mode, the per-level label is controlled by the LOG_LABELS environment variable or the labels constructor option (the option takes precedence):

LOG_LABELS Labels Example line
icon (default) 18:42:05 ● Server started
text DEBUG INFO WARN ERROR FATAL 18:42:05 INFO Server started
none (timestamp only) 18:42:05 Server started
import { Logger } from 'jsonl-logger'

const logger = new Logger({}, { labels: 'text' }) // overrides LOG_LABELS

The neutral .log() method always renders without an icon or text label — just blank padding where the label would sit — so level-less lines stay visually distinct from leveled ones (any meta context is still appended). In JSON mode these styles are ignored; labels apply to plain text only.

Formatters

Set LOG_FORMAT to enable JSON output with a specific formatter:

Google Cloud Logging

LOG_FORMAT=google-cloud-logging bun run server.ts
# Output: {"message":"...","timestamp":"...","severity":"INFO",...}

VictoriaLogs

LOG_FORMAT=victoria-logs bun run server.ts
# Output: {"_msg":"...","_time":"...","level":"info",...}

Elastic Common Schema (ECS)

For the Elastic/ELK stack — ingested directly by Filebeat / Elastic Agent:

LOG_FORMAT=ecs bun run server.ts
# Output: {"@timestamp":"...","log.level":"info","message":"...","ecs.version":"8.11.0",...}

Trace context maps to trace.id / span.id; errors map to error.type / error.message / error.stack_trace (with the cause chain under error.cause.*).

Datadog

For Datadog, collected from stdout by the Datadog Agent:

LOG_FORMAT=datadog bun run server.ts
# Output: {"status":"info","message":"...","timestamp":"...","dd.trace_id":"...","dd.span_id":"..."}

Maps status (the Datadog severity), message, timestamp, and errors to error.kind / error.message / error.stack (cause chain under error.cause.*).

Trace correlation. Trace and span IDs are written to dd.trace_id / dd.span_id exactly as your traceContext getter returns them — no conversion. Datadog APM log↔trace correlation expects IDs in Datadog's own format: dd-trace-js already produces these, so its IDs correlate out of the box. If instead you wire an OpenTelemetry SDK (128-bit hex IDs), correlation may require aligning the ID format on both the log and trace sides.

Pino

Not a destination — emits the line shape the Pino ecosystem expects, so you can pipe output into pino-pretty, transports, or processors:

LOG_FORMAT=pino bun run server.ts | pino-pretty
# Output: {"level":30,"time":1735689600000,"msg":"...","err":{"type":"...","message":"...","stack":"..."}}

Numeric level (debug=20 … fatal=60), epoch-millisecond time, msg, and errors nested under err (type / message / stack, cause chain under err.cause). Trace context maps to trace_id / span_id / trace_flags (matching @opentelemetry/instrumentation-pino).

Pino's pid / hostname bindings are not added by the library — that keeps it free of a node:os import. Supply them (or any base fields) via the logger context if you want them on every line:

import os from 'node:os'
import { Logger } from 'jsonl-logger'

// pid/hostname become base bindings emitted on every line (with LOG_FORMAT=pino)
const logger = new Logger({ pid: process.pid, hostname: os.hostname() })

Custom Formatter

import type { Formatter } from 'jsonl-logger'

const myFormatter: Formatter = {
  messageKey: 'msg',
  format: (record) => ({
    msg: record.message,
    ts: record.timestamp,
    lvl: record.level,
    ...record.context,
  }),
}

Console Interception

Monkey-patch console.* methods to output structured JSON — captures logs from third-party libraries:

import { intercept, originalConsole } from 'jsonl-logger/intercept'

intercept({
  // Optional: custom formatter (default: GoogleCloudLogging)
  formatter: VictoriaLogs,
  // Optional: filter out noisy messages
  filter: (level, message) => !message.includes('deprecation'),
  // Optional: minimum log level
  level: 'warn',
})

// Already-formatted JSON from the Logger class passes through unchanged
console.log('plain text') // → structured JSON
originalConsole.log('bypass interception')

OpenTelemetry

The logger supports automatic trace context injection. Supply a traceContext getter that returns the active span's trace/span IDs — the formatter maps them to platform-specific fields automatically.

With @opentelemetry/api

import { trace } from '@opentelemetry/api'
import { Logger } from 'jsonl-logger'

const logger = new Logger({}, {
  traceContext: () => {
    const span = trace.getActiveSpan()
    if (!span) return undefined
    const { traceId, spanId, traceFlags } = span.spanContext()
    return { traceId, spanId, traceFlags }
  },
})

logger.info('request handled', { path: '/api' })
// GCL output includes "logging.googleapis.com/trace", "logging.googleapis.com/spanId", etc.
// VictoriaLogs output includes "trace_id", "span_id", etc.

Custom trace context

const logger = new Logger({}, {
  traceContext: () => ({
    traceId: myTracer.currentTraceId(),
    spanId: myTracer.currentSpanId(),
  }),
})

The traceContext option is also available on intercept():

import { intercept } from 'jsonl-logger/intercept'

intercept({
  traceContext: () => {
    const span = trace.getActiveSpan()
    if (!span) return undefined
    const { traceId, spanId, traceFlags } = span.spanContext()
    return { traceId, spanId, traceFlags }
  },
})

Child loggers inherit the traceContext getter from their parent.

Next.js Integration

The preload module reads LOG_FORMAT and only activates when it's set. Safe to include unconditionally — it's a no-op without LOG_FORMAT.

Instrumentation

Next.js auto-detects instrumentation.ts at the project root. Use it to load the preload module on the server:

export async function register() {
  if (process.env.NEXT_RUNTIME === 'nodejs' || typeof Bun !== 'undefined') {
    await import('jsonl-logger/preload')
  }
}

Dockerfile (Standalone with Bun)

Next.js standalone output doesn't include all node_modules. Copy jsonl-logger explicitly from the build stage:

COPY --from=build /app/node_modules/jsonl-logger ./node_modules/jsonl-logger

ENV LOG_FORMAT=victoria-logs

CMD ["bun", "--preload", "jsonl-logger/preload", "server.js"]

Node.js

For non-Bun deployments, use --import to preload:

LOG_FORMAT=google-cloud-logging node --import jsonl-logger/preload server.js

Child Loggers

const requestLogger = logger.child({ requestId: 'abc-123', service: 'api' })
requestLogger.info('Processing request')
// All entries include requestId and service

Error Handling

Errors passed to error() / fatal() capture the full stack trace and error.cause chain:

const inner = new Error('ECONNREFUSED')
const outer = new Error('fetch failed', { cause: inner })
logger.error('API call failed', { endpoint: '/users' }, outer)

Dev mode (no LOG_FORMAT) — colored plain text with full stack:

18:42:05 ✖ API call failed {"endpoint":"/users"}
Error: fetch failed
    at handler (/app/api/route.ts:42:5)
Caused by: Error: ECONNREFUSED
    at connect (/app/db.ts:10:3)

Production (LOG_FORMAT set) — structured JSON with error.* and error.cause.* fields:

{
  "message": "API call failed",
  "severity": "ERROR",
  "endpoint": "/users",
  "error.name": "Error",
  "error.message": "fetch failed",
  "error.stack": "Error: fetch failed\n    at handler ...",
  "error.cause.name": "Error",
  "error.cause.message": "ECONNREFUSED",
  "error.cause.stack": "Error: ECONNREFUSED\n    at connect ..."
}

The errorInfo() helper is exported for use in custom formatters:

import { errorInfo } from 'jsonl-logger'

const info = errorInfo(caughtError)
// { name, message, stack, cause?: { name, message, stack, cause?: ... } }

Environment Variables

Variable Default Description
LOG_FORMAT (unset) Set to enable JSON — selects the formatter (see Formatters). Unset = plain text (colored on a TTY)
LOG_LEVEL info/debug Minimum log level (info when JSON, debug otherwise)
LOG_LABELS icon Plain-text label style: icon, text, or none (also via the labels option)
NO_COLOR (unset) Set to any value to disable plain-text color (no-color.org)
FORCE_COLOR (unset) Force plain-text color on (0/false disables); overrides NO_COLOR

Runtime Detection

The logger auto-detects the runtime and uses the fastest available I/O:

  • Bun / Node.jsprocess.stdout.write / process.stderr.write (bypasses console overhead)
  • DenoDeno.stdout.writeSync / Deno.stderr.writeSync
  • Browser / unknown — falls back to console.log / console.error

Exports

Subpath Export
jsonl-logger Logger, logger, errorInfo(), flattenError(), logLevelValues, stripAnsi(), types (ErrorInfo, LogRecord, TraceContext, etc.)
jsonl-logger/datadog Datadog formatter
jsonl-logger/elastic-common-schema ElasticCommonSchema formatter
jsonl-logger/google-cloud-logging GoogleCloudLogging formatter
jsonl-logger/pino Pino formatter
jsonl-logger/victoria-logs VictoriaLogs formatter
jsonl-logger/intercept intercept(), originalConsole
jsonl-logger/preload Side-effect auto-intercept

Stability

1.0 marks a stable public API under semantic versioning. The stable surface is:

  • The package's public exports — the Logger class, logger singleton, formatter objects, intercept() / originalConsole, and the exported helpers (errorInfo, flattenError, logLevelValues, stripAnsi) and types (see Exports).
  • The Formatter contract ({ messageKey, format(record) }) and the LogRecord / ErrorInfo shapes.
  • The LOG_FORMAT values, LOG_LEVEL, LOG_LABELS, NO_COLOR / FORCE_COLOR, and the Logger / intercept options.

Removing, renaming, or changing the behavior of any of the above bumps the major version. Additive changes — a new LOG_FORMAT value, a new optional option — are non-breaking and ship in minor releases.

License

MIT

About

Lightweight JSON Lines (JSONL) logger with pluggable formatters for VictoriaLogs, Google Cloud Logging, and more

Topics

Resources

License

Stars

2 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors