/

jsonl-logger GitHub

Легкий JSONL логер з форматерами для VictoriaLogs, Google Cloud Logging та інших.

2.3K/mo 2 typescriptloggingjsonlbunnodedenoopen-sourcenpmgoogle-cloudvictorialogs
bun add jsonl-logger

Структуровані логи — правильна відповідь для продакшену і неправильна для мого ноутбука. Коли логи читає конвеєр, я хочу один JSON-об’єкт на рядок; коли читаю я — кольоровий рядок, зрозумілий людині. Зазвичай тримати обидва варіанти означає або два налаштування логування, або обгортку, яку ніхто не хоче підтримувати.

jsonl-logger робить це однією змінною середовища, без жодних змін у коді.

Приклади

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'))

Локально це виглядає як текст — кольоровий у TTY і звичайний, щойно ви перенаправите вивід у файл чи CI:

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)

Виставте LOG_FORMAT — і ті самі виклики дадуть JSONL у формі, яку очікує ваша платформа логів:

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

Навіщо це

Кожен логер, який я брав, змушував вибирати. Гарний вивід для людини — це окрема залежність, яку треба не забути не потягнути в продакшен. Продакшен-логери сиплять JSON усюди, і локальна розробка перетворюється на стіну з дужок. Проблема не у функціях: змінюється призначення виводу між середовищами, а місця викликів змінюватися не повинні.

Тож форма виводу стала питанням запуску, а не коду. LOG_LEVEL навіть має різні типові значення: debug для тексту й info для JSON — бо саме цього й хочеться в кожному з випадків.

Друга причина суто побутова: фреймворки з жорстко зашитим текстовим логуванням незручні там, де приймають лише JSON. Пропустити їх через форматер простіше, ніж патчити фреймворк.

Чим відрізняється

ПакетПро що він
jsonl-loggerодні місця викликів, текст локально і JSONL у продакшені, без залежностей, лише ESM
pinoпропускна здатність і велика екосистема транспортів та процесорів
winstonмаксимальна налаштовуваність — транспорти, власні рівні, формати
consolaвивід для CLI та гарні репортери для розробника

Беріть pino, якщо потрібна його екосистема транспортів або цілеспрямована ставка на пропускну здатність — а цей проєкт навіть уміє видавати форму рядка pino, щоб можна було передати вивід у pino-pretty та подібні. Беріть winston, якщо вам справді потрібні довільні транспорти. Цей — зовсім маленький, але для багатьох типових проєктів його цілком досить: зручний локальний вивід, чистий JSON у продакшені й жодного стека логування, який треба збирати.

Що всередині

Готові форматери під типові призначення

LOG_FORMATЩо видає
google-cloud-loggingseverity, timestamp, payload у формі GCP
victoria-logs_msg, _time, level
ecs@timestamp, log.level, ecs.version для Elastic/Filebeat
datadogstatus, dd.trace_id, dd.span_id
pinoчисловий level, epoch-time, msg, err

Trace context лягає туди, куди очікує кожне призначення — trace.id/span.id для ECS, trace_id/span_id/trace_flags для pino.

Мітки та колір, коли це текст

import { Logger } from 'jsonl-logger'

const logger = new Logger({}, { labels: 'text', colors: false })

LOG_LABELS перемикає між іконками (◆ ● ▲ ✖ ‼), текстовими мітками або нічим. Колір спершу дивиться на FORCE_COLOR і NO_COLOR, а вже потім на TTY, тож у CI поводиться правильно без окремих вказівок.

Власні форматери, бо готовий список — це старт, а не межа

Форматер — це невеликий об’єкт з назвами ключів і способом рендеру запису, тож внутрішня схема логів — це кілька рядків, а не форк:

import type { Formatter } from 'jsonl-logger'

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

Нотатки про рішення

Перемикач — це змінна середовища, а не API. Якби це був параметр конструктора, кожен сервіс переписував би ту саму перевірку process.env, і рано чи пізно хтось помилився б у Dockerfile. Коли рішення живе в бібліотеці, воно там само, де й середовище.

Жодного імпорту node:os. Прив’язок pid і hostname, як у pino, свідомо немає: імпорт node:os прив’язав би пакет до Node-подібних середовищ. Ви можете додати їх самі як базовий контекст — натомість логер лишається переносним між Bun, Node і Deno.

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

const logger = new Logger({ pid: process.pid, hostname: os.hostname() })

Trace ID для Datadog передаються без змін. Кореляція в Datadog APM очікує ідентифікатори у власному форматі, і dd-trace-js уже їх такими й видає. Тихо конвертувати 128-бітні hex-ідентифікатори OpenTelemetry виглядало б турботливо, але давало б неправильну кореляцію — тож бібліотека пише рівно те, що повернув ваш traceContext, і документує розбіжність.

Статус

Живий і в роботі. Лише ESM і Bun-first за задумом — працює на Node і Deno, але не намагається підтримувати збірки, старші за ESM.

Деталі

Автор
Dmytro Klymenko
Опубліковано
Annexare на GitHub, NPM
Початок
Оновлено

Інші проєкти