[READ-ONLY] Mirror of https://github.com/FoxxMD/logging. A typed, opinionated, batteries-included, Pino-based logging solution for backend TS/JS projects foxxmd.github.io/logging
child-logger logging logging-library nodejs pinojs typescript-library
0

Configure Feed

Select the types of activity you want to include in your feed.

feat: Add prettified json/object stream destination

FoxxMD (Mar 21, 2024, 12:43 PM EDT) e8cd4871 f12f08a4

+161 -11
+15 -3
package-lock.json
··· 1 1 { 2 2 "name": "@foxxmd/logging", 3 - "version": "0.1.9", 3 + "version": "0.1.10", 4 4 "lockfileVersion": 3, 5 5 "requires": true, 6 6 "packages": { 7 7 "": { 8 8 "name": "@foxxmd/logging", 9 - "version": "0.1.9", 9 + "version": "0.1.10", 10 10 "license": "MIT", 11 11 "dependencies": { 12 12 "pino": "^8.19.0", 13 + "pino-abstract-transport": "^1.1.0", 13 14 "pino-pretty": "^11.0.0", 14 - "pino-roll": "^1.0.1" 15 + "pino-roll": "^1.0.1", 16 + "pump": "^3.0.0" 15 17 }, 16 18 "devDependencies": { 17 19 "@types/chai": "^4.3.0", ··· 19 21 "@types/dateformat": "^5.0.2", 20 22 "@types/mocha": "^9.1.0", 21 23 "@types/node": "^18.0.0", 24 + "@types/pump": "^1.1.3", 22 25 "chai": "^4.3.6", 23 26 "chai-as-promised": "^7.1.1", 24 27 "dateformat": "^5.0.3", ··· 605 608 "dev": true, 606 609 "dependencies": { 607 610 "undici-types": "~5.26.4" 611 + } 612 + }, 613 + "node_modules/@types/pump": { 614 + "version": "1.1.3", 615 + "resolved": "https://registry.npmjs.org/@types/pump/-/pump-1.1.3.tgz", 616 + "integrity": "sha512-ZyooTTivmOwPfOwLVaszkF8Zq6mvavgjuHYitZhrIjfQAJDH+kIP3N+MzpG1zDAslsHvVz6Q8ECfivix3qLJaQ==", 617 + "dev": true, 618 + "dependencies": { 619 + "@types/node": "*" 608 620 } 609 621 }, 610 622 "node_modules/abort-controller": {
+5 -2
package.json
··· 1 1 { 2 2 "name": "@foxxmd/logging", 3 3 "type": "module", 4 - "version": "0.1.9", 4 + "version": "0.1.10", 5 5 "repository": "https://github.com/foxxmd/logging", 6 6 "description": "A typed, opinionated, batteries-included, Pino-based logging solution for backend TS/JS projects", 7 7 "scripts": { ··· 49 49 "@types/dateformat": "^5.0.2", 50 50 "@types/mocha": "^9.1.0", 51 51 "@types/node": "^18.0.0", 52 + "@types/pump": "^1.1.3", 52 53 "chai": "^4.3.6", 53 54 "chai-as-promised": "^7.1.1", 54 55 "dateformat": "^5.0.3", ··· 68 69 }, 69 70 "dependencies": { 70 71 "pino": "^8.19.0", 72 + "pino-abstract-transport": "^1.1.0", 71 73 "pino-pretty": "^11.0.0", 72 - "pino-roll": "^1.0.1" 74 + "pino-roll": "^1.0.1", 75 + "pump": "^3.0.0" 73 76 }, 74 77 "overrides": { 75 78 "with-local-tmp-dir": {
+75 -3
src/destinations.ts
··· 3 3 LogLevelStreamEntry, 4 4 LogLevel, 5 5 StreamDestination, 6 - FileDestination, 6 + FileDestination, JsonPrettyDestination, 7 7 } from "./types.js"; 8 - import {destination} from "pino"; 9 - import {build} from "pino-pretty" 8 + import {destination, DestinationStream} from "pino"; 9 + import {build, prettyFactory} from "pino-pretty" 10 10 import {PRETTY_OPTS_FILE, prettyOptsConsoleFactory, prettyOptsFileFactory} from "./pretty.js"; 11 11 import {fileOrDirectoryIsWriteable} from "./util.js"; 12 12 import path from "path"; 13 + import {Transform, TransformCallback} from "node:stream"; 14 + import pump from 'pump'; 15 + import abstractTransport from 'pino-abstract-transport'; 13 16 14 17 15 18 const pRoll = pinoRoll as unknown as typeof pinoRoll.default; ··· 110 113 return { 111 114 level: level, 112 115 stream: build({...prettyOptsConsoleFactory(options)}) 116 + } 117 + } 118 + 119 + /** 120 + * Creates a `LogLevelStreamEntry` stream that writes the raw log data with prettified log line, as either a JSON string or object, to a `NodeJs.WriteableStream` or [Sonic Boom `DestinationStream`](https://github.com/pinojs/sonic-boom) at or above the minimum `level` 121 + * 122 + * Use this to get raw log data + formatted line rendered by pino-pretty. The prettified line is set to the `line` key in the object. 123 + * 124 + * WARNING: This is not a fast operation. The log data must be parsed to json before being prettified, which also parses data to json. If you only need raw log data you should pass a plain Passthrough or Transform stream as an additional destination and then JSON.parse() on 'data' event manually. 125 + * 126 + * If used with `object: true` then 127 + * 128 + * * the `destination` cannot be a file or SonicBoom object 129 + * * the `destination` stream passed MUST be set to `objectMode: true` 130 + * 131 + * @see DestinationStream 132 + * */ 133 + export const buildDestinationJsonPrettyStream = (level: LogLevel, options: JsonPrettyDestination): LogLevelStreamEntry => { 134 + const {object = false} = options; 135 + const factoryOpts = prettyOptsConsoleFactory(options); 136 + const prettyFunc = prettyFactory(factoryOpts); 137 + 138 + const stream = new Transform({ 139 + objectMode: object, 140 + autoDestroy: true, 141 + transform(chunk: any, encoding: BufferEncoding, callback: TransformCallback) { 142 + const data = JSON.parse(chunk.toString()); 143 + const line = prettyFunc(chunk); 144 + data.line = line.substring(0, line.length - 1); 145 + if(object) { 146 + callback(null, data); 147 + } else { 148 + callback(null, `${JSON.stringify(data)}\n`); 149 + } 150 + } 151 + }); 152 + 153 + let destinationStream: DestinationStream | NodeJS.WritableStream; 154 + 155 + if(typeof options.destination === 'object' && typeof options.destination.write === 'function') { 156 + destinationStream = options.destination; 157 + } else { 158 + destinationStream = destination({...factoryOpts, dest: stream}); 159 + } 160 + 161 + if(object) { 162 + // @ts-ignore 163 + pump(stream, destinationStream) 164 + 165 + return { 166 + level: level, 167 + stream: stream 168 + } 169 + } 170 + 171 + // @ts-ignore 172 + const transport = abstractTransport(function (source) { 173 + source.on('unknown', function (line) { 174 + destinationStream.write(line + '\n') 175 + }) 176 + 177 + // @ts-ignore 178 + pump(source, stream, destinationStream); 179 + return stream; 180 + }, { parse: 'lines' }); 181 + 182 + return { 183 + level: level, 184 + stream: transport 113 185 } 114 186 } 115 187
+5 -2
src/factory.ts
··· 9 9 } from "./pretty.js"; 10 10 import { 11 11 buildDestinationStream, 12 + buildDestinationJsonPrettyStream, 12 13 buildDestinationRollingFile, 13 14 buildDestinationStdout, 14 15 buildDestinationStderr, 15 16 buildDestinationFile 16 17 } from "./destinations.js"; 17 18 import {buildLogger} from './loggers.js'; 18 - import {FileDestination, PRETTY_COLORS, PRETTY_COLORS_STR, PRETTY_ISO8601, StreamDestination} from './types.js' 19 + import {FileDestination, PRETTY_COLORS, PRETTY_COLORS_STR, PRETTY_ISO8601, StreamDestination, JsonPrettyDestination} from './types.js' 19 20 20 21 export { 21 22 PRETTY_OPTS_CONSOLE, ··· 29 30 prettyOptsFileFactory, 30 31 prettyOptsConsoleFactory, 31 32 buildDestinationStream, 33 + buildDestinationJsonPrettyStream, 32 34 buildDestinationStdout, 33 35 buildDestinationStderr, 34 36 buildDestinationFile, ··· 38 40 39 41 export type { 40 42 FileDestination, 41 - StreamDestination 43 + StreamDestination, 44 + JsonPrettyDestination 42 45 }
+6
src/types.ts
··· 155 155 156 156 export type FileDestination = Omit<PrettyOptionsExtra, 'destination' | 'sync'> & FileOptionsParsed; 157 157 export type StreamDestination = Omit<PrettyOptionsExtra, 'destination'> & {destination: number | DestinationStream | NodeJS.WritableStream}; 158 + export type JsonPrettyDestination = StreamDestination & { 159 + /** 160 + * Specify if the stream should output log as object or stringified JSON 161 + * */ 162 + object?: boolean 163 + }; 158 164 159 165 export type LogOptionsParsed = Omit<Required<LogOptions>, 'file'> & { file: FileLogOptionsParsed } 160 166
+55 -1
tests/index.test.ts
··· 14 14 import {LogData, LOG_LEVEL_NAMES, PRETTY_ISO8601} from "../src/types.js"; 15 15 import withLocalTmpDir from 'with-local-tmp-dir'; 16 16 import {readdirSync,} from 'node:fs'; 17 - import {buildDestinationStream, buildDestinationRollingFile, buildDestinationFile} from "../src/destinations.js"; 17 + import { 18 + buildDestinationStream, 19 + buildDestinationRollingFile, 20 + buildDestinationFile, 21 + buildDestinationJsonPrettyStream 22 + } from "../src/destinations.js"; 18 23 import {buildLogger} from "../src/loggers.js"; 19 24 import {readFileSync} from "fs"; 20 25 import path from "path"; ··· 32 37 opts.console, 33 38 { 34 39 destination: testStream, 40 + colorize: false, 41 + ...opts 42 + } 43 + ), 44 + { 45 + level: opts.console, 46 + stream: rawStream 47 + } 48 + ]); 49 + return [logger, testStream, rawStream]; 50 + } 51 + 52 + const testObjectLogger = (config?: object, object?: boolean): [Logger, Transform, Transform] => { 53 + const opts = parseLogOptions(config, process.cwd()); 54 + const testStream = new PassThrough({objectMode: true}); 55 + const rawStream = new PassThrough(); 56 + const logger = buildLogger('debug', [ 57 + buildDestinationJsonPrettyStream( 58 + opts.console, 59 + { 60 + destination: testStream, 61 + object, 35 62 colorize: false, 36 63 ...opts 37 64 } ··· 217 244 defaultLogger.debug('Test'); 218 245 const res = await race; 219 246 expect(res).to.be.undefined; 247 + }); 248 + }); 249 + 250 + describe('Pretty Object Stream', function () { 251 + it('Writes pretty line to stream as jsonified object', async function () { 252 + const [defaultLogger, testStream] = testObjectLogger(undefined, false); 253 + const race = Promise.race([ 254 + pEvent(testStream, 'data'), 255 + sleep(10) 256 + ]) as Promise<object>; 257 + defaultLogger.debug('Test'); 258 + const res = (await race).toString(); 259 + expect(res).to.not.be.undefined; 260 + expect(res).match(/"line":".*\sDEBUG\s*:\s*Test"/).is.not.null; 261 + }); 262 + 263 + it('Writes pretty line to stream as object', async function () { 264 + const [defaultLogger, testStream] = testObjectLogger(undefined, true); 265 + const race = Promise.race([ 266 + pEvent(testStream, 'data'), 267 + sleep(10) 268 + ]) as Promise<LogData>; 269 + defaultLogger.debug('Test'); 270 + const res = await race; 271 + expect(res).to.not.be.undefined; 272 + expect(res.line).to.not.be.undefined; 273 + expect(res.line).match(/DEBUG\s*:\s*Test/).is.not.null; 220 274 }); 221 275 }); 222 276