feat(backend-defaults): add backend.logger configuration options
Signed-off-by: Thomas Cardonne <thomas.cardonne@adevinta.com>
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
---
|
||||
'@backstage/backend-defaults': patch
|
||||
---
|
||||
|
||||
Add `backend.logger` config options to configure the `RootLoggerService`.
|
||||
|
||||
Read more about the new configuration options in the
|
||||
[Root Logger Service](https://backstage.io/docs/backend-system/core-services/root-logger/)
|
||||
documentation.
|
||||
@@ -13,6 +13,51 @@ If you want to override the implementation for logging across all of the backend
|
||||
|
||||
## Configuring the service
|
||||
|
||||
The Root Logger Service can be configured with the `backend.logger` section of your `app-config.yaml`.
|
||||
|
||||
The following parameters are available:
|
||||
|
||||
- `level` (string, optional): Sets the global log level. Possible values are 'debug', 'info', 'warn', or 'error'. Only messages at or above this level will be logged. This can also be set via the `LOG_LEVEL` environment variable, which takes precedence. Defaults to 'info'.
|
||||
|
||||
- `meta` (object, optional): Additional metadata to include with every log entry.
|
||||
|
||||
- `overrides` (array, optional): Allows to specify logger overrides for specific plugins or messages. Each override can match on plugin names, message patterns, or any field contained in the log, and set a custom log level for those matches.
|
||||
|
||||
Log level overrides are useful for controlling the volume of logs generated in Backstage.
|
||||
They allow you to apply a global log level, `info` for example, while setting a stricter level, such as `warn`, for specific verbose plugins.
|
||||
|
||||
The reverse is also possible: you can set a global log level of `warn` while enabling a more detailed level, such as `debug`, for certain logs.
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
backend:
|
||||
logger:
|
||||
meta:
|
||||
env: prod # Every log message will have `env="prod"`
|
||||
|
||||
level: info # Set the global log level to info (the default)
|
||||
|
||||
overrides:
|
||||
# Set the log level to 'debug' for the catalog plugin logs
|
||||
- matchers:
|
||||
plugin: catalog
|
||||
level: debug
|
||||
|
||||
# Ignore 'info' incoming HTTP requests logs from the rootHttpRouter service
|
||||
- matchers:
|
||||
service: rootHttpRouter
|
||||
type: incomingRequest
|
||||
level: warn
|
||||
|
||||
# Ignore logs starting with "Task worker starting", unless they're warnings or errors
|
||||
- matchers:
|
||||
message: ['/^Task worker starting/']
|
||||
level: warn
|
||||
```
|
||||
|
||||
## Overriding the service
|
||||
|
||||
The following example is how you can override the root logger service to add additional metadata to all log lines.
|
||||
|
||||
```ts
|
||||
|
||||
+61
-1
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { HumanDuration } from '@backstage/types';
|
||||
import { HumanDuration, JsonObject } from '@backstage/types';
|
||||
|
||||
export interface Config {
|
||||
app: {
|
||||
@@ -790,6 +790,66 @@ export interface Config {
|
||||
headers?: { [name: string]: string };
|
||||
};
|
||||
|
||||
/**
|
||||
* Options to configure the default RootLoggerService.
|
||||
*/
|
||||
logger?: {
|
||||
/**
|
||||
* Configures the global log level for messages.
|
||||
*
|
||||
* This can also be configured using the LOG_LEVEL environment variable, which
|
||||
* takes precedence over this configuration.
|
||||
*
|
||||
* Defaults to 'info'.
|
||||
*/
|
||||
level?: 'debug' | 'info' | 'warn' | 'error';
|
||||
|
||||
/**
|
||||
* Additional metadata to include with every log entry.
|
||||
*/
|
||||
meta?: JsonObject;
|
||||
|
||||
/**
|
||||
* List of logger overrides.
|
||||
*
|
||||
* Can be used to configure a different level for logs matching certain criterias.
|
||||
* For example, it can be used to ignore 'info' logs of given plugins.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```yaml
|
||||
* logger:
|
||||
* level: info
|
||||
* overrides:
|
||||
* # For catalog and auth plugins, messages less important than 'warn' will be ignored.
|
||||
* - matchers:
|
||||
* plugin: [catalog, auth]
|
||||
* level: warn
|
||||
* # Ignore all messages that starts with 'Forget'
|
||||
* - matchers:
|
||||
* message: '/^Forget/'
|
||||
* level: warn
|
||||
* ```
|
||||
*/
|
||||
overrides?: Array<{
|
||||
/**
|
||||
* Conditions that must be met to override the log level.
|
||||
*
|
||||
* A 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)
|
||||
*/
|
||||
matchers: JsonObject;
|
||||
|
||||
/**
|
||||
* Log level to use for matched entries.
|
||||
*/
|
||||
level: 'debug' | 'info' | 'warn' | 'error';
|
||||
}>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Rate limiting options. Defining this as `true` will enable rate limiting with default values.
|
||||
*/
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { config } from 'winston';
|
||||
import { Format } from 'logform';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { JsonValue } from '@backstage/types';
|
||||
import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import { RootLoggerService } from '@backstage/backend-plugin-api';
|
||||
import { ServiceFactory } from '@backstage/backend-plugin-api';
|
||||
@@ -31,14 +33,31 @@ export class WinstonLogger implements RootLoggerService {
|
||||
error(message: string, meta?: JsonObject): void;
|
||||
// (undocumented)
|
||||
info(message: string, meta?: JsonObject): void;
|
||||
static logLevelFilter(defaultLogLevel: keyof config.NpmConfigSetLevels): {
|
||||
format: Format;
|
||||
setOverrides: (overrides: WinstonLoggerLevelOverride[]) => void;
|
||||
};
|
||||
static redacter(): {
|
||||
format: Format;
|
||||
add: (redactions: Iterable<string>) => void;
|
||||
};
|
||||
// (undocumented)
|
||||
setLevelOverrides(overrides: WinstonLoggerLevelOverride[]): void;
|
||||
// (undocumented)
|
||||
warn(message: string, meta?: JsonObject): void;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type WinstonLoggerLevelOverride = {
|
||||
matchers: WinstonLoggerLevelOverrideMatchers;
|
||||
level: string;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type WinstonLoggerLevelOverrideMatchers = {
|
||||
[key: string]: JsonValue | undefined;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export interface WinstonLoggerOptions {
|
||||
// (undocumented)
|
||||
|
||||
@@ -118,4 +118,56 @@ describe('WinstonLogger', () => {
|
||||
add([null as any, undefined as any, 'valid-secret']);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should filter logs below the default log level', () => {
|
||||
const mockTransport = new Transport({
|
||||
log: jest.fn(),
|
||||
logv: jest.fn(),
|
||||
});
|
||||
|
||||
const logger = WinstonLogger.create({
|
||||
level: 'warn',
|
||||
format: format.json(),
|
||||
transports: [mockTransport],
|
||||
});
|
||||
|
||||
logger.debug('debug log');
|
||||
|
||||
expect(mockTransport.log).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not filter logs below the default log level with an override', () => {
|
||||
const mockTransport = new Transport({
|
||||
log: jest.fn(),
|
||||
logv: jest.fn(),
|
||||
});
|
||||
|
||||
const logger = WinstonLogger.create({
|
||||
level: 'warn',
|
||||
format: format.json(),
|
||||
transports: [mockTransport],
|
||||
});
|
||||
|
||||
logger.setLevelOverrides([
|
||||
{
|
||||
matchers: {
|
||||
plugin: 'catalog',
|
||||
},
|
||||
level: 'debug',
|
||||
},
|
||||
]);
|
||||
|
||||
logger.debug('debug log', { plugin: 'catalog' });
|
||||
|
||||
expect(mockTransport.log).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
[MESSAGE]: JSON.stringify({
|
||||
level: 'debug',
|
||||
message: 'debug log',
|
||||
plugin: 'catalog',
|
||||
}),
|
||||
}),
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,9 +26,12 @@ import {
|
||||
createLogger,
|
||||
transports,
|
||||
transport as Transport,
|
||||
config as winstonConfig,
|
||||
} from 'winston';
|
||||
import { MESSAGE } from 'triple-beam';
|
||||
import { escapeRegExp } from '../../lib/escapeRegExp';
|
||||
import { winstonLevels, WinstonLoggerLevelOverride } from './types';
|
||||
import { isLogMatching } from './utils';
|
||||
|
||||
/**
|
||||
* @public
|
||||
@@ -48,20 +51,27 @@ export interface WinstonLoggerOptions {
|
||||
export class WinstonLogger implements RootLoggerService {
|
||||
#winston: Logger;
|
||||
#addRedactions?: (redactions: Iterable<string>) => void;
|
||||
#setLevelOverrides?: (overrides: WinstonLoggerLevelOverride[]) => void;
|
||||
|
||||
/**
|
||||
* Creates a {@link WinstonLogger} instance.
|
||||
*/
|
||||
static create(options: WinstonLoggerOptions): WinstonLogger {
|
||||
const defaultLogLevel = process.env.LOG_LEVEL || options.level || 'info';
|
||||
|
||||
const redacter = WinstonLogger.redacter();
|
||||
const logLevelFilter = WinstonLogger.logLevelFilter(defaultLogLevel);
|
||||
|
||||
const defaultFormatter =
|
||||
process.env.NODE_ENV === 'production'
|
||||
? format.json()
|
||||
: WinstonLogger.colorFormat();
|
||||
|
||||
let logger = createLogger({
|
||||
level: process.env.LOG_LEVEL || options.level || 'info',
|
||||
// Lowest level possible as we let the logLevelFilter do the filtering
|
||||
level: 'silly',
|
||||
format: format.combine(
|
||||
logLevelFilter.format,
|
||||
options.format ?? defaultFormatter,
|
||||
redacter.format,
|
||||
),
|
||||
@@ -72,7 +82,7 @@ export class WinstonLogger implements RootLoggerService {
|
||||
logger = logger.child(options.meta);
|
||||
}
|
||||
|
||||
return new WinstonLogger(logger, redacter.add);
|
||||
return new WinstonLogger(logger, redacter.add, logLevelFilter.setOverrides);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -169,12 +179,54 @@ export class WinstonLogger implements RootLoggerService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatter that filters log levels using overrides, falling back to the default level when no criteria match.
|
||||
*/
|
||||
static logLevelFilter(
|
||||
defaultLogLevel: keyof winstonConfig.NpmConfigSetLevels,
|
||||
): {
|
||||
format: Format;
|
||||
setOverrides: (overrides: WinstonLoggerLevelOverride[]) => void;
|
||||
} {
|
||||
const overrides: WinstonLoggerLevelOverride[] = [];
|
||||
|
||||
return {
|
||||
format: format(log => {
|
||||
for (const override of overrides) {
|
||||
if (isLogMatching(log, override.matchers)) {
|
||||
// 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]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return log;
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore logs that are below the global level
|
||||
// eg, if the global level is 'warn' (1) and the log level is 'debug' (5)
|
||||
if (winstonLevels[log.level] > winstonLevels[defaultLogLevel]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return log;
|
||||
})(),
|
||||
setOverrides: newOverrides => {
|
||||
// Replace the content while preserving the reference
|
||||
overrides.splice(0, overrides.length, ...newOverrides);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private constructor(
|
||||
winston: Logger,
|
||||
addRedactions?: (redactions: Iterable<string>) => void,
|
||||
setLevelOverrides?: (overrides: WinstonLoggerLevelOverride[]) => void,
|
||||
) {
|
||||
this.#winston = winston;
|
||||
this.#addRedactions = addRedactions;
|
||||
this.#setLevelOverrides = setLevelOverrides;
|
||||
}
|
||||
|
||||
error(message: string, meta?: JsonObject): void {
|
||||
@@ -200,4 +252,8 @@ export class WinstonLogger implements RootLoggerService {
|
||||
addRedactions(redactions: Iterable<string>) {
|
||||
this.#addRedactions?.(redactions);
|
||||
}
|
||||
|
||||
setLevelOverrides(overrides: WinstonLoggerLevelOverride[]) {
|
||||
this.#setLevelOverrides?.(overrides);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { mockServices } from '@backstage/backend-test-utils';
|
||||
import { getRootLoggerConfig } from './config';
|
||||
|
||||
describe('getRootLoggerConfig', () => {
|
||||
it('should load the configuration without throwing', () => {
|
||||
const config = {
|
||||
backend: {
|
||||
logger: {
|
||||
level: 'info',
|
||||
meta: {
|
||||
env: 'prod',
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
matchers: {
|
||||
plugin: 'catalog',
|
||||
},
|
||||
level: 'warn',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(() =>
|
||||
getRootLoggerConfig(
|
||||
mockServices.rootConfig({
|
||||
data: config,
|
||||
}),
|
||||
),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('should throw if an override is using an invalid level', () => {
|
||||
const config = {
|
||||
backend: {
|
||||
logger: {
|
||||
level: 'info',
|
||||
meta: {
|
||||
env: 'prod',
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
matchers: {
|
||||
plugin: 'catalog',
|
||||
},
|
||||
level: 'invalid',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(() =>
|
||||
getRootLoggerConfig(mockServices.rootConfig({ data: config })),
|
||||
).toThrow(
|
||||
"Invalid config at backend.logger.overrides[0].level, 'invalid' is not a valid Winston npm logging level",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { RootConfigService } from '@backstage/backend-plugin-api';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { RootLoggerConfig, winstonLevels } from './types';
|
||||
|
||||
export const getRootLoggerConfig = (
|
||||
config: RootConfigService,
|
||||
): RootLoggerConfig => {
|
||||
const level = config.getOptionalString('backend.logger.level');
|
||||
const meta = config
|
||||
.getOptionalConfig('backend.logger.meta')
|
||||
?.get<JsonObject>();
|
||||
|
||||
const overridesConfig = config.getOptionalConfigArray(
|
||||
'backend.logger.overrides',
|
||||
);
|
||||
const overrides = overridesConfig?.map((override, i) => {
|
||||
const overrideLevel = override.getString('level');
|
||||
if (winstonLevels[overrideLevel] === undefined) {
|
||||
throw new Error(
|
||||
`Invalid config at backend.logger.overrides[${i}].level, '${overrideLevel}' is not a valid Winston npm logging level`,
|
||||
);
|
||||
}
|
||||
|
||||
const matchers = override.getConfig('matchers').get<JsonObject>();
|
||||
|
||||
return {
|
||||
matchers,
|
||||
level: overrideLevel,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
meta,
|
||||
level,
|
||||
overrides,
|
||||
};
|
||||
};
|
||||
@@ -16,3 +16,7 @@
|
||||
|
||||
export { rootLoggerServiceFactory } from './rootLoggerServiceFactory';
|
||||
export { WinstonLogger, type WinstonLoggerOptions } from './WinstonLogger';
|
||||
export {
|
||||
type WinstonLoggerLevelOverride,
|
||||
type WinstonLoggerLevelOverrideMatchers,
|
||||
} from './types';
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
mockServices,
|
||||
ServiceFactoryTester,
|
||||
} from '@backstage/backend-test-utils';
|
||||
import { rootLoggerServiceFactory } from './rootLoggerServiceFactory';
|
||||
|
||||
import { WinstonLogger } from './WinstonLogger';
|
||||
|
||||
describe('rootLoggerServiceFactory', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(WinstonLogger, 'create');
|
||||
});
|
||||
|
||||
it('should create WinstonLogger with defaults', async () => {
|
||||
await ServiceFactoryTester.from(rootLoggerServiceFactory, {
|
||||
dependencies: [mockServices.rootConfig.factory()],
|
||||
}).getSubject();
|
||||
|
||||
expect(WinstonLogger.create).toHaveBeenCalledWith({
|
||||
level: 'info',
|
||||
meta: {
|
||||
service: 'backstage',
|
||||
},
|
||||
format: expect.anything(),
|
||||
transports: expect.anything(),
|
||||
});
|
||||
});
|
||||
|
||||
it('should create WinstonLogger from config', async () => {
|
||||
await ServiceFactoryTester.from(rootLoggerServiceFactory, {
|
||||
dependencies: [
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
backend: {
|
||||
logger: {
|
||||
meta: {
|
||||
env: 'test',
|
||||
},
|
||||
level: 'warn',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
}).getSubject();
|
||||
|
||||
expect(WinstonLogger.create).toHaveBeenCalledWith({
|
||||
level: 'warn',
|
||||
meta: {
|
||||
service: 'backstage',
|
||||
env: 'test',
|
||||
},
|
||||
format: expect.anything(),
|
||||
transports: expect.anything(),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -15,12 +15,13 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
createServiceFactory,
|
||||
coreServices,
|
||||
createServiceFactory,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { transports, format } from 'winston';
|
||||
import { WinstonLogger } from '../rootLogger/WinstonLogger';
|
||||
import { format, transports } from 'winston';
|
||||
import { createConfigSecretEnumerator } from '../rootConfig/createConfigSecretEnumerator';
|
||||
import { WinstonLogger } from '../rootLogger/WinstonLogger';
|
||||
import { getRootLoggerConfig } from './config';
|
||||
|
||||
/**
|
||||
* Root-level logging.
|
||||
@@ -37,11 +38,14 @@ export const rootLoggerServiceFactory = createServiceFactory({
|
||||
config: coreServices.rootConfig,
|
||||
},
|
||||
async factory({ config }) {
|
||||
const rootLoggerConfig = getRootLoggerConfig(config);
|
||||
|
||||
const logger = WinstonLogger.create({
|
||||
meta: {
|
||||
service: 'backstage',
|
||||
...rootLoggerConfig.meta,
|
||||
},
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
level: process.env.LOG_LEVEL || rootLoggerConfig.level || 'info',
|
||||
format:
|
||||
process.env.NODE_ENV === 'production'
|
||||
? format.json()
|
||||
@@ -53,6 +57,11 @@ export const rootLoggerServiceFactory = createServiceFactory({
|
||||
logger.addRedactions(secretEnumerator(config));
|
||||
config.subscribe?.(() => logger.addRedactions(secretEnumerator(config)));
|
||||
|
||||
logger.setLevelOverrides(rootLoggerConfig.overrides ?? []);
|
||||
config.subscribe?.(() =>
|
||||
logger.setLevelOverrides(getRootLoggerConfig(config).overrides ?? []),
|
||||
);
|
||||
|
||||
return logger;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { JsonObject, JsonValue } from '@backstage/types';
|
||||
import { config as winstonConfig } from 'winston';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type WinstonLoggerLevelOverrideMatchers = {
|
||||
[key: string]: JsonValue | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type WinstonLoggerLevelOverride = {
|
||||
matchers: WinstonLoggerLevelOverrideMatchers;
|
||||
level: string;
|
||||
};
|
||||
|
||||
export type RootLoggerConfig = {
|
||||
level?: string;
|
||||
meta?: JsonObject;
|
||||
overrides?: WinstonLoggerLevelOverride[];
|
||||
};
|
||||
|
||||
export const winstonLevels = winstonConfig.npm.levels;
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { isLogMatching } from './utils';
|
||||
|
||||
describe('isLogMatching', () => {
|
||||
const log = {
|
||||
level: 'info',
|
||||
message: 'This is a simple log from the catalog plugin',
|
||||
plugin: 'catalog',
|
||||
status: 200,
|
||||
action: 'read',
|
||||
};
|
||||
|
||||
it('should match with a simple matcher', () => {
|
||||
expect(
|
||||
isLogMatching(log, {
|
||||
plugin: 'catalog',
|
||||
}),
|
||||
).toEqual(true);
|
||||
});
|
||||
|
||||
it('should not match with a simple matcher', () => {
|
||||
expect(
|
||||
isLogMatching(log, {
|
||||
plugin: 'search',
|
||||
}),
|
||||
).toEqual(false);
|
||||
});
|
||||
|
||||
it('should match with an AND matcher', () => {
|
||||
expect(
|
||||
isLogMatching(log, {
|
||||
plugin: 'catalog',
|
||||
action: 'read',
|
||||
}),
|
||||
).toEqual(true);
|
||||
});
|
||||
|
||||
it('should not match log with an AND matcher', () => {
|
||||
expect(
|
||||
isLogMatching(log, {
|
||||
plugin: 'catalog',
|
||||
action: 'write',
|
||||
}),
|
||||
).toEqual(false);
|
||||
});
|
||||
|
||||
it('should match with an OR matcher', () => {
|
||||
expect(
|
||||
isLogMatching(log, {
|
||||
plugin: ['auth', 'catalog'],
|
||||
}),
|
||||
).toEqual(true);
|
||||
});
|
||||
|
||||
it('should not match log with an OR matcher', () => {
|
||||
expect(
|
||||
isLogMatching(log, {
|
||||
plugin: ['auth', 'search'],
|
||||
}),
|
||||
).toEqual(false);
|
||||
});
|
||||
|
||||
it('should match log with a regex matcher', () => {
|
||||
expect(
|
||||
isLogMatching(log, {
|
||||
message: '/This is a simple log/',
|
||||
}),
|
||||
).toEqual(true);
|
||||
});
|
||||
|
||||
it('should not match log with a regex matcher', () => {
|
||||
expect(
|
||||
isLogMatching(log, {
|
||||
message: '/^simple log/',
|
||||
}),
|
||||
).toEqual(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { TransformableInfo } from 'logform';
|
||||
import { WinstonLoggerLevelOverrideMatchers } from './types';
|
||||
|
||||
/**
|
||||
* Determines if a given log field matches a specified 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`.
|
||||
*/
|
||||
const isLogFieldMatching = (
|
||||
logField: unknown,
|
||||
matcher: WinstonLoggerLevelOverrideMatchers[0],
|
||||
): boolean => {
|
||||
if (Array.isArray(matcher)) {
|
||||
return matcher.some(m => isLogFieldMatching(logField, m));
|
||||
}
|
||||
|
||||
if (typeof matcher !== 'string') {
|
||||
return logField === matcher;
|
||||
}
|
||||
|
||||
if (
|
||||
matcher.startsWith('/') &&
|
||||
matcher.endsWith('/') &&
|
||||
typeof logField === 'string'
|
||||
) {
|
||||
const regex = new RegExp(matcher.slice(1, -1));
|
||||
return regex.test(logField);
|
||||
}
|
||||
|
||||
return logField === matcher;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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`.
|
||||
*/
|
||||
export const isLogMatching = (
|
||||
log: TransformableInfo,
|
||||
matchers: WinstonLoggerLevelOverrideMatchers,
|
||||
): boolean => {
|
||||
const matched = Object.entries(matchers).every(([key, matcher]) => {
|
||||
const logField = log[key];
|
||||
return isLogFieldMatching(logField, matcher);
|
||||
});
|
||||
|
||||
return matched;
|
||||
};
|
||||
Reference in New Issue
Block a user