# @foxxmd/logging [![Latest Release](https://img.shields.io/github/v/release/foxxmd/logging)](https://github.com/FoxxMD/logging/releases) [![NPM Version](https://img.shields.io/npm/v/%40foxxmd%2Flogging)](https://www.npmjs.com/package/@foxxmd/logging) [![Try on Runkit](https://img.shields.io/static/v1?label=Try%20it%20online%20on&message=RunKit&color=f55fa6)](https://npm.runkit.com/%40foxxmd%2Flogging) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) A typed, opinionated, batteries-included, [Pino](https://getpino.io)-based logging solution for backend TS/JS projects. Features: * Fully typed for Typescript projects * One-line, [turn-key logging](#quick-start) to console and rotating file * [Child (nested) loggers](#child-loggers) with hierarchical label prefixes for log messages * Per-destination level filtering configurable via ENV or arguments * Clean, opinionated log output format powered by [pino-pretty](https://github.com/pinojs/pino-pretty): * Colorized output when stream is TTY and supports it * Automatically [serialize passed objects and Errors](#serializing-objects-and-errors), including [Error Cause](https://github.com/tc39/proposal-error-cause) * Automatically redact current working directory from log output * [Bring-Your-Own settings](#additional-app-logger-configuration) * Add or use your own streams/[transports](https://getpino.io/#/docs/transports?id=known-transports) for destinations * All pino-pretty configs are exposed and extensible * [Build-Your-Own Logger](#building-a-logger) * Don't want to use any of the pre-built transports? Leverage the convenience of @foxxmd/logging wrappers and default settings but build your logger from scratch example log output **Documentation best viewed on [https://foxxmd.github.io/logging](https://foxxmd.github.io/logging)** # Install ``` npm install @foxxmd/logging ``` # Quick Start ```ts import { loggerAppRolling, loggerApp } from "@foxxmd/logging"; const logger = loggerApp(); logger.info('Test'); /* * Logs to -> console, colorized * Logs to -> CWD/logs/app.log * * [2024-03-07 10:31:34.963 -0500] DEBUG: Test * */ // or for rolling log files we need to scan logs dir before opening a file // and need to await initial logger const rollingLogger = await loggerAppRolling(); rollingLogger.info('Test'); /* * Logs to -> console, colorized * Logs to daily log file, max 10MB size -> CWD/logs/app.1.log * * [2024-03-07 10:31:34.963 -0500] DEBUG: Test * */ ``` # Loggers The package exports 4 top-level loggers. ### App Loggers These are the loggers that should be used for the majority of your application. They accept an optional configuration object for configuring log destinations. * [`loggerApp`](https://foxxmd.github.io/logging/functions/index.loggerApp.html) - Logs to console and a fixed file destination * [`loggerAppRolling`](https://foxxmd.github.io/logging/functions/index.loggerAppRolling.html) - Logs to console and a rolling file destination ### Helper Loggers These loggers are pre-defined for specific use cases: * [`loggerTrace`](https://foxxmd.github.io/logging/variables/index.loggerTrace.html) - Logs to console at the lowest minimum level, `trace`. Can be used during application startup before a logger app configuration has been parsed. * [`loggerDebug`](https://foxxmd.github.io/logging/variables/index.loggerDebug.html) - Logs ONLY to console at minimum `debug` level. Can be used during application startup before a logger app configuration has been parsed. * [`loggerTest`](https://foxxmd.github.io/logging/variables/index.loggerTest.html) - A noop logger (will not log anywhere) for use in tests/mockups. ## Configuring The [App Loggers](#app-loggers) take an optional [`LogOptions`](https://foxxmd.github.io/logging/interfaces/index.LogOptions.html) to configure [`LogLevel`](https://foxxmd.github.io/logging/types/index.LogLevel.html) globally or individually for **Console** and **File** outputs. [`file` in `LogOptions`](https://foxxmd.github.io/logging/interfaces/index.FileLogOptions.html) may also be an object that specifies more behavior for log file output. ```ts const infoLogger = loggerApp({ level: 'info' // console and file will log any levels `info` and above }); const logger = loggerApp({ console: 'debug', // console will log `debug` and higher file: 'warn' // file will log `warn` and higher }); const fileLogger = loggerRollingApp({ // no level specified => console defaults to `info` level file: { level: 'warn', // file will log `warn` and higher path: '/my/cool/path/output.log', // output to log file at this path frequency: 'daily', // rotate hourly size: '20MB', // rotate if file size grows larger than 20MB timestamp: 'unix' // use unix epoch timestamp instead of iso8601 in rolling file } }); ``` An optional second parameter, [`LoggerAppExtras`](https://foxxmd.github.io/logging/interfaces/index.LoggerAppExtras.html), may be passed that allows adding additional log destinations or pino-pretty customization to the [App Loggers](#app-loggers). Some defaults and convenience variables for pino-pretty options are also available in [`@foxxmd/logging/factory`]((https://foxxmd.github.io/logging/modules/factory.html)) prefixed with `PRETTY_`. An example using `LoggerAppExtras`: ```ts import { loggerApp } from '@foxxmd/logging'; import { PRETTY_ISO8601, buildDestinationFile } from "@foxxmd/logging/factory"; // additional file logging but only at `warn` or higher const warnFileDestination = buildDestinationFile('warn', {path: './myLogs/warn.log'}); const logger = loggerApp({ level: 'debug', // console AND built-in file logging will log `debug` and higher }, { destinations: [warnFileDestination], pretty: { translateTime: PRETTY_ISO8601 // replaces standard timestamp with ISO8601 format } }); logger.debug('Test'); // [2024-03-07T11:27:41-05:00] DEBUG: Test ``` See [Building A Logger](#building-a-logger) for more information. #### Colorizing Docker Logs Color output to STD out/err is normally automatically detected by [colorette](https://github.com/jorgebucaran/colorette) or can manually be set using `colorize` anywhere [PrettyOptions](https://foxxmd.github.io/logging/interfaces/factory._internal_.PrettyOptions_.html) are accepted. However docker output can be hard to detect as supporting colorizing, or the output may not be TTY at the container interface but is viewed by a terminal or web app that does support colorizing. Therefore `@foxxmd/logging` will look for a `COLORED_STD` environmental variable and, if no other `colorize` option is set _and the ENV is not empty_, will use the truthy value of this variable to set `colorize` **for any `buildDestinationStdout` or `buildDestinationStderr` transports.** This includes the built-in stdout transports for `loggerApp` and `loggerAppRolling`. Thus you could set `COLORED_STD=true` in your Dockerfile to coerce colored output to docker logs. If a user does not want colored output for any reason they can simply override the environmental variable like `COLORED_STD=false` ## Usage ### Child Loggers [Pino Child loggers](https://getpino.io/#/docs/child-loggers) can be created using the [`childLogger`](https://foxxmd.github.io/logging/functions/index.childLogger.html) function with the added ability to inherit **Labels** from their parent loggers. **Labels** are inserted between the log level and message contents of a log. The child logger inherits **all** labels from **all** its parent loggers. `childLogger` accepts a single string/function label or an array of string/function labels. ```ts import {loggerApp, childLogger} from '@foxxmd/logging'; logger = loggerApp(); logger.debug('Test'); // [2024-03-07 11:27:41.944 -0500] DEBUG: Test const nestedChild1 = childLogger(logger, 'First'); nestedChild1.debug('I am nested one level'); // [2024-03-07 11:27:41.945 -0500] DEBUG: [First] I am nested one level const nestedChild2 = childLogger(nestedChild1, ['Second', () => 'Third']); nestedChild2.warn('I am nested two levels but with more labels'); // [2024-03-07 11:27:41.945 -0500] WARN: [First] [Second] [Third] I am nested two levels but with more labels const siblingLogger = childLogger(logger, ['1Sib','2Sib']); siblingLogger.info('Test'); // [2024-03-07 11:27:41.945 -0500] INFO: [1Sib] [2Sib] Test ``` Labels can also be added at "runtime" by passing an object with `labels` prop to the logger level function. These labels will be appended to any existing labels on the logger. ```ts logger.debug({labels: ['MyLabel']}, 'My log message'); ``` **NOTE:** If a label *function* throws an error then the label will be the error's message. Make sure your labels don't throw! #### Filtering By Label Logs can be filtered by labels similar to how [debug-js](https://github.com/debug-js/debug) works. By default *no* filtering is done. Your child loggers follow the Pino Child Logger behavior of inherting parent log level. Using either `LOG_FILTER_ENABLE` or `LOG_FILTER_DISABLE` you can enable or disable a child logger and it's children. ```ts /* * process.env.LOG_FILTER_ENABLE = 'Third:Fourth,Second:*:Foo,Bar' */ import {loggerApp, childLogger} from '@foxxmd/logging'; logger = loggerApp(); logger.debug('Test'); // [2024-03-07 11:27:41.944 -0500] DEBUG: Test const nestedChild1 = childLogger(logger, 'First'); nestedChild1.debug('I am nested one level'); // [2024-03-07 11:27:41.945 -0500] DEBUG: [First] I am nested one level const nestedChild2 = childLogger(nestedChild1, ['Second', 'Third'], { level: 'silent' }); nestedChild2.info('I do not log because of set level and not enabled by filter'); // without LOG_FILTER_ENABLE from above set, the below logging would be silent // because they inherit the level from their parent const nestedChild3 = childLogger(nestedChild2, ['Fourth']); nestedChild3.info('I do log because of filter Third:Fourth'); const nestedChild4 = childLogger(nestedChild3, ['Foo']); nestedChild4.info('I do log because of filter Second:*:Foo'); const nestedChild5 = childLogger(nestedChild4, ['Bar']); nestedChild5.info('I do log because of filter Bar'); ``` The minimum level a child logger is "enabled" to is ENV `LOG_FILTER_ENABLE_LEVEL` or `trace`, by default. It can also be specified by passing `labelEnableLevel` to childLogger options. The minimum level a child logger is "disabled" to is ENV `LOG_FILTER_DISABLE_LEVEL` or `silent`, by default. It can also be specified by passing `labelDisableLevel` to childLogger options. There are also options for passing your own function to determine if a child logger should be enabled or disabled. See [`childLogger`](https://foxxmd.github.io/logging/functions/index.childLogger.html) docs for full a full reference. A caveat to be aware of: **enable/disable by filter is evaluated once, when `childLogger` is instantiated.** This means: * Only the labels added by `childLogger` insantiation are applicable. Labels added during logging, IE `logger.info({labels: ['Runtime']}, "a log")`, are not considered. * Labels that are functions are evaluated **once**, when `childLogger` is instantiated * Changing the `LOG_FILTER_*` envs after a childLogger is created will have no effect on it ### Serializing Objects and Errors Passing an object or array as the first argument to the logger will cause the object to be JSONified and pretty printed below the log message ```ts logger.debug({myProp: 'a string', nested: {anotherProps: ['val1', 'val2'], boolProp: true}}, 'Test'); /* [2024-03-07 11:39:37.687 -0500] DEBUG: Test myProp: "a string" nested: { "anotherProps": [ "val1", "val2" ], "boolProp": true } */ ``` Passing an `Error` as the first argument will pretty print the error stack including any [causes.](https://github.com/tc39/proposal-error-cause) ```ts const er = new Error('This is the original error'); const causeErr = new ErrorWithCause('A top-level error', {cause: er}); logger.debug(causeErr, 'Test'); /* [2024-03-07 11:43:27.453 -0500] DEBUG: Test Error: A top-level error at (/my/dir/src/index.ts:55:18) caused by: Error: This is the original error at (/my/dir/src/index.ts:54:12) */ ``` Passing an `Error` without a second argument (message) will cause the top-level error's message to be printed instead of log message. # Building A Logger All the functionality required to build your own logger is exported by [`@foxxmd/logging/factory`](https://foxxmd.github.io/logging/modules/factory.html). You can customize almost every facet of logging. A logger is composed of a minimum default level and array of objects that implement [`StreamEntry`](https://foxxmd.github.io/logging/interfaces/index._internal_.StreamEntry-1.html), the same interface used by [`pino.multistream`](https://getpino.io/#/docs/api?id=pino-multistream). The only constraint is that your streams must accept the same levels as `@foxxmd/logging` using the [`LogLevelStreamEntry`](https://foxxmd.github.io/logging/types/index.LogLevelStreamEntry.html) interface that extends `StreamEntry`. ```ts import {LogLevelStreamEntry} from '@foxxmd/logging'; import { buildLogger } from "@foxxmd/logging/factory"; const myStreams: LogLevelStreamEntry[] = []; // build streams const logger = buildLogger('debug', myStreams); logger.debug('Test'); ``` `factory` exports several "destination" `LogLevelStreamEntry` function creators with default configurations that can be overridden. ```ts import { buildLogger, buildDestinationStream, // generic NodeJS.WriteableStream or SonicBoom DestinationStream buildDestinationStdout, // stream to STDOUT buildDestinationStderr, // stream to STDERR buildDestinationFile, // write to static file buildDestinationRollingFile // write to rolling file } from "@foxxmd/logging/factory"; ``` All `buildDestination` functions take args: * `level` (first arg) - minimum level to log at * `options` (second arg) - an object extending [`pino-pretty` options](https://github.com/pinojs/pino-pretty?tab=readme-ov-file#options), [`PrettyOptions`](https://foxxmd.github.io/logging/interfaces/factory._internal_.PrettyOptions_.html) `options` inherits a default `pino-pretty` configuration that comprises `@foxxmd/logging`'s opinionated logging format. The common default config can be generated using [`prettyOptsFactory`](https://foxxmd.github.io/logging/functions/factory.prettyOptsFactory.html) which accepts an optional `PrettyOptions` object to override defaults: ```ts import { prettyOptsFactory } from "@foxxmd/logging/factory"; const defaultConfig = prettyOptsFactory(); // override with your own config const myCustomizedConfig = prettyOptsFactory({ colorize: false }); ``` Pre-configured `PrettyOptions` are also provided for different destinations: ```ts import { PRETTY_OPTS_CONSOLE, // default config PRETTY_OPTS_FILE // disables colorize } from "@foxxmd/logging/factory"; ``` Specific buildDestinations also require passing a stream or path: [`buildDestinationStream`](https://foxxmd.github.io/logging/functions/factory.buildDestinationStream.html) must pass a `NodeJS.WriteableStream` or SonicBoom `DestinationStream` to options as [`destination`](https://foxxmd.github.io/logging/types/factory.StreamDestination.html) ```ts import {buildDestinationStream} from "@foxxmd/logging/factory"; const myStream = new WritableStream(); const dest = buildDestinationStream('debug', {destination: myStream}); ``` [`buildDestinationStdout`](https://foxxmd.github.io/logging/functions/factory.buildDestinationStdout.html) and [`buildDestinationStderr`](https://foxxmd.github.io/logging/functions/factory.buildDestinationStderr.html) do not require a destination as they are fixed to STDOUT/STDERR [`buildDestinationFile`](https://foxxmd.github.io/logging/functions/factory.buildDestinationFile.html) and [`buildDestinationRollingFile`](https://foxxmd.github.io/logging/functions/factory.buildDestinationRollingFile.html) must pass a [`path`](https://foxxmd.github.io/logging/types/factory.FileDestination.html) to options ```ts import {buildDestinationFile} from "@foxxmd/logging/factory"; const dest = buildDestinationFile('debug', {path: '/path/to/file.log'}); ``` ### Example Putting everything above together ```ts import { buildDestinationStream, buildDestinationFile, prettyOptsFactory, buildDestinationStdout, buildLogger } from "@foxxmd/logging/factory"; import { PassThrough } from "node:stream"; const hookStream = new PassThrough(); const hookDestination = buildDestinationStream('debug', { ...prettyOptsFactory({sync: true, ignore: 'pid'}), destination: hookStream }); const debugFileDestination = buildDestinationFile('debug', {path: './myLogs/debug.log'}); const warnFileDestination = buildDestinationFile('warn', {path: './myLogs/warn.log'}); const logger = buildLogger('debug', [ hookDestination, buildDestinationStdout('debug'), debugFileDestination, warnFileDestination ]); hookStream.on('data', (log) => {console.log(log)}); logger.debug('Test') // logs to hookStream // logs to STDOUT // logs to file ./myLogs/debug.log // does NOT log to file ./myLogs/warn.log ``` ### Parsing LogOptions If you wish to use [`LogOptions`](#configuring) to get default log levels for your destinations use [`parseLogOptions`](https://foxxmd.github.io/logging/functions/index.parseLogOptions.html): ```ts import {parseLogOptions, LogOptions} from '@foxxmd/logging'; const parsedOptions: LogOptions = parseLogOptions(myConfig); ``` # Examples Various use-cases for `@foxxmd/logging` and how to configure a logger for them. Remember, `loggerApp` and `loggerAppRolling` **accept the same arguments.** The examples below use `loggerApp` but `loggerAppRolling` can be used as a drop-in replacement in order to use a rolling log file. #### Log to Console and File ```ts import {loggerApp, loggerAppRolling} from '@foxxmd/logging'; // static log file at ./logs/app.log const staticLogger = loggerApp(); // rolling log file at ./logs/app.1.log const rollingLogger = loggerAppRolling(); ``` #### Log At Specific Level Or Higher for Console and File ```ts import {loggerApp} from '@foxxmd/logging'; // INFO is the default level // when 'console' is not specified it logs to 'info' or higher // when 'file' is not specified it logs to 'info' or higher const infoLogger = loggerApp(); // logs to console and log at 'debug' level and higher const debugLogger = loggerApp({level: 'debug'}); ``` #### Log At `debug` for Console and `warn` for File ```ts import {loggerApp} from '@foxxmd/logging'; const logger = loggerApp({ console: 'debug', file: 'warn' }); ``` #### Do not log to File ```ts import {loggerApp} from '@foxxmd/logging'; // also logs to console at 'info' level const logger = loggerApp({ file: false }); ``` #### Log to Specific File ```ts import {loggerApp} from '@foxxmd/logging'; // also logs to console at 'info' level const logger = loggerApp({ file: { path: './path/to/file.log' } }); ``` #### Log to Rolling File with Unix Timestamp ```ts import {loggerApp} from '@foxxmd/logging'; // also logs to console at 'info' level const logger = loggerApp({ file: { timestamp: 'unix' } }); ``` #### Log to Rolling File with no timestamp ```ts import {loggerApp} from '@foxxmd/logging'; // also logs to console at 'info' level const logger = loggerApp({ file: { // specify size but NOT 'frequency' to disable timestamps in filename size: '10M' } }); ``` #### Log to additional File for 'error' only ```ts import {loggerApp} from '@foxxmd/logging'; import { buildDestinationFile } from "@foxxmd/logging/factory"; const errorFileDestination = buildDestinationFile('error', {path: './myLogs/warn.log'}); // also logs to console and file at 'info' level const logger = loggerApp({}, { destinations: [errorFileDestination] }); ``` #### Log raw, newline-delimited json logs to additional File ```ts import {loggerApp} from '@foxxmd/logging'; import { buildDestinationFile } from "@foxxmd/logging/factory"; import fs from 'node:fs'; const rawFile = fs.createWriteStream('myRawFile.log'); // also logs to console and file at 'info' level const logger = loggerApp({}, { destinations: [ { level: 'debug', stream: rawFile // logs are NOT prettified, only raw data from pino } ] }); ``` #### Log prettified data to additional stream This could be used to trigger something when a log object with a specific property is found. Or to stream prettified log json to a client over websockets. To emit data as an object ([`LogDataPretty`](https://foxxmd.github.io/logging/types/index.LogDataPretty.html)) set `objectMode` and `object` to true. ```ts import {loggerApp} from '@foxxmd/logging'; import { buildDestinationJsonPrettyStream } from "@foxxmd/logging/factory"; import { PassThrough } from "node:stream"; const prettyObjectStream = new Passthrough({objectMode: true}); // objectMode MUST be true to get objects from the stream const prettyObjectDestination = buildDestinationJsonPrettyStream('debug', { destination: prettyObjectStream, object: true, // must be set to true to use with objectMode stream colorize: true }); const prettyStringStream = new Passthrough(); // will emit data as a json string const prettyStringDestination = buildDestinationJsonPrettyStream('debug', { destination: prettyStringStream, object: false, colorize: true }); // also logs to console and file at 'info' level const logger = loggerApp({}, { destinations: [ prettyObjectDestination, prettyStringDestination ] }); prettyObjectStream.on('data', (log) => { // do something with log object (LogDataPretty) }); prettyStringStream.on('data', (log) => { // do something with log string }); ``` #### Log to additional Pino Transports Log to a [Pino Transport](https://getpino.io/#/docs/transports) like [pino-elasticsearch](https://getpino.io/#/docs/transports?id=pino-elasticsearch): ```ts import {loggerApp} from '@foxxmd/logging'; import pinoElastic from 'pino-elasticsearch' const streamToElastic = pinoElastic({ index: 'an-index', node: 'http://localhost:9200', esVersion: 7, flushBytes: 1000 }); // also logs to console and file at 'info' level const logger = loggerApp({}, { destinations: [ { level: 'debug', stream: streamToElastic } ] }); ```