@vvlad1973/logger-tree
v1.4.1
Published
Hierarchical logger tree system with configuration cascading for TypeScript/JavaScript applications
Maintainers
Readme
@vvlad1973/logger-tree
Иерархическая система логеров с каскадной конфигурацией для TypeScript/JavaScript. Позволяет собирать логирование в единую древовидную структуру, задавать базовые правила на уровне корня и переопределять их точечно для нужных веток.
English version: README.md
Возможности
- Иерархия логеров по dot-путям, чтобы каждый модуль имел свой контекст.
- Наследование конфигурации: дети получают настройки родителя и могут их переопределять.
- Ленивое создание логеров и кеширование, чтобы не тратить ресурсы до первого обращения.
- Гибкие обновления параметров с
force,cascadeиrestrictдля контроля распространения изменений. - Автоматическая привязка логеров к объектам через Binder с последующими обновлениями.
- Полная поддержка TypeScript и готовые типы.
- Внешних зависимостей нет, кроме
@vvlad1973/simple-logger.
Установка
npm install @vvlad1973/logger-treeБыстрый старт
Ниже — минимальный пример. Создаем дерево, поднимаем логеры для разных веток, задаем общий уровень и точечное переопределение для API.
import { LoggerTree, LoggerBinder } from '@vvlad1973/logger-tree';
import SimpleLogger from '@vvlad1973/simple-logger';
// Создаем дерево логеров
const tree = new LoggerTree(() => new SimpleLogger());
// Логеры на разные пути
const appLogger = tree.createLogger('app');
const apiLogger = tree.createLogger('app.api');
const dbLogger = tree.createLogger('app.database');
// Базовый уровень для всего приложения
tree.updateParam('app', 'level', 'debug');
// Переопределение для сервиса
tree.updateParam('app.api', 'level', 'info');
// Использование
appLogger.debug('Application started');
apiLogger.info('API server listening on port 3000');
dbLogger.debug('Database connection established');Ключевые идеи
Иерархические пути
Каждый логер живет на пути вида app.service.worker. Дерево позволяет легко задавать общие правила на уровне родителя и локально менять поведение для конкретной ветки.
app
├── app.api
│ ├── app.api.routes
│ └── app.api.middleware
└── app.database
├── app.database.postgres
└── app.database.redisКаскадирование конфигурации
Настройки на родителе автоматически наследуются детьми:
// Базовая конфигурация
tree.updateParam('app', 'level', 'warn');
// Дети наследуют WARN
const service = tree.createLogger('app.service'); // level: warn
// Переопределение для конкретного узла
tree.updateParam('app.debug', 'level', 'debug');
const debug = tree.createLogger('app.debug'); // level: debugБазовый/root-логер с явной конфигурацией
- Узлы появляются только после
createLogger(path). ВызовupdateParamдля несуществующего пути ничего не меняет. - Предварительную конфигурацию удобно задать через
initialConfigв конструкторе. Она применяется лениво при первом создании узла. Если нужно задать только префикс сообщения, можно передать строку — это шорткат дляmsgPrefix. - В
initialConfigдолжен быть один корневой узел. Для нескольких корней создайте отдельные экземплярыLoggerTree. - Если
initialConfigне используется, сначала создайте узел черезcreateLogger, затем вызывайтеupdateParam.
// Предопределенный root-конфиг (ленивое применение)
const tree = new LoggerTree(() => new SimpleLogger(), undefined, {
app: {
options: { level: 'warn' },
children: {
worker: '[Worker] ', // shorthand для { msgPrefix: '[Worker] ' }
service: {
options: { level: 'info' },
children: {
api: { options: { level: 'debug' } },
worker: { options: { msgPrefix: '[SvcWorker] ' } }
}
}
}
}
});
// Конфиг применится при первом создании узла
const worker = tree.createLogger('app.worker.job'); // msgPrefix: [Worker]
const api = tree.createLogger('app.api'); // level: warn
// Создаем корень
tree.createLogger('app');
// Базовая конфигурация через updateParam
tree.updateParam('app', 'level', 'warn');
tree.updateParam('app', 'transports', {
targets: [{ tag: 'console', level: 'warn' }]
});
// Наследование для детей
const apiAgain = tree.createLogger('app.api'); // inherits warnRoot alias (префикс пути)
Если у вас есть единый корень (например, app), передайте его как alias. Тогда во всех вызовах можно указывать короткие пути, а дерево само добавит префикс. Это упрощает использование и сохраняет консистентность имен.
const tree = new LoggerTree(() => new SimpleLogger(), undefined, undefined, 'app');
// Фактически создает/обновляет app.worker.email
const workerLogger = tree.createLogger('worker.email');
tree.updateParam('worker', 'level', 'error', { force: true }); // применяется к app.worker.*Автосоздание промежуточных узлов
При первом вызове createLogger по новому пути дерево достроит все недостающие сегменты (app, app.agents, app.agents.some_agent) и создаст логеры на каждом уровне. Это гарантирует, что последующие обновления уровня или транспорта смогут нормально каскадировать. Все новые узлы сразу получают агрегированный конфиг родителей (включая initialConfig), поэтому стартуют с согласованными уровнями и префиксами.
const tree = new LoggerTree(() => new SimpleLogger(), undefined, {
app: { options: { level: 'warn' } }
});
// Создаст loggers: app, app.agents, app.agents.some_agent с level: warn
const agentLogger = tree.createLogger('app.agents.some_agent');
// Родитель уже есть, обновления каскадируют сразу
tree.updateParam('app.agents', 'level', 'error', { force: true });
agentLogger.error('will log at error');Расширенные опции каскадирования уровней:
// Force: перезаписать уровни у потомков
tree.updateParam('app', 'level', 'error', { force: true });
// Cascade off: только текущий узел
tree.updateParam('app.worker', 'level', 'fatal', { cascade: false });
// Restrict (one-time clamp): поднять потомков до WARN, если они болтливее
tree.updateParam('app', 'level', 'warn', { restrict: true });
// Попытка задать более болтливый уровень будет ограничена до WARN
tree.updateParam('app.worker', 'level', 'debug');
const workerLogger2 = tree.createLogger('app.worker'); // level: warnИспользование с pino и per-child transports
LoggerTree передает конфигурацию в SimpleLogger, а тот — в Pino (фабрика или .child()) с transport/transports. Это позволяет раздавать разные транспорты по веткам, сохраняя общую структуру дерева.
import pino from 'pino';
import { LoggerTree } from '@vvlad1973/logger-tree';
const tree = new LoggerTree(pino);
// Базовый транспорт
tree.updateParam('app', 'transport', {
target: 'pino-pretty',
options: { colorize: true }
});
// Специальный транспорт для ребенка
tree.updateParam('app.worker', 'transport', {
target: 'pino/file',
options: { destination: './worker.log' }
});
const worker = tree.createLogger('app.worker'); // логирует в worker.logС Pino v8 можно использовать несколько таргетов через transports.targets:
tree.updateParam('app.worker', 'transports', {
targets: [
{ target: 'pino/file', options: { destination: './worker.log' } },
{ target: 'pino-pretty', options: { colorize: false } }
]
});Обновление логеров (совместимость с logrotate)
При ротации файлов (например, через logrotate) пересоздайте логеры, чтобы они заново открыли файлы. Можно обновлять точку или целую ветку; для больших поддеревьев есть батчевое обновление binder.
// Только ветка app.worker и потомки
tree.refreshLoggers('app.worker', { recursive: true });
// Только конкретный узел (без детей)
tree.refreshLoggers('app.audit');
// Батчевое обновление binder для больших поддеревьев
tree.refreshLoggers('app', { recursive: true, batchedBinder: true });refreshLoggers очищает кеш и создает новые экземпляры с текущей конфигурацией. При batchedBinder: true обновление binder выполняется один раз за вызов.
Ленивое создание
Логер создается только при первом обращении и затем возвращается из кеша. Это избавляет от лишних аллокаций, если путь никогда не используется.
const logger1 = tree.createLogger('app.service');
const logger2 = tree.createLogger('app.service');
console.log(logger1 === logger2); // trueAPI
LoggerTree
Класс для управления иерархией логеров.
Usage guide
createLogger(path: string)
Создает или возвращает кешированный логер с наследованной конфигурацией.updateParam(path, key, value, options?)- Обновление параметра узла (
level,transport,transportsи т.д.). force: true— перезаписать значения у потомков.cascade: false— применить только к этому узлу.restrict: trueсlevel— одноразово поднять уровни потомков до уровня родителя.
Типовые сценарии:
// Базовый уровень tree.updateParam('app', 'level', 'info'); // Поднять поддерево tree.updateParam('app.api', 'level', 'warn', { force: true }); // Локальное переопределение tree.updateParam('app.api.worker', 'level', 'fatal', { cascade: false }); // Ограничить чрезмерно болтливых потомков tree.updateParam('app', 'level', 'warn', { restrict: true });- Обновление параметра узла (
updateTargetParam(path, param, value, { tag, force? })
Обновить таргеты транспорта по тегу (в т.ч. включить/выключить) с опциональным каскадом. Работает и сtransport, и сtransports.targets.// Выключить консоль на поддереве tree.updateTargetParam('app', 'enabled', false, { tag: 'console', force: true }); // Поднять уровень для file-транспорта только на узле tree.updateTargetParam('app.worker', 'level', 'error', { tag: 'file' }); // Изменить формат для console на конкретном ребенке tree.updateTargetParam('app.worker', 'format', 'json', { tag: 'console' });refreshLoggers(path, { recursive?, batchedBinder? })
Пересоздать логеры (например, после ротации).recursive: trueзахватывает потомков;batchedBinder: trueбатчит обновление binder.LoggerBinder.bind(path, target)/unbind/refresh/refreshAll/refreshMany
Биндинг объектов (LoggerAware) к путям и их автообновление при изменении конфигурации.refreshMany— батчевое обновление.Цель должна иметь свойство
logger(реализоватьLoggerAware), чтобы binder мог присвоить логер.
Конструктор
constructor(
rootLogger?: ExternalLogger | LoggerFactory | null,
binder?: LoggerBinder,
initialConfig?: Record<string, LoggerOptions | string | { children?: Record<string, unknown> }>
)Методы
createLogger(path: string, inlineConfig?: LoggerOptions, options?: CreateLoggerOptions): SimpleLogger
Создает или возвращает кешированный логер по пути.
const logger = tree.createLogger('app.service.api');
// Переопределить агрегированный конфиг при первом создании
const debugLogger = tree.createLogger('app.debug', { level: 'debug' });
// Глубокое слияние inline-конфига вместо поверхностной замены
const patchedLogger = tree.createLogger('app.service',
{ transports: { targets: [{ tag: 'file', level: 'debug' }] } },
{ patch: true }
);
// Автогенерация msgPrefix из сегментов пути
const autoLogger = tree.createLogger('app.worker.queue'); // msgPrefix: [queue]
// Полное отключение msgPrefix
const noPrefix = tree.createLogger('app.clean', {}, { useMsgPrefix: false });Опции:
patch: boolean- При значении true выполняется глубокое слияние inline-конфига с агрегированным конфигом вместо поверхностной замены. Полезно для слияния вложенных объектов типаtransports.autoMsgPrefix: boolean- Автоматически генерировать msgPrefix из последнего сегмента пути, если не указан явно. По умолчанию true.useMsgPrefix: boolean- Включить/отключить msgPrefix полностью. При false префикс не будет добавлен, даже если он есть в агрегированном конфиге.
Примечания:
- Если путь (или родительские сегменты) не существует, цепочка создается автоматически, а логеры — для каждого нового сегмента с унаследованным конфигом.
- Повторные вызовы возвращают кешированный логер, пока не передан
inlineConfig.
updateParam(path: string, key: string, value: unknown, options?: { force?: boolean; restrict?: boolean; cascade?: boolean }): void
Обновляет параметр конфигурации узла.
// Только этот узел
tree.updateParam('app.service', 'level', 'debug');
// Каскадом на детей без явной конфигурации
tree.updateParam('app', 'level', 'error', { force: true });
// Только этот узел (дети не меняются)
tree.updateParam('app.worker', 'level', 'fatal', { cascade: false });
// Запретить детям быть болтливее WARN
tree.updateParam('app', 'level', 'warn', { restrict: true });updateTargetParam(path: string, param: string, value: unknown, options?: { tag: string; force?: boolean; restrict?: boolean }): void
Обновляет параметры транспортного таргета по тегу. Работает с transport и transports.targets.
tree.updateTargetParam('app', 'enabled', false, {
tag: 'console',
force: true
});
// Поднять уровень только для file-таргета
tree.updateTargetParam('app.worker', 'level', 'error', { tag: 'file' });
// Изменить формат для console
tree.updateTargetParam('app.worker', 'format', 'json', { tag: 'console' });
// Ограничить дочерние логеры от более подробного логирования, чем родительский уровень
tree.updateTargetParam('app', 'level', 'warn', { tag: 'console', restrict: true });Опции:
tag: string- Тег таргета для обновления (требуется при обновлении конкретных транспортных таргетов)force?: boolean- Применить изменение ко всем дочерним узламrestrict?: boolean- При обновлении уровня ограничить подробные дочерние логеры до родительского уровня
Пример с тегами:
// Базовые транспорты с тегами
tree.updateParam('app', 'transports', {
targets: [
{ tag: 'console', target: 'pino-pretty', level: 'info' }
]
});
// Собственный файл для сервиса A
tree.updateParam('app.serviceA', 'transports', {
targets: [
{ tag: 'console', target: 'pino-pretty', level: 'info' },
{ tag: 'file', target: 'pino/file', options: { destination: './serviceA.log' }, level: 'debug' }
]
});
// Другой файл для сервиса B
tree.updateParam('app.serviceB', 'transports', {
targets: [
{ tag: 'console', target: 'pino-pretty', level: 'info' },
{ tag: 'file', target: 'pino/file', options: { destination: './serviceB.log' }, level: 'warn' }
]
});
// Отключить консоль для всех сервисов
tree.updateTargetParam('app', 'enabled', false, { tag: 'console', force: true });
// Поднять уровень файла только для serviceA
tree.updateTargetParam('app.serviceA', 'level', 'error', { tag: 'file' });
// Вернуть консоль только для serviceB
tree.updateTargetParam('app.serviceB', 'enabled', true, { tag: 'console' });LoggerBinder
Управляет автоматической инъекцией и обновлением логеров в объектах. Позволяет навесить логер на объект один раз и быть уверенным, что при изменении конфигурации он обновится без ручных действий.
Цели должны реализовывать
LoggerAware(иметь свойствоlogger), чтобы binder мог присваивать/обновлять логер.
Конструктор
constructor(tree: LoggerTree)Методы
bind(path: string, target: LoggerAware): void
Привязывает объект к пути логера.
import type { LoggerAware } from '@vvlad1973/logger-tree';
class MyService implements LoggerAware {
logger!: SimpleLogger;
}
const service = new MyService();
binder.bind('app.service', service);
// service.logger заполнен автоматическиunbind(path: string, target: LoggerAware): void
Отвязывает объект от обновлений логера.
binder.unbind('app.service', service);refresh(path: string): void
Обновляет логеры для всех объектов, привязанных к пути.
tree.updateParam('app', 'level', 'debug');
binder.refresh('app'); // обновляет все привязанные объектыrefreshAll(): void
Обновляет все привязанные логеры за один проход.
binder.refreshAll();refreshMany(paths: string[]): void
Батчевое обновление без рекурсивного fan-out (если нужен контроль).
binder.refreshMany(['app', 'app.worker']);Advanced Usage
Автоматический биндинг логеров
import { LoggerTree, LoggerBinder, LoggerAware } from '@botapp/logger-tree';
const tree = new LoggerTree();
const binder = new LoggerBinder(tree);
class DatabaseService implements LoggerAware {
logger!: SimpleLogger; // Проставляется binder'ом
async connect() {
this.logger.info('Connecting to database...');
// ...
this.logger.info('Connected successfully');
}
}
const dbService = new DatabaseService();
binder.bind('app.database', dbService);
// Логер обновится при изменении конфигурации
tree.updateParam('app.database', 'level', 'debug');
binder.refresh('app.database');Binder с alias
import { LoggerTree, LoggerBinder, LoggerAware } from '@botapp/logger-tree';
import SimpleLogger from '@vvlad1973/simple-logger';
class Agent implements LoggerAware {
logger!: SimpleLogger;
}
const tree = new LoggerTree(() => new SimpleLogger(), undefined, undefined, 'app');
const binder = new LoggerBinder(tree);
const agent = new Agent();
binder.bind('agents.email', agent); // фактически app.agents.email
tree.updateParam('agents', 'level', 'warn', { force: true });
binder.refresh('agents'); // обновит app.agents.* (включая email)Настройка транспортов
Транспорты можно включать/выключать и менять параметры по тегу. Это удобно, чтобы быстро приглушить консоль в проде или изменить уровень файлового вывода.
tree.updateParam('app', 'transports', {
targets: [
{ tag: 'console', level: 'info', enabled: true },
{ tag: 'file', level: 'debug', enabled: true }
]
});
// Выключить консоль в проде
tree.updateTargetParam('app', 'enabled', false, {
tag: 'console',
force: true
});Интеграция с Pino
LoggerTree может использовать Pino как внешний логер через SimpleLogger. Это дает привычный API Pino, но с иерархической структурой и каскадными настройками.
import { LoggerTree } from '@vvlad1973/logger-tree';
import pino from 'pino';
const pinoLogger = pino({
level: 'info',
transport: {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'HH:MM:ss Z',
ignore: 'pid,hostname'
}
}
});
const tree = new LoggerTree(pinoLogger);
const appLogger = tree.createLogger('app');
const apiLogger = tree.createLogger('app.api');
const dbLogger = tree.createLogger('app.database');
tree.updateParam('app', 'level', 'debug');
tree.updateParam('app.api', 'level', 'info');
appLogger.debug('Application started'); // Pino внутри
apiLogger.info('API server ready'); // Pino внутри
dbLogger.debug('Database connected'); // Pino внутриПреимущества с Pino:
- Производительность Pino в сочетании с иерархией.
- Наследование конфигурации по дереву без дублирования настроек.
- Быстрый JSON-логгинг и богатые транспорты Pino.
- Автобиндинг логеров к объектам.
- Динамические обновления конфигурации без перезапуска.
