refactor: avoid recompiling the regex on every test
Signed-off-by: Thomas Cardonne <thomas.cardonne@adevinta.com>
This commit is contained in:
@@ -31,7 +31,7 @@ import {
|
||||
import { MESSAGE } from 'triple-beam';
|
||||
import { escapeRegExp } from '../../lib/escapeRegExp';
|
||||
import { winstonLevels, WinstonLoggerLevelOverride } from './types';
|
||||
import { isLogMatching } from './utils';
|
||||
import { createLogMatcher } from './utils';
|
||||
|
||||
/**
|
||||
* @public
|
||||
@@ -188,12 +188,15 @@ export class WinstonLogger implements RootLoggerService {
|
||||
format: Format;
|
||||
setOverrides: (overrides: WinstonLoggerLevelOverride[]) => void;
|
||||
} {
|
||||
const overrides: WinstonLoggerLevelOverride[] = [];
|
||||
const overrides: {
|
||||
predicate: (log: TransformableInfo) => boolean;
|
||||
level: string;
|
||||
}[] = [];
|
||||
|
||||
return {
|
||||
format: format(log => {
|
||||
for (const override of overrides) {
|
||||
if (isLogMatching(log, override.matchers)) {
|
||||
if (override.predicate(log)) {
|
||||
// Discard the log if the log level is below the override
|
||||
// eg, if the override level is 'warn' (1) and the log is 'debug' (5)
|
||||
if (winstonLevels[log.level] > winstonLevels[override.level]) {
|
||||
@@ -213,8 +216,12 @@ export class WinstonLogger implements RootLoggerService {
|
||||
return log;
|
||||
})(),
|
||||
setOverrides: newOverrides => {
|
||||
// Replace the content while preserving the reference
|
||||
overrides.splice(0, overrides.length, ...newOverrides);
|
||||
const newOverridesPredicates = newOverrides.map(o => ({
|
||||
predicate: createLogMatcher(o.matchers),
|
||||
level: o.level,
|
||||
}));
|
||||
// Replace the content while preserving the reference to support live config updates
|
||||
overrides.splice(0, overrides.length, ...newOverridesPredicates);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { isLogMatching } from './utils';
|
||||
import { createLogMatcher } from './utils';
|
||||
|
||||
describe('isLogMatching', () => {
|
||||
describe('createLogMatcher', () => {
|
||||
const log = {
|
||||
level: 'info',
|
||||
message: 'This is a simple log from the catalog plugin',
|
||||
@@ -25,68 +25,56 @@ describe('isLogMatching', () => {
|
||||
};
|
||||
|
||||
it('should match with a simple matcher', () => {
|
||||
expect(
|
||||
isLogMatching(log, {
|
||||
plugin: 'catalog',
|
||||
}),
|
||||
).toEqual(true);
|
||||
const matcher = createLogMatcher({ plugin: 'catalog' });
|
||||
expect(matcher(log)).toEqual(true);
|
||||
});
|
||||
|
||||
it('should not match with a simple matcher', () => {
|
||||
expect(
|
||||
isLogMatching(log, {
|
||||
plugin: 'search',
|
||||
}),
|
||||
).toEqual(false);
|
||||
const matcher = createLogMatcher({ plugin: 'search' });
|
||||
expect(matcher(log)).toEqual(false);
|
||||
});
|
||||
|
||||
it('should match with an AND matcher', () => {
|
||||
expect(
|
||||
isLogMatching(log, {
|
||||
plugin: 'catalog',
|
||||
action: 'read',
|
||||
}),
|
||||
).toEqual(true);
|
||||
const matcher = createLogMatcher({
|
||||
plugin: 'catalog',
|
||||
action: 'read',
|
||||
});
|
||||
expect(matcher(log)).toEqual(true);
|
||||
});
|
||||
|
||||
it('should not match log with an AND matcher', () => {
|
||||
expect(
|
||||
isLogMatching(log, {
|
||||
plugin: 'catalog',
|
||||
action: 'write',
|
||||
}),
|
||||
).toEqual(false);
|
||||
const matcher = createLogMatcher({
|
||||
plugin: 'catalog',
|
||||
action: 'write',
|
||||
});
|
||||
expect(matcher(log)).toEqual(false);
|
||||
});
|
||||
|
||||
it('should match with an OR matcher', () => {
|
||||
expect(
|
||||
isLogMatching(log, {
|
||||
plugin: ['auth', 'catalog'],
|
||||
}),
|
||||
).toEqual(true);
|
||||
const matcher = createLogMatcher({
|
||||
plugin: ['auth', 'catalog'],
|
||||
});
|
||||
expect(matcher(log)).toEqual(true);
|
||||
});
|
||||
|
||||
it('should not match log with an OR matcher', () => {
|
||||
expect(
|
||||
isLogMatching(log, {
|
||||
plugin: ['auth', 'search'],
|
||||
}),
|
||||
).toEqual(false);
|
||||
const matcher = createLogMatcher({
|
||||
plugin: ['auth', 'search'],
|
||||
});
|
||||
expect(matcher(log)).toEqual(false);
|
||||
});
|
||||
|
||||
it('should match log with a regex matcher', () => {
|
||||
expect(
|
||||
isLogMatching(log, {
|
||||
message: '/This is a simple log/',
|
||||
}),
|
||||
).toEqual(true);
|
||||
const matcher = createLogMatcher({
|
||||
message: '/This is a simple log/',
|
||||
});
|
||||
expect(matcher(log)).toEqual(true);
|
||||
});
|
||||
|
||||
it('should not match log with a regex matcher', () => {
|
||||
expect(
|
||||
isLogMatching(log, {
|
||||
message: '/^simple log/',
|
||||
}),
|
||||
).toEqual(false);
|
||||
const matcher = createLogMatcher({
|
||||
message: '/^simple log/',
|
||||
});
|
||||
expect(matcher(log)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,62 +17,78 @@
|
||||
import { TransformableInfo } from 'logform';
|
||||
import { WinstonLoggerLevelOverrideMatchers } from './types';
|
||||
|
||||
/** Parse a slash-delimited regex like `/pattern/flags` into a RegExp, or null if not a regex-string */
|
||||
const parseRegex = (s: string): RegExp | null => {
|
||||
if (!s.startsWith('/')) return null;
|
||||
const lastSlash = s.lastIndexOf('/');
|
||||
if (lastSlash <= 0) return null;
|
||||
|
||||
const pattern = s.slice(1, lastSlash);
|
||||
const flags = s.slice(lastSlash + 1);
|
||||
|
||||
try {
|
||||
return new RegExp(pattern, flags);
|
||||
} catch {
|
||||
return null; // fall back to treating it as a plain string
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if a given log field matches a specified matcher.
|
||||
* Create a predicate function that determines whether a log field matches a given matcher.
|
||||
*
|
||||
* The matcher can be:
|
||||
* - A string (exact match or regex pattern delimited by slashes, e.g. `/pattern/`)
|
||||
* - A non-string value (compared by strict equality)
|
||||
* - An array of matchers (returns true if any matcher matches)
|
||||
*
|
||||
* @param logField - The log field value to test for a match.
|
||||
* @param matcher - The matcher or array of matchers to compare against the log field.
|
||||
* @returns `true` if the log field matches the matcher, otherwise `false`.
|
||||
* @returns A function that takes a log field and returns `true` if it matches the matcher, otherwise `false`.
|
||||
*/
|
||||
const isLogFieldMatching = (
|
||||
logField: unknown,
|
||||
const createLogFieldMatcher = (
|
||||
matcher: WinstonLoggerLevelOverrideMatchers[0],
|
||||
): boolean => {
|
||||
): ((logField: unknown) => boolean) => {
|
||||
// Array of matchers: create predicates for each element and OR them together
|
||||
if (Array.isArray(matcher)) {
|
||||
return matcher.some(m => isLogFieldMatching(logField, m));
|
||||
const fns = matcher.map(m => createLogFieldMatcher(m));
|
||||
return (logField: unknown) => fns.some(fn => fn(logField));
|
||||
}
|
||||
|
||||
// Non-string matcher: strict equality
|
||||
if (typeof matcher !== 'string') {
|
||||
return logField === matcher;
|
||||
return (logField: unknown) => logField === matcher;
|
||||
}
|
||||
|
||||
if (
|
||||
matcher.startsWith('/') &&
|
||||
matcher.endsWith('/') &&
|
||||
typeof logField === 'string'
|
||||
) {
|
||||
const regex = new RegExp(matcher.slice(1, -1));
|
||||
return regex.test(logField);
|
||||
// String matcher: maybe a slash-delimited regex (/pattern/flags)
|
||||
const regex = parseRegex(matcher);
|
||||
if (regex) {
|
||||
return (logField: unknown) =>
|
||||
typeof logField === 'string' && regex.test(logField);
|
||||
}
|
||||
|
||||
return logField === matcher;
|
||||
// Plain string matcher: strict equality
|
||||
return (logField: unknown) => logField === matcher;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines whether a log entry matches all specified override matchers.
|
||||
* Create a predicate function that determines whether a log entry matches
|
||||
* all specified override matchers.
|
||||
*
|
||||
* Iterates over each key-matcher pair in the provided `matchers` object,
|
||||
* retrieves the corresponding field from the `log` object, and checks if
|
||||
* the field matches the matcher using `isLogFieldMatching`. Returns `true`
|
||||
* only if all matchers are satisfied.
|
||||
*
|
||||
* @param log - The log entry to be checked, typically containing various log fields.
|
||||
* @param matchers - An object where each key corresponds to a log field and each value is a matcher to test against that field.
|
||||
* @returns `true` if the log entry matches all provided matchers, otherwise `false`.
|
||||
* @returns A function that takes a log entry and returns `true` if it matches all specified matchers, otherwise `false`.
|
||||
*/
|
||||
export const isLogMatching = (
|
||||
log: TransformableInfo,
|
||||
export const createLogMatcher = (
|
||||
matchers: WinstonLoggerLevelOverrideMatchers,
|
||||
): boolean => {
|
||||
const matched = Object.entries(matchers).every(([key, matcher]) => {
|
||||
const logField = log[key];
|
||||
return isLogFieldMatching(logField, matcher);
|
||||
): ((log: TransformableInfo) => boolean) => {
|
||||
const logFieldMatchers = Object.entries(matchers).map(([key, m]) => {
|
||||
const fn = createLogFieldMatcher(m);
|
||||
return [key, fn] as const;
|
||||
});
|
||||
|
||||
return matched;
|
||||
return (log: TransformableInfo) =>
|
||||
logFieldMatchers.every(([key, fn]) => fn(log[key]));
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user