[Feature Flags] ECS-compliant logger (#214231)

## Summary

The OpenFeature clients receive a logger, but it logs errors like
`log('something went wrong', error)`. Our core logger then removes the
`error.message` as it prefers the message provided as the first
argument.

This PR wraps the logger to make sure that we handle this type of usage.

cc @pmuellr as he found out about this bug


### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Alejandro Fernández Haro 2025-03-13 00:24:40 +01:00 committed by GitHub
parent 29a8ac5210
commit 98986a86a1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 104 additions and 1 deletions

View file

@ -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' }],
});
});
});

View file

@ -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]<LogMetaWithExtraArguments>(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),
};
}

View file

@ -25,6 +25,7 @@ import {
import deepMerge from 'deepmerge';
import { filter, switchMap, startWith, Subject } from 'rxjs';
import { get } from 'lodash';
import { createOpenFeatureLogger } from './create_open_feature_logger';
import { setProviderWithRetries } from './set_provider_with_retries';
import { type FeatureFlagsConfig, featureFlagsConfig } from './feature_flags_config';
@ -56,7 +57,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')));
}
/**