[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 level padding

FoxxMD (Mar 11, 2024, 10:57 AM EDT) 84c591d8 d6799355

+91 -27
+3 -3
src/funcs.ts
··· 1 1 import process from "process"; 2 - import {FileLogOptions, FileLogOptionsParsed, LOG_LEVELS, LogLevel, LogOptions, LogOptionsParsed} from "./types.js"; 2 + import {FileLogOptions, FileLogOptionsParsed, LOG_LEVEL_NAMES, LogLevel, LogOptions, LogOptionsParsed} from "./types.js"; 3 3 import {logPath, projectDir} from "./constants.js"; 4 4 import {isAbsolute, resolve} from 'node:path'; 5 5 ··· 18 18 if (t !== 'string') { 19 19 return false; 20 20 } 21 - return LOG_LEVELS.includes(val.toLocaleLowerCase()); 21 + return LOG_LEVEL_NAMES.includes(val.toLocaleLowerCase()); 22 22 }); 23 23 } 24 24 const isFileLogOptions = (obj: any): obj is FileLogOptions => { 25 25 if (obj === null || typeof obj !== 'object') { 26 26 return false; 27 27 } 28 - const levelOk = obj.level === undefined || ('level' in obj && obj.level === false || LOG_LEVELS.includes(obj.level.toLocaleLowerCase())); 28 + const levelOk = obj.level === undefined || ('level' in obj && obj.level === false || LOG_LEVEL_NAMES.includes(obj.level.toLocaleLowerCase())); 29 29 const pathOk = obj.path === undefined || ('path' in obj && typeof obj.path === 'string' || typeof obj.path === 'function'); 30 30 const frequencyOk = obj.frequency === undefined || ('frequency' in obj && typeof obj.frequency === 'string' || typeof obj.frequency === 'number'); 31 31 const sizeOk = obj.size === undefined || ('size' in obj && typeof obj.size === 'string' || typeof obj.size === 'number');
+2 -2
src/index.ts
··· 5 5 FileLogOptions, 6 6 LogData, 7 7 LogLevel, 8 - LOG_LEVELS, 8 + LOG_LEVEL_NAMES, 9 9 LoggerAppExtras, 10 10 PrettyOptionsExtra 11 11 } from './types.js' ··· 33 33 loggerAppRolling, 34 34 loggerDebug, 35 35 loggerTest, 36 - LOG_LEVELS, 36 + LOG_LEVEL_NAMES, 37 37 childLogger, 38 38 parseLogOptions, 39 39 isLogOptions
+31 -2
src/pretty.ts
··· 1 1 import {PrettyOptions} from "pino-pretty"; 2 - import {CWD} from "./util.js"; 3 - import {PRETTY_COLORS, PRETTY_COLORS_STR, PRETTY_LEVELS, PRETTY_LEVELS_STR, PrettyOptionsExtra} from "./types.js"; 2 + import {CWD, getLongestStr} from "./util.js"; 3 + import { 4 + LOG_LEVEL_NAMES, 5 + PRETTY_COLORS, 6 + PRETTY_COLORS_STR, 7 + PRETTY_LEVELS, 8 + PRETTY_LEVELS_STR, 9 + PrettyOptionsExtra 10 + } from "./types.js"; 4 11 5 12 /** 6 13 * Builds the opinionated `@foxxmd/logging` defaults for pino-pretty `PrettyOptions` and merges them with an optional user-provided `PrettyOptions` object ··· 10 17 //customLevels = {}, 11 18 customColors = {}, 12 19 redactCwd = true, 20 + padLevels = true, 21 + customPrettifiers = {}, 13 22 ...rest 14 23 } = opts; 15 24 ··· 20 29 redactFunc = (content) => content; 21 30 } 22 31 32 + const longestLevelNameLength = getLongestStr([...LOG_LEVEL_NAMES]); 33 + const padLevelFunc = padLevel(longestLevelNameLength); 34 + 35 + if(padLevels && customPrettifiers.level === undefined) { 36 + customPrettifiers.level = (logLevel, key, log, { label, labelColorized }) => padLevelFunc(label, labelColorized) 37 + } 38 + 23 39 return { 24 40 messageFormat: (log, messageKey, levelLabel, {colors}) => { 25 41 const labels: string[] = log.labels as string[] ?? []; ··· 36 52 colorizeObjects: true, 37 53 // @ts-ignore 38 54 useOnlyCustomProps: false, 55 + customPrettifiers, 39 56 ...rest 40 57 } 41 58 } ··· 70 87 }, []).join(','); 71 88 } 72 89 return colors; 90 + } 91 + 92 + export const padLevel = (length: number) => (label: string, labelColorized?: string) => { 93 + // pad to fix alignment 94 + // assuming longest level is VERBOSE 95 + // and may be colorized 96 + const paddedLabel = label.padEnd(length) 97 + if(labelColorized !== undefined && labelColorized !== label) { 98 + const padDiff = paddedLabel.length - label.length; 99 + return labelColorized.padEnd(labelColorized.length + padDiff); 100 + } 101 + return paddedLabel; 73 102 } 74 103 75 104 const PRETTY_OPTS_CONSOLE_DEFAULTS: PrettyOptions = {sync: true};
+38 -13
src/types.ts
··· 1 - import {DestinationStream, Logger as PinoLogger, LoggerOptions, StreamEntry} from 'pino'; 1 + import {DestinationStream, Logger as PinoLogger, LoggerOptions, StreamEntry, Level} from 'pino'; 2 2 import {ErrorWithCause} from "pony-cause"; 3 3 import {PrettyOptions} from "pino-pretty"; 4 4 import {MarkRequired} from "ts-essentials"; 5 5 6 - export type LogLevel = typeof LOG_LEVELS[number]; 7 - export const LOG_LEVELS= ['silent', 'fatal', 'error', 'warn', 'info', 'log', 'verbose', 'debug'] as const; 6 + export type LogLevel = typeof LOG_LEVEL_NAMES[number]; 7 + 8 + /** 9 + * Pino default levels 10 + * 11 + * @see pino/lib/constants.js 12 + * */ 13 + const PINO_DEFAULT_LEVELS = { 14 + trace: 10, 15 + debug: 20, 16 + info: 30, 17 + warn: 40, 18 + error: 50, 19 + fatal: 60 20 + } 21 + 22 + const PINO_DEFAULT_LEVEL_NAMES = Object.keys(PINO_DEFAULT_LEVELS) as (Level)[]; 23 + 24 + /** 25 + * Additional levels included in @foxxmd/logging as an object 26 + * 27 + * These are always applied when using `prettyOptsFactory` but can be overridden 28 + * */ 29 + export const CUSTOM_LEVELS: LoggerOptions<"verbose" | "log">['customLevels'] = { 30 + verbose: 25, 31 + log: 21 32 + } 33 + 34 + const CUSTOM_LEVEL_NAMES = Object.keys(CUSTOM_LEVELS); 35 + 36 + export const LOG_LEVEL_NAMES= ['silent', 'fatal', 'error', 'warn', 'info', 'log', 'verbose', 'debug'] as const; 8 37 9 38 export interface LogOptions { 10 39 /** ··· 145 174 } 146 175 147 176 /** 148 - * Additional levels included in @foxxmd/logging as an object 149 - * 150 - * These are always applied when using `prettyOptsFactory` but can be overridden 151 - * */ 152 - export const CUSTOM_LEVELS: LoggerOptions<"verbose" | "log">['customLevels'] = { 153 - verbose: 25, 154 - log: 21 155 - } 156 - 157 - /** 158 177 * Additional [Pino Log options](https://getpino.io/#/docs/api?id=options) that are passed to `pino()` on logger creation 159 178 * */ 160 179 export type PinoLoggerOptions = Omit<LoggerOptions, 'level' | 'mixin' | 'mixinMergeStrategy' | 'customLevels' | 'useOnlyCustomLevels' | 'transport'> ··· 173 192 * @default true 174 193 * */ 175 194 redactCwd?: boolean 195 + /** 196 + * Pads levels in log string so all are the same length 197 + * 198 + * @default true 199 + * */ 200 + padLevels?: boolean 176 201 } 177 202 178 203 /**
+10
src/util.ts
··· 51 51 } 52 52 53 53 export const CWD = process.cwd(); 54 + 55 + export const getLongestStr = (levels: string[]): number => { 56 + let longest = 0; 57 + for (const l of levels) { 58 + if (l.length > longest) { 59 + longest = l.length; 60 + } 61 + } 62 + return longest; 63 + }
+7 -7
tests/index.test.ts
··· 4 4 import chai, {expect} from "chai"; 5 5 import {pEvent} from 'p-event'; 6 6 import {sleep} from "../src/util.js"; 7 - import {LogData, LOG_LEVELS} from "../src/types.js"; 7 + import {LogData, LOG_LEVEL_NAMES} from "../src/types.js"; 8 8 import withLocalTmpDir from 'with-local-tmp-dir'; 9 9 import {readdirSync,} from 'node:fs'; 10 10 import {buildDestinationStream, buildDestinationRollingFile, buildDestinationFile} from "../src/destinations.js"; ··· 131 131 }) 132 132 133 133 it('does not throw when a valid level is given', function () { 134 - for (const level of LOG_LEVELS) { 134 + for (const level of LOG_LEVEL_NAMES) { 135 135 expect(() => parseLogOptions({level})).to.not.throw; 136 136 } 137 137 }) ··· 189 189 defaultLogger.debug('Test'); 190 190 const res = await race; 191 191 expect(res).to.not.be.undefined; 192 - expect(res.toString()).to.include('DEBUG: Test'); 192 + expect(res.toString().match(/DEBUG\s*:\s*Test/)).is.not.null; 193 193 }); 194 194 195 195 it('Does NOT write to console with DEBUG when higher level is specified', async function () { ··· 259 259 await sleep(20); 260 260 const res = await race; 261 261 expect(res).to.not.be.undefined; 262 - expect(res.toString()).to.include('DEBUG: Test'); 262 + expect(res.toString().match(/DEBUG\s*:\s*Test/)).is.not.null; 263 263 const paths = readdirSync('./logs'); 264 264 expect(paths.length).eq(1); 265 265 const fileContents = readFileSync(path.resolve('./logs', paths[0])).toString(); 266 - expect(fileContents).includes('DEBUG: Test'); 266 + expect(fileContents.match(/DEBUG\s*:\s*Test/)).is.not.null; 267 267 }, {unsafeCleanup: true}); 268 268 }); 269 269 ··· 279 279 await sleep(20); 280 280 const res = await race; 281 281 expect(res).to.not.be.undefined; 282 - expect(res.toString()).to.include('DEBUG: Test'); 282 + expect(res.toString().match(/DEBUG\s*:\s*Test/)).is.not.null 283 283 const paths = readdirSync('./logs'); 284 284 expect(paths.length).eq(1); 285 285 const fileContents = readFileSync(path.resolve('./logs', paths[0])).toString(); 286 - expect(fileContents).includes('DEBUG: Test'); 286 + expect(fileContents.match(/DEBUG\s*:\s*Test/)).is.not.null; 287 287 }, {unsafeCleanup: true}); 288 288 }); 289 289 });