From 175528cce350c835fbdfda3221aaa4f6ab41f9b2 Mon Sep 17 00:00:00 2001 From: Paul Schultz Date: Tue, 4 Mar 2025 13:21:19 -0600 Subject: [PATCH] feat: add backend.auditor.severityLogLevelMappings to map severity levels to log levels Signed-off-by: Paul Schultz --- .changeset/rotten-windows-fly.md | 5 + docs/backend-system/core-services/auditor.md | 38 ++++++ packages/backend-defaults/config.d.ts | 26 ++++ .../auditor/auditorServiceFactory.test.ts | 117 ++++++++++++++++++ .../auditor/auditorServiceFactory.ts | 57 +++++++-- 5 files changed, 236 insertions(+), 7 deletions(-) create mode 100644 .changeset/rotten-windows-fly.md diff --git a/.changeset/rotten-windows-fly.md b/.changeset/rotten-windows-fly.md new file mode 100644 index 0000000000..c476d45226 --- /dev/null +++ b/.changeset/rotten-windows-fly.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +Adds `backend.auditor.severityLogLevelMappings` to map severity levels to log levels. diff --git a/docs/backend-system/core-services/auditor.md b/docs/backend-system/core-services/auditor.md index 43e789a70f..fe4ecd196c 100644 --- a/docs/backend-system/core-services/auditor.md +++ b/docs/backend-system/core-services/auditor.md @@ -151,3 +151,41 @@ To clarify how to utilize the Auditor feature effectively, we recommend explorin - It illustrates how to detail various `eventId` values and their corresponding `meta` fields (e.g., `queryType`, `actionType`) for different plugin operations. These examples provide both a code-level demonstration and a documentation guideline for effectively utilizing the `AuditorService` to manage audit events within your Backstage plugins. + +## Severity Log Level Mappings + +The Auditor Service provides a way for plugins to log significant events, categorized by their severity. The `severityLogLevelMappings` configuration option enables you to customize how these severity levels are mapped to actual log levels within your Backstage backend, giving you precise control over the verbosity of your audit logs. + +### Configuration + +The `severityLogLevelMappings` are configured under the `backend.auditor` section of your `app-config.yaml` file. This structure allows you to specify the log level for each severity level supported by the Auditor Service. You can override individual severity levels without changing the entire mapping. + +Example configuration: + +```yaml +backend: + auditor: + severityLogLevelMappings: + low: debug + medium: info + high: warn + critical: error +``` + +### Severity Levels and Default Mappings + +The Auditor Service supports the following severity levels: + +- `low`: Represents low-importance events, typically informational or debug-level. +- `medium`: Represents events of moderate importance, requiring some attention. +- `high`: Represents high-importance events, potentially indicating a problem or security issue. +- `critical`: Represents critical events, requiring immediate attention. + +By default, these severity levels are mapped to the following log levels: + +- `low`: `debug` +- `medium`: `info` +- `high`: `info` +- `critical`: `info` + +As a result, medium, high, and critical events are logged as info-level events by default, while low-level events are treated as debug. diff --git a/packages/backend-defaults/config.d.ts b/packages/backend-defaults/config.d.ts index 45bf2f1c7e..efd14afc2e 100644 --- a/packages/backend-defaults/config.d.ts +++ b/packages/backend-defaults/config.d.ts @@ -82,6 +82,32 @@ export interface Config { }; }; + /** + * Options used by the default auditor service. + */ + auditor?: { + /** + * Defines how audit event severity levels are mapped to log levels. + * This allows you to control the verbosity of audit logs based on the + * severity of the event. For example, you might want to log 'low' severity + * events as 'debug' messages, while logging 'critical' events as 'error' + * messages. Each severity level ('low', 'medium', 'high', 'critical') + * can be mapped to one of the standard log levels ('debug', 'info', 'warn', 'error'). + * + * By default, audit events are mapped to log levels as follows: + * - `low`: `debug` + * - `medium`: `info` + * - `high`: `info` + * - `critical`: `info` + */ + severityLogLevelMappings?: { + low?: 'debug' | 'info' | 'warn' | 'error'; + medium?: 'debug' | 'info' | 'warn' | 'error'; + high?: 'debug' | 'info' | 'warn' | 'error'; + critical?: 'debug' | 'info' | 'warn' | 'error'; + }; + }; + /** * Options used by the default auth, httpAuth and userInfo services. */ diff --git a/packages/backend-defaults/src/entrypoints/auditor/auditorServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/auditor/auditorServiceFactory.test.ts index a4f5c96835..5c17d173d2 100644 --- a/packages/backend-defaults/src/entrypoints/auditor/auditorServiceFactory.test.ts +++ b/packages/backend-defaults/src/entrypoints/auditor/auditorServiceFactory.test.ts @@ -69,4 +69,121 @@ describe('auditorServiceFactory', () => { status: 'initiated', }); }); + + it('should log with custom log level mapping', async () => { + const mockLogger = mockServices.logger.mock(); + mockLogger.child.mockReturnValue(mockLogger); + + const auditor = await ServiceFactoryTester.from(auditorServiceFactory, { + dependencies: [ + mockLogger.factory, + mockServices.rootConfig.factory({ + data: { + backend: { + auditor: { + severityLogLevelMappings: { + low: 'info', + medium: 'debug', + high: 'warn', + critical: 'error', + }, + }, + }, + }, + }), + ], + }).getSubject(); + + await auditor.createEvent({ + eventId: 'test1', + severityLevel: 'low', + }); + await auditor.createEvent({ + eventId: 'test2', + }); + await auditor.createEvent({ + eventId: 'test3', + severityLevel: 'medium', + }); + await auditor.createEvent({ + eventId: 'test4', + severityLevel: 'high', + }); + await auditor.createEvent({ + eventId: 'test5', + severityLevel: 'critical', + }); + + expect(mockLogger.info).toHaveBeenCalledWith('test.test1', { + eventId: 'test1', + severityLevel: 'low', + actor: { + actorId: 'plugin:test', + }, + plugin: 'test', + status: 'initiated', + }); + expect(mockLogger.info).toHaveBeenCalledWith('test.test2', { + eventId: 'test2', + severityLevel: 'low', + actor: { + actorId: 'plugin:test', + }, + plugin: 'test', + status: 'initiated', + }); + expect(mockLogger.debug).toHaveBeenCalledWith('test.test3', { + eventId: 'test3', + severityLevel: 'medium', + actor: { + actorId: 'plugin:test', + }, + plugin: 'test', + status: 'initiated', + }); + expect(mockLogger.warn).toHaveBeenCalledWith('test.test4', { + eventId: 'test4', + severityLevel: 'high', + actor: { + actorId: 'plugin:test', + }, + plugin: 'test', + status: 'initiated', + }); + expect(mockLogger.error).toHaveBeenCalledWith('test.test5', { + eventId: 'test5', + severityLevel: 'critical', + actor: { + actorId: 'plugin:test', + }, + plugin: 'test', + status: 'initiated', + }); + }); + + it('should throw an error given an incorrect custom level', async () => { + const mockLogger = mockServices.logger.mock(); + mockLogger.child.mockReturnValue(mockLogger); + + await expect( + ServiceFactoryTester.from(auditorServiceFactory, { + dependencies: [ + mockLogger.factory, + mockServices.rootConfig.factory({ + data: { + backend: { + auditor: { + severityLogLevelMappings: { + low: 'invalidloglevel', + }, + }, + }, + }, + }), + ], + }).getSubject(), + ).rejects.toThrow( + "Failed to instantiate service 'core.auditor' for 'test' because the factory function threw an error, InputError: The configuration value for 'backend.auditor.severityLogLevelMappings.low' was given an invalid value: 'invalidloglevel'. Expected one of the following valid values: 'debug, info, warn, error'.", + ); + }); }); diff --git a/packages/backend-defaults/src/entrypoints/auditor/auditorServiceFactory.ts b/packages/backend-defaults/src/entrypoints/auditor/auditorServiceFactory.ts index 6d26bbf258..34ea627762 100644 --- a/packages/backend-defaults/src/entrypoints/auditor/auditorServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/auditor/auditorServiceFactory.ts @@ -19,6 +19,15 @@ import { createServiceFactory, } from '@backstage/backend-plugin-api'; import { DefaultAuditorService } from './DefaultAuditorService'; +import { z } from 'zod'; +import { InputError } from '@backstage/errors'; + +const CONFIG_ROOT_KEY = 'backend.auditor'; + +const severityLogLevelMappingsSchema = z.record( + z.enum(['low', 'medium', 'high', 'critical']), + z.enum(['debug', 'info', 'warn', 'error']), +); /** * Plugin-level auditing. @@ -32,21 +41,55 @@ import { DefaultAuditorService } from './DefaultAuditorService'; export const auditorServiceFactory = createServiceFactory({ service: coreServices.auditor, deps: { + config: coreServices.rootConfig, logger: coreServices.logger, auth: coreServices.auth, httpAuth: coreServices.httpAuth, plugin: coreServices.pluginMetadata, }, - factory({ logger, plugin, auth, httpAuth }) { + factory({ config, logger, plugin, auth, httpAuth }) { const auditLogger = logger.child({ isAuditEvent: true }); + const auditorConfig = config.getOptionalConfig(CONFIG_ROOT_KEY); + + const severityLogLevelMappings = { + low: + auditorConfig?.getOptionalString('severityLogLevelMappings.low') ?? + 'debug', + medium: + auditorConfig?.getOptionalString('severityLogLevelMappings.medium') ?? + 'info', + high: + auditorConfig?.getOptionalString('severityLogLevelMappings.high') ?? + 'info', + critical: + auditorConfig?.getOptionalString('severityLogLevelMappings.critical') ?? + 'info', + } as Required>; + + const res = severityLogLevelMappingsSchema.safeParse( + severityLogLevelMappings, + ); + if (!res.success) { + const key = res.error.issues.at(0)?.path.at(0) as string; + const value = ( + res.error.issues.at(0) as unknown as Record + ).received as string; + const validKeys = ( + res.error.issues.at(0) as unknown as Record + ).options as string[]; + throw new InputError( + `The configuration value for 'backend.auditor.severityLogLevelMappings.${key}' was given an invalid value: '${value}'. Expected one of the following valid values: '${validKeys.join( + ', ', + )}'.`, + ); + } + return DefaultAuditorService.create( event => { - const message = `${event.plugin}.${event.eventId}`; - if (event.severityLevel === 'low') { - auditLogger.debug(message, event); - } else { - auditLogger.info(message, event); - } + auditLogger[severityLogLevelMappings[event.severityLevel]]( + `${event.plugin}.${event.eventId}`, + event, + ); }, { plugin, auth, httpAuth }, );