[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: Implement label filtering

FoxxMD (Feb 27, 2026, 9:44 PM UTC) 4e1b7cbd 2e4de38f

+363 -10
+37
README.md
··· 178 178 179 179 **NOTE:** If a label *function* throws an error then the label will be the error's message. Make sure your labels don't throw! 180 180 181 + #### Filtering By Label 182 + 183 + Logs can be filtered by labels similar to how [debug-js](https://github.com/debug-js/debug) works. 184 + 185 + By default *no* filtering is done. Your child loggers follow the Pino Child Logger behavior of inherting parent log level. 186 + 187 + Using either `LOG_FILTER_ENABLE` or `LOG_FILTER_DISABLE` you can enable or disable a child logger and it's children. 188 + 189 + ```ts 190 + 191 + /* 192 + * process.env.LOG_FILTER_ENABLE = 'Third:Fourth,Second:*:Foo,Bar' 193 + */ 194 + 195 + import {loggerApp, childLogger} from '@foxxmd/logging'; 196 + 197 + logger = loggerApp(); 198 + logger.debug('Test'); 199 + // [2024-03-07 11:27:41.944 -0500] DEBUG: Test 200 + 201 + const nestedChild1 = childLogger(logger, 'First'); 202 + nestedChild1.debug('I am nested one level'); 203 + // [2024-03-07 11:27:41.945 -0500] DEBUG: [First] I am nested one level 204 + 205 + const nestedChild2 = childLogger(nestedChild1, ['Second', 'Third'], { level: 'silent' }); 206 + nestedChild2.info('I do not log because of set level and not enabled by filter'); 207 + 208 + const nestedChild3 = childLogger(nestedChild2, ['Fourth']); 209 + nestedChild3.info('I do log because of filter Third:Fourth'); 210 + 211 + const nestedChild4 = childLogger(nestedChild3, ['Foo']); 212 + nestedChild4.info('I do log because of filter Second:*:Foo'); 213 + 214 + const nestedChild5 = childLogger(nestedChild4, ['Bar']); 215 + nestedChild5.info('I do log because of filter Bar'); 216 + ``` 217 + 181 218 ### Serializing Objects and Errors 182 219 183 220 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
+53
src/funcs.ts
··· 1 1 import process from "process"; 2 2 import { 3 + ChildLabel, 3 4 FileLogOptions, 4 5 FileLogOptionsParsed, 5 6 FileLogPathOptions, 7 + FilterLabelsFunc, 6 8 LOG_LEVEL_NAMES, 7 9 LogLevel, 8 10 LogOptions, ··· 111 113 112 114 return resolve(baseDir, pathVal); 113 115 } 116 + 117 + export const labelsStrBuilder = (labels: ChildLabel[]) => { 118 + return labels.map(x => typeof x === 'function' ? 'dynamicfunc' : x.toLocaleLowerCase().trim()).reverse().join(':'); 119 + } 120 + export const labelFilterToRegex = (filter: string) => { 121 + const reverseCleaned = filter.split(':').map(x => x.trim().toLocaleLowerCase().replace('*','.+')).reverse().join(':'); 122 + return new RegExp(`^${reverseCleaned}`); 123 + } 124 + 125 + export const labelFiltersFromStr = (str?: string): RegExp[] | undefined => { 126 + if(str === undefined || str.trim() === '') { 127 + return undefined; 128 + } 129 + return str.split(',').map(x => labelFilterToRegex(x.trim())); 130 + } 131 + 132 + export const labelFiltersFromEnvSingleton = (envName: string): FilterLabelsFunc => { 133 + let envFilterRes: false | RegExp[]; 134 + return (labels: ChildLabel[]) => { 135 + if(envFilterRes === undefined) { 136 + envFilterRes = labelFiltersFromStr(process.env[envName]); 137 + if(envFilterRes === undefined) { 138 + envFilterRes = false; 139 + } 140 + } 141 + if(envFilterRes === false) { 142 + return false; 143 + } 144 + 145 + const labelStr = labelsStrBuilder(labels); 146 + return envFilterRes.some(x => x.test(labelStr)); 147 + } 148 + } 149 + 150 + 151 + export const labelsEnableFromEnvSingleton = labelFiltersFromEnvSingleton('LOG_FILTER_ENABLE'); 152 + export const labelsDisableFromEnvSingleton = labelFiltersFromEnvSingleton('LOG_FILTER_DISABLE'); 153 + 154 + export const labelsFilterFromEnv = (envName: string): FilterLabelsFunc => { 155 + return (labels: ChildLabel[]) => { 156 + const envFilterRes = labelFiltersFromStr(process.env[envName]); 157 + if(envFilterRes === undefined) { 158 + return false; 159 + } 160 + const labelStr = labelsStrBuilder(labels); 161 + return envFilterRes.some(x => x.test(labelStr)); 162 + } 163 + } 164 + 165 + export const labelsEnableFromEnv = labelsFilterFromEnv('LOG_FILTER_ENABLE'); 166 + export const labelsDisableFromEnv = labelsFilterFromEnv('LOG_FILTER_DISABLE');
+21 -3
src/index.ts
··· 8 8 LogLevel, 9 9 LOG_LEVEL_NAMES, 10 10 LoggerAppExtras, 11 - PrettyOptionsExtra 11 + PrettyOptionsExtra, 12 + ChildLoggerOptions 12 13 } from './types.js' 13 14 14 15 import { 15 16 isLogOptions, 16 17 parseLogOptions, 18 + labelsStrBuilder, 19 + labelFiltersFromStr, 20 + labelFiltersFromEnvSingleton, 21 + labelsEnableFromEnvSingleton, 22 + labelsDisableFromEnvSingleton, 23 + labelsFilterFromEnv, 24 + labelsEnableFromEnv, 25 + labelsDisableFromEnv 17 26 } from './funcs.js' 18 27 19 28 import {childLogger, loggerApp, loggerDebug, loggerTest, loggerTrace, loggerAppRolling} from './loggers.js'; ··· 27 36 LogDataPretty, 28 37 LogLevel, 29 38 LoggerAppExtras, 30 - PrettyOptionsExtra 39 + PrettyOptionsExtra, 40 + ChildLoggerOptions 31 41 } 32 42 33 43 export { ··· 39 49 LOG_LEVEL_NAMES, 40 50 childLogger, 41 51 parseLogOptions, 42 - isLogOptions 52 + isLogOptions, 53 + labelsStrBuilder, 54 + labelFiltersFromStr, 55 + labelFiltersFromEnvSingleton, 56 + labelsEnableFromEnvSingleton, 57 + labelsDisableFromEnvSingleton, 58 + labelsFilterFromEnv, 59 + labelsEnableFromEnv, 60 + labelsDisableFromEnv 43 61 }
+21 -6
src/loggers.ts
··· 1 - import {parseLogOptions} from "./funcs.js"; 1 + import {labelsDisableFromEnv, labelsDisableFromEnvSingleton, labelsEnableFromEnv, labelsEnableFromEnvSingleton, parseLogOptions} from "./funcs.js"; 2 2 import { 3 3 PinoLoggerOptions, 4 4 CUSTOM_LEVELS, ··· 6 6 LoggerAppExtras, 7 7 LogLevel, 8 8 LogLevelStreamEntry, 9 - LogOptions 9 + LogOptions, 10 + ChildLoggerOptions 10 11 } from "./types.js"; 11 12 import {buildDestinationFile, buildDestinationRollingFile, buildDestinationStdout} from "./destinations.js"; 12 13 import {pino, levels, stdSerializers} from "pino"; ··· 45 46 }, 46 47 mixinMergeStrategy(mergeObject: Record<any, any>, mixinObject: Record<any, any>) { 47 48 if(mergeObject.labels === undefined || mixinObject.labels === undefined || mixinObject.labels.length === 0) { 48 - return Object.assign(mergeObject, mixinObject) 49 + return Object.assign(mergeObject, mixinObject); 49 50 } 50 51 const runtimeLabels = Array.isArray(mergeObject.labels) ? mergeObject.labels : [mergeObject.labels]; 51 52 const finalObj = Object.assign(mergeObject, mixinObject); ··· 91 92 * @param parent Logger Parent logger to inherit from 92 93 * @param labelsVal (any | any[]) Labels to always apply to logs from this logger 93 94 * @param context object Additional properties to always apply to logs from this logger 94 - * @param options object 95 + * @param options ChildLoggerOptions 95 96 * */ 96 - export const childLogger = (parent: Logger, labelsVal: any | any[] = [], context: object = {}, options = {}): Logger => { 97 - const newChild = parent.child(context, options) as Logger; 97 + export const childLogger = (parent: Logger, labelsVal: any | any[] = [], context: object = {}, options: ChildLoggerOptions = {}): Logger => { 98 + const { 99 + labelEnable = labelsEnableFromEnvSingleton, 100 + labelEnableLevel = parent.level, 101 + labelDisable = labelsDisableFromEnvSingleton, 102 + labelDisableLevel = 'silent', 103 + ...rest 104 + } = options; 105 + 106 + const newChild = parent.child(context, rest) as Logger; 98 107 const labels = Array.isArray(labelsVal) ? labelsVal : [labelsVal]; 99 108 newChild.labels = [...[...(parent.labels ?? [])], ...labels]; 100 109 newChild.addLabel = function (value) { ··· 102 111 this.labels = []; 103 112 } 104 113 this.labels.push(value); 114 + } 115 + 116 + if(newChild.level !== labelEnableLevel && labelEnable !== undefined && labelEnable(newChild.labels)) { 117 + newChild.level = labelEnableLevel; 118 + } else if (newChild.level !== labelDisableLevel && labelDisable !== undefined && labelDisable(newChild.labels)) { 119 + newChild.level = labelDisableLevel; 105 120 } 106 121 return newChild 107 122 }
+12
src/types.ts
··· 51 51 52 52 export const LOG_LEVEL_NAMES= ['silent', 'fatal', 'error', 'warn', 'info', 'log', 'verbose', 'debug', 'trace'] as const; 53 53 54 + export type ChildLabel = (string | CallableFunction); 55 + 56 + export type FilterLabelsFunc = (labels: ChildLabel[]) => boolean; 57 + 58 + export interface ChildLoggerOptions { 59 + labelEnable?: (labels: ChildLabel[]) => boolean, 60 + labelEnableLevel?: LogLevel, 61 + labelDisable?: (labels: ChildLabel[]) => boolean, 62 + labelDisableLevel?: LogLevel 63 + [key: string]: any 64 + } 65 + 54 66 /** 55 67 * Configure log levels and file options for an AppLogger. 56 68 *
+219 -1
tests/index.test.ts
··· 24 24 import {readFileSync} from "fs"; 25 25 import path from "path"; 26 26 import dateFormatDef from "dateformat"; 27 + import { labelsDisableFromEnv, labelsEnableFromEnv, labelFilterToRegex, labelsStrBuilder } from "../src/funcs.js"; 27 28 28 29 const dateFormat = dateFormatDef as unknown as typeof dateFormatDef.default; 29 30 ··· 32 33 const opts = parseLogOptions(config, {logBaseDir: process.cwd()}); 33 34 const testStream = new PassThrough(); 34 35 const rawStream = new PassThrough(); 35 - const logger = buildLogger('trace', [ 36 + const logger = buildLogger('debug', [ 36 37 buildDestinationStream( 37 38 opts.console, 38 39 { ··· 702 703 expect(formatted).includes(' [Runtime] '); 703 704 expect(formatted).includes(' [Runtime2] '); 704 705 }); 706 + 707 + describe('Filtering', function () { 708 + 709 + before(function () { 710 + delete process.env.LOG_FILTER_ENABLE; 711 + delete process.env.LOG_FILTER_DISABLE; 712 + }); 713 + 714 + it('Does not output if silent', async function () { 715 + const [logger, testStream, rawStream] = testConsoleLogger(); 716 + const child = childLogger(logger, 'Test', {}, {level: 'silent'}); 717 + const race = Promise.race([ 718 + pEvent(testStream, 'data'), 719 + sleep(10) 720 + ]) as Promise<Buffer>; 721 + child.debug('Test'); 722 + const res = await race; 723 + expect(res).to.be.undefined; 724 + }); 725 + 726 + it('Does not output if parent is silent', async function () { 727 + const [logger, testStream, rawStream] = testConsoleLogger(); 728 + const child = childLogger(logger, 'Test', {}, {level: 'silent'}); 729 + const child2 = childLogger(child, 'Test1', {}); 730 + const race = Promise.race([ 731 + pEvent(testStream, 'data'), 732 + sleep(10) 733 + ]) as Promise<Buffer>; 734 + child2.debug('Test'); 735 + const res = await race; 736 + expect(res).to.be.undefined; 737 + }); 738 + 739 + it('Does output if enabled by env filter', async function () { 740 + process.env.LOG_FILTER_ENABLE = 'Test:*'; 741 + 742 + const [logger, testStream, rawStream] = testConsoleLogger(); 743 + const child = childLogger(logger, ['Test','Bar'], {}, { 744 + level: 'silent', 745 + labelEnable: labelsEnableFromEnv 746 + }); 747 + const race = Promise.race([ 748 + pEvent(testStream, 'data'), 749 + sleep(10) 750 + ]) as Promise<Buffer>; 751 + child.debug('Test'); 752 + const res = await race; 753 + expect(res).to.not.be.undefined; 754 + }); 755 + 756 + it('Does output if enabled by env filter and parent was silent', async function () { 757 + process.env.LOG_FILTER_ENABLE = 'Bar'; 758 + 759 + const [logger, testStream, rawStream] = testConsoleLogger(); 760 + const parent = childLogger(logger, ['Foo'], {}, {level: 'silent'}) 761 + const child = childLogger(parent, ['Test','Bar'], {}, { 762 + labelEnable: labelsEnableFromEnv, 763 + labelEnableLevel: 'debug' 764 + }); 765 + const race = Promise.race([ 766 + pEvent(testStream, 'data'), 767 + sleep(10) 768 + ]) as Promise<Buffer>; 769 + child.debug('Test'); 770 + const res = await race; 771 + expect(res).to.not.be.undefined; 772 + }); 773 + 774 + it('Does not output if disabled by env filter', async function () { 775 + process.env.LOG_FILTER_ENABLE = 'Bar:Foo'; 776 + 777 + const [logger, testStream, rawStream] = testConsoleLogger(); 778 + const child = childLogger(logger, ['Test','Bar','Foo'], {}, { 779 + labelDisable: labelsDisableFromEnv 780 + }); 781 + const race = Promise.race([ 782 + pEvent(testStream, 'data'), 783 + sleep(10) 784 + ]) as Promise<Buffer>; 785 + child.debug('Test'); 786 + const res = await race; 787 + expect(res).to.not.be.undefined; 788 + }); 789 + }); 705 790 }); 706 791 }); 707 792 ··· 781 866 expect(formatted).includes(dt); 782 867 }); 783 868 }); 869 + 870 + describe('Label Filtering', function() { 871 + 872 + describe('Filter Building and Parsing Funcs', function() { 873 + 874 + it('takes a user filter, cleans it, and reverses it', function() { 875 + 876 + const userFilter = 'My Foo: My Bar'; 877 + 878 + const expected = new RegExp(/^my bar:my foo/); 879 + 880 + const parsed = labelFilterToRegex(userFilter); 881 + expect(parsed.toString()).eq(expected.toString()); 882 + }); 883 + 884 + it('takes a user filter with wildcard and converts to .+', function() { 885 + 886 + const userFilter = 'My Foo:*:My Bar'; 887 + 888 + const expected = new RegExp(/^my bar:.+:my foo/); 889 + 890 + const parsed = labelFilterToRegex(userFilter); 891 + expect(parsed.toString()).eq(expected.toString()); 892 + }); 893 + 894 + it('takes labels and converts to string for filter', function() { 895 + 896 + const labels = ['My Foo','My Bar', 'My GAZ']; 897 + 898 + const expected = 'my gaz:my bar:my foo'; 899 + 900 + const parsed = labelsStrBuilder(labels); 901 + expect(parsed).eq(expected); 902 + }); 903 + 904 + }); 905 + 906 + describe('Filter regex testing', function() { 907 + 908 + it('Passes when single label matches single filter', function() { 909 + 910 + const labels = ['My Label']; 911 + const filter = 'My Label'; 912 + 913 + 914 + const reg = labelFilterToRegex(filter); 915 + const labelStr = labelsStrBuilder(labels); 916 + 917 + expect(reg.test(labelStr)).is.true; 918 + 919 + }); 920 + 921 + it('Passes based on last user filter matching right-most labels', function() { 922 + 923 + const labels = ['My Label', 'My Second']; 924 + const filter = 'My Second'; 925 + 926 + 927 + const reg = labelFilterToRegex(filter); 928 + const labelStr = labelsStrBuilder(labels); 929 + 930 + expect(reg.test(labelStr)).is.true; 931 + 932 + }); 933 + 934 + it('Does not pass if last user filter does not match right-most label', function() { 935 + 936 + const labels = ['My Label', 'My Second']; 937 + const filter = 'My Label'; 938 + 939 + 940 + const reg = labelFilterToRegex(filter); 941 + const labelStr = labelsStrBuilder(labels); 942 + 943 + expect(reg.test(labelStr)).is.false; 944 + 945 + }); 946 + 947 + it('Passes when explicit, multiple user filters match right-most labels', function() { 948 + 949 + const labels = ['Foo','My Label', 'My Second']; 950 + const filter = 'My Label:My Second'; 951 + 952 + 953 + const reg = labelFilterToRegex(filter); 954 + const labelStr = labelsStrBuilder(labels); 955 + 956 + expect(reg.test(labelStr)).is.true; 957 + 958 + }); 959 + 960 + it('Passes when wildcard satisfies interim labels', function() { 961 + 962 + const labels = ['Foo','My Label','Other','My Second']; 963 + const filter = 'Foo:*:My Second'; 964 + 965 + 966 + const reg = labelFilterToRegex(filter); 967 + const labelStr = labelsStrBuilder(labels); 968 + 969 + expect(reg.test(labelStr)).is.true; 970 + 971 + }); 972 + 973 + it('Does not pass if wildcard satisfies interim labels but not all explicit right-most labels', function() { 974 + 975 + const labels = ['Foo','My Label','Other','My Second']; 976 + const filter = 'Foo:*:My Second:Final'; 977 + 978 + 979 + const reg = labelFilterToRegex(filter); 980 + const labelStr = labelsStrBuilder(labels); 981 + 982 + expect(reg.test(labelStr)).is.false; 983 + 984 + }); 985 + 986 + it('Passes if wildcard is for all subsequent labels', function() { 987 + 988 + const labels = ['Foo','My Label','Other','My Second']; 989 + const filter = 'Foo:My Label:*'; 990 + 991 + 992 + const reg = labelFilterToRegex(filter); 993 + const labelStr = labelsStrBuilder(labels); 994 + 995 + expect(reg.test(labelStr)).is.true; 996 + 997 + }); 998 + 999 + }); 1000 + 1001 + });