diff --git a/packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.ts b/packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.ts index 90f3042e33..c48cef2f1f 100644 --- a/packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.ts +++ b/packages/backend-defaults/src/entrypoints/rootLogger/WinstonLogger.ts @@ -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); }, }; } diff --git a/packages/backend-defaults/src/entrypoints/rootLogger/utils.test.ts b/packages/backend-defaults/src/entrypoints/rootLogger/utils.test.ts index c60414cfc3..ede3de600b 100644 --- a/packages/backend-defaults/src/entrypoints/rootLogger/utils.test.ts +++ b/packages/backend-defaults/src/entrypoints/rootLogger/utils.test.ts @@ -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); }); }); diff --git a/packages/backend-defaults/src/entrypoints/rootLogger/utils.ts b/packages/backend-defaults/src/entrypoints/rootLogger/utils.ts index 205a1b1520..adf38fc4da 100644 --- a/packages/backend-defaults/src/entrypoints/rootLogger/utils.ts +++ b/packages/backend-defaults/src/entrypoints/rootLogger/utils.ts @@ -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])); };