diff --git a/src/core/packages/feature-flags/server-internal/src/create_open_feature_logger.test.ts b/src/core/packages/feature-flags/server-internal/src/create_open_feature_logger.test.ts new file mode 100644 index 0000000000000..6ed5e50448c5e --- /dev/null +++ b/src/core/packages/feature-flags/server-internal/src/create_open_feature_logger.test.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { Logger as OpenFeatureLogger } from '@openfeature/server-sdk'; +import { type MockedLogger, loggerMock } from '@kbn/logging-mocks'; +import { createOpenFeatureLogger } from './create_open_feature_logger'; + +const LOG_LEVELS = ['debug', 'info', 'warn', 'error'] as const; + +describe('createOpenFeatureLogger', () => { + let kbnLogger: MockedLogger; + let openFeatureLogger: OpenFeatureLogger; + + beforeEach(() => { + kbnLogger = loggerMock.create(); + openFeatureLogger = createOpenFeatureLogger(kbnLogger); + }); + + test.each(LOG_LEVELS)('should log.%s() a simple message', (logLevel) => { + openFeatureLogger[logLevel]('message'); + expect(kbnLogger[logLevel]).toHaveBeenCalledWith('message', { extraArguments: [] }); + }); + + test.each(LOG_LEVELS)('should log.%s() a message with 1 argument (non-error)', (logLevel) => { + openFeatureLogger[logLevel]('message', 'something else'); + expect(kbnLogger[logLevel]).toHaveBeenCalledWith('message', { + extraArguments: ['something else'], + }); + }); + + test.each(LOG_LEVELS)( + 'should log.%s() a message with 1 argument (error) in their expected ECS field', + (logLevel) => { + const error = new Error('Something went wrong'); + openFeatureLogger[logLevel]('An error occurred:', error); + expect(kbnLogger[logLevel]).toHaveBeenCalledWith('An error occurred:', { error }); + } + ); + + test.each(LOG_LEVELS)('should log.%s() a message with additional arguments', (logLevel) => { + openFeatureLogger[logLevel]('message', 'something else', 'another thing', { foo: 'bar' }); + expect(kbnLogger[logLevel]).toHaveBeenCalledWith('message', { + extraArguments: ['something else', 'another thing', { foo: 'bar' }], + }); + }); +}); diff --git a/src/core/packages/feature-flags/server-internal/src/create_open_feature_logger.ts b/src/core/packages/feature-flags/server-internal/src/create_open_feature_logger.ts new file mode 100644 index 0000000000000..d45d1264e8c89 --- /dev/null +++ b/src/core/packages/feature-flags/server-internal/src/create_open_feature_logger.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { Logger as OpenFeatureLogger } from '@openfeature/server-sdk'; +import type { Logger, LogMeta } from '@kbn/logging'; + +interface LogMetaWithExtraArguments extends LogMeta { + extraArguments: unknown[]; +} + +function normalizeLogArguments( + logger: Logger, + logLevel: 'debug' | 'info' | 'warn' | 'error', + message: string, + ...args: unknown[] +) { + // Special case when calling log('something went wrong', error) + if (args.length === 1 && args[0] instanceof Error) { + logger[logLevel](message, { error: args[0] }); + } else { + logger[logLevel](message, { extraArguments: args }); + } +} + +/** + * The way OpenFeature logs messages is very similar to the console.log approach, + * which is not compatible with our LogMeta approach. This can result in our log removing information like any 3rd+ + * arguments passed or the error.message when using log('message', error). + * + * This wrapper addresses this by making it ECS-compliant. + * @param logger The Kibana logger + */ +export function createOpenFeatureLogger(logger: Logger): OpenFeatureLogger { + return { + debug: (message: string, ...args: unknown[]) => + normalizeLogArguments(logger, 'debug', message, ...args), + info: (message: string, ...args: unknown[]) => + normalizeLogArguments(logger, 'info', message, ...args), + warn: (message: string, ...args: unknown[]) => + normalizeLogArguments(logger, 'warn', message, ...args), + error: (message: string, ...args: unknown[]) => + normalizeLogArguments(logger, 'error', message, ...args), + }; +} diff --git a/src/core/packages/feature-flags/server-internal/src/feature_flags_service.ts b/src/core/packages/feature-flags/server-internal/src/feature_flags_service.ts index 47e158fafb5a3..48a2ac625abc6 100644 --- a/src/core/packages/feature-flags/server-internal/src/feature_flags_service.ts +++ b/src/core/packages/feature-flags/server-internal/src/feature_flags_service.ts @@ -24,6 +24,7 @@ import { } from '@openfeature/server-sdk'; import deepMerge from 'deepmerge'; import { filter, switchMap, startWith, Subject } from 'rxjs'; +import { createOpenFeatureLogger } from './create_open_feature_logger'; import { setProviderWithRetries } from './set_provider_with_retries'; import { type FeatureFlagsConfig, featureFlagsConfig } from './feature_flags_config'; @@ -55,7 +56,7 @@ export class FeatureFlagsService { constructor(private readonly core: CoreContext) { this.logger = core.logger.get('feature-flags-service'); this.featureFlagsClient = OpenFeature.getClient(); - OpenFeature.setLogger(this.logger.get('open-feature')); + OpenFeature.setLogger(createOpenFeatureLogger(this.logger.get('open-feature'))); } /**