Skip to content

Commit 4e1b7cb

Browse files
committed
feat: Implement label filtering
1 parent 2e4de38 commit 4e1b7cb

File tree

6 files changed

+363
-10
lines changed

6 files changed

+363
-10
lines changed

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,43 @@ logger.debug({labels: ['MyLabel']}, 'My log message');
178178

179179
**NOTE:** If a label *function* throws an error then the label will be the error's message. Make sure your labels don't throw!
180180

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+
181218
### Serializing Objects and Errors
182219

183220
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

src/funcs.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import process from "process";
22
import {
3+
ChildLabel,
34
FileLogOptions,
45
FileLogOptionsParsed,
56
FileLogPathOptions,
7+
FilterLabelsFunc,
68
LOG_LEVEL_NAMES,
79
LogLevel,
810
LogOptions,
@@ -111,3 +113,54 @@ export const getLogPath = (path?: string, options: FileLogPathOptions = {}) => {
111113

112114
return resolve(baseDir, pathVal);
113115
}
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');

src/index.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,21 @@ import {
88
LogLevel,
99
LOG_LEVEL_NAMES,
1010
LoggerAppExtras,
11-
PrettyOptionsExtra
11+
PrettyOptionsExtra,
12+
ChildLoggerOptions
1213
} from './types.js'
1314

1415
import {
1516
isLogOptions,
1617
parseLogOptions,
18+
labelsStrBuilder,
19+
labelFiltersFromStr,
20+
labelFiltersFromEnvSingleton,
21+
labelsEnableFromEnvSingleton,
22+
labelsDisableFromEnvSingleton,
23+
labelsFilterFromEnv,
24+
labelsEnableFromEnv,
25+
labelsDisableFromEnv
1726
} from './funcs.js'
1827

1928
import {childLogger, loggerApp, loggerDebug, loggerTest, loggerTrace, loggerAppRolling} from './loggers.js';
@@ -27,7 +36,8 @@ export type {
2736
LogDataPretty,
2837
LogLevel,
2938
LoggerAppExtras,
30-
PrettyOptionsExtra
39+
PrettyOptionsExtra,
40+
ChildLoggerOptions
3141
}
3242

3343
export {
@@ -39,5 +49,13 @@ export {
3949
LOG_LEVEL_NAMES,
4050
childLogger,
4151
parseLogOptions,
42-
isLogOptions
52+
isLogOptions,
53+
labelsStrBuilder,
54+
labelFiltersFromStr,
55+
labelFiltersFromEnvSingleton,
56+
labelsEnableFromEnvSingleton,
57+
labelsDisableFromEnvSingleton,
58+
labelsFilterFromEnv,
59+
labelsEnableFromEnv,
60+
labelsDisableFromEnv
4361
}

src/loggers.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import {parseLogOptions} from "./funcs.js";
1+
import {labelsDisableFromEnv, labelsDisableFromEnvSingleton, labelsEnableFromEnv, labelsEnableFromEnvSingleton, parseLogOptions} from "./funcs.js";
22
import {
33
PinoLoggerOptions,
44
CUSTOM_LEVELS,
55
Logger,
66
LoggerAppExtras,
77
LogLevel,
88
LogLevelStreamEntry,
9-
LogOptions
9+
LogOptions,
10+
ChildLoggerOptions
1011
} from "./types.js";
1112
import {buildDestinationFile, buildDestinationRollingFile, buildDestinationStdout} from "./destinations.js";
1213
import {pino, levels, stdSerializers} from "pino";
@@ -45,7 +46,7 @@ export const buildLogger = (defaultLevel: LogLevel, streams: LogLevelStreamEntry
4546
},
4647
mixinMergeStrategy(mergeObject: Record<any, any>, mixinObject: Record<any, any>) {
4748
if(mergeObject.labels === undefined || mixinObject.labels === undefined || mixinObject.labels.length === 0) {
48-
return Object.assign(mergeObject, mixinObject)
49+
return Object.assign(mergeObject, mixinObject);
4950
}
5051
const runtimeLabels = Array.isArray(mergeObject.labels) ? mergeObject.labels : [mergeObject.labels];
5152
const finalObj = Object.assign(mergeObject, mixinObject);
@@ -91,10 +92,18 @@ export const buildLogger = (defaultLevel: LogLevel, streams: LogLevelStreamEntry
9192
* @param parent Logger Parent logger to inherit from
9293
* @param labelsVal (any | any[]) Labels to always apply to logs from this logger
9394
* @param context object Additional properties to always apply to logs from this logger
94-
* @param options object
95+
* @param options ChildLoggerOptions
9596
* */
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;
98107
const labels = Array.isArray(labelsVal) ? labelsVal : [labelsVal];
99108
newChild.labels = [...[...(parent.labels ?? [])], ...labels];
100109
newChild.addLabel = function (value) {
@@ -103,6 +112,12 @@ export const childLogger = (parent: Logger, labelsVal: any | any[] = [], context
103112
}
104113
this.labels.push(value);
105114
}
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;
120+
}
106121
return newChild
107122
}
108123
/**

src/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,18 @@ const CUSTOM_LEVEL_NAMES = Object.keys(CUSTOM_LEVELS);
5151

5252
export const LOG_LEVEL_NAMES= ['silent', 'fatal', 'error', 'warn', 'info', 'log', 'verbose', 'debug', 'trace'] as const;
5353

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+
5466
/**
5567
* Configure log levels and file options for an AppLogger.
5668
*

0 commit comments

Comments
 (0)