mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[logging] Add mechanism for setting global meta & set service.node.roles
for all logs. (#136243)
This commit is contained in:
parent
8a5bf42bc0
commit
71d375848e
17 changed files with 645 additions and 101 deletions
|
@ -31,6 +31,7 @@ RUNTIME_DEPS = [
|
|||
"@npm//elastic-apm-node",
|
||||
"//packages/elastic-safer-lodash-set",
|
||||
"//packages/kbn-config-schema",
|
||||
"//packages/kbn-std",
|
||||
]
|
||||
|
||||
TYPES_DEPS = [
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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 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 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { mergeGlobalContext } from './merge_global_context';
|
||||
export type { GlobalContext } from './types';
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* 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 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 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { LogMeta } from '@kbn/logging';
|
||||
import { GlobalContext } from './types';
|
||||
import { mergeGlobalContext } from './merge_global_context';
|
||||
|
||||
describe('mergeGlobalContext', () => {
|
||||
test('inserts global meta in entry meta', () => {
|
||||
const context: GlobalContext = {
|
||||
bar: false,
|
||||
};
|
||||
const meta: LogMeta = {
|
||||
// @ts-expect-error Custom ECS field
|
||||
foo: true,
|
||||
};
|
||||
|
||||
expect(mergeGlobalContext(context, meta)).toEqual({
|
||||
foo: true,
|
||||
bar: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('handles nested context', () => {
|
||||
const context: GlobalContext = {
|
||||
'bar.baz': false,
|
||||
};
|
||||
const meta: LogMeta = {
|
||||
// @ts-expect-error Custom ECS field
|
||||
foo: true,
|
||||
};
|
||||
|
||||
expect(mergeGlobalContext(context, meta)).toEqual({
|
||||
foo: true,
|
||||
bar: { baz: false },
|
||||
});
|
||||
});
|
||||
|
||||
test('does not overwrite meta with global context if the path already exists', () => {
|
||||
const context: GlobalContext = {
|
||||
foo: false,
|
||||
bar: [false],
|
||||
};
|
||||
const meta: LogMeta = {
|
||||
// @ts-expect-error Custom ECS field
|
||||
foo: true,
|
||||
bar: [true],
|
||||
};
|
||||
|
||||
expect(mergeGlobalContext(context, meta)).toEqual({
|
||||
foo: true,
|
||||
bar: [true],
|
||||
});
|
||||
});
|
||||
|
||||
test('if conflicting entries exist in the context, the most specific entry wins', () => {
|
||||
const context: GlobalContext = {
|
||||
'a.b.c': 'd',
|
||||
'a.b': 'c',
|
||||
};
|
||||
|
||||
// Note that this "most specific entry wins" behavior should not happen in practice,
|
||||
// as the `LoggingSystem` is handling deconfliction of paths before anything is
|
||||
// provided to the `LoggerAdapter` in the first place. Including this test just to
|
||||
// ensure the actual behavior of this function is documented for posterity.
|
||||
expect(mergeGlobalContext(context)).toEqual({
|
||||
a: { b: { c: 'd' } },
|
||||
});
|
||||
});
|
||||
|
||||
test('does nothing if no global meta has been set', () => {
|
||||
const context: GlobalContext = {};
|
||||
const meta: LogMeta = {
|
||||
// @ts-expect-error Custom ECS field
|
||||
foo: true,
|
||||
};
|
||||
|
||||
expect(mergeGlobalContext(context, meta)).toEqual({
|
||||
foo: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('adds global meta even if no user-provided meta exists', () => {
|
||||
const context: GlobalContext = {
|
||||
foo: true,
|
||||
};
|
||||
|
||||
expect(mergeGlobalContext(context)).toEqual({
|
||||
foo: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('does nothing if no global meta or user-provided meta has been set', () => {
|
||||
const context: GlobalContext = {};
|
||||
|
||||
expect(mergeGlobalContext(context)).toBeUndefined();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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 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 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { has } from 'lodash';
|
||||
import { set } from '@elastic/safer-lodash-set';
|
||||
import { LogMeta } from '@kbn/logging';
|
||||
import { GlobalContext } from './types';
|
||||
|
||||
/**
|
||||
* Takes a flattened object of {@link GlobalContext} and applies it to the
|
||||
* provided {@link LogMeta}.
|
||||
*
|
||||
* @remarks
|
||||
* The provided `LogMeta` takes precedence over the `GlobalContext`;
|
||||
* if duplicate keys are found, the `GlobalContext` will be overridden.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const meta: LogMeta = {
|
||||
* a: { b: false },
|
||||
* d: 'hi',
|
||||
* };
|
||||
* const context: GlobalContext = {
|
||||
* 'a.b': true,
|
||||
* c: [1, 2, 3],
|
||||
* };
|
||||
*
|
||||
* mergeGlobalContext(context, meta);
|
||||
* // {
|
||||
* // a: { b: false },
|
||||
* // c: [1, 2, 3],
|
||||
* // d: 'hi',
|
||||
* // }
|
||||
* ```
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function mergeGlobalContext(globalContext: GlobalContext, meta?: LogMeta) {
|
||||
if (!meta && Object.keys(globalContext).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mergedMeta = meta ?? {};
|
||||
for (const [path, data] of Object.entries(globalContext)) {
|
||||
if (!has(mergedMeta, path)) {
|
||||
set(mergedMeta, path, data);
|
||||
}
|
||||
}
|
||||
|
||||
return mergedMeta;
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 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 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A flattened object containing lodash-style dot-separated keys, which
|
||||
* indicate the path to where each corresponding value should live in a
|
||||
* nested object.
|
||||
*
|
||||
* @remarks
|
||||
* Arrays are treated as primitives here: array entries should not be broken
|
||||
* down into separate keys.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const context: GlobalContext = {
|
||||
* a: true,
|
||||
* 'b.c': [1, 2, 3],
|
||||
* 'd.e.f': 'g',
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export type GlobalContext = Record<string, unknown>;
|
|
@ -7,84 +7,172 @@
|
|||
*/
|
||||
|
||||
import type { Logger } from '@kbn/logging';
|
||||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
import { LoggerAdapter } from './logger_adapter';
|
||||
|
||||
test('proxies all method calls to the internal logger.', () => {
|
||||
const internalLogger: Logger = {
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
fatal: jest.fn(),
|
||||
info: jest.fn(),
|
||||
log: jest.fn(),
|
||||
trace: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
get: jest.fn(),
|
||||
};
|
||||
describe('LoggerAdapter', () => {
|
||||
let internalLogger: Logger;
|
||||
|
||||
const adapter = new LoggerAdapter(internalLogger);
|
||||
beforeEach(() => {
|
||||
internalLogger = loggerMock.create();
|
||||
});
|
||||
|
||||
adapter.trace('trace-message');
|
||||
expect(internalLogger.trace).toHaveBeenCalledTimes(1);
|
||||
expect(internalLogger.trace).toHaveBeenCalledWith('trace-message', undefined);
|
||||
test('proxies all method calls to the internal logger.', () => {
|
||||
const adapter = new LoggerAdapter(internalLogger);
|
||||
|
||||
adapter.debug('debug-message');
|
||||
expect(internalLogger.debug).toHaveBeenCalledTimes(1);
|
||||
expect(internalLogger.debug).toHaveBeenCalledWith('debug-message', undefined);
|
||||
adapter.trace('trace-message');
|
||||
expect(internalLogger.trace).toHaveBeenCalledTimes(1);
|
||||
expect(internalLogger.trace).toHaveBeenCalledWith('trace-message', undefined);
|
||||
|
||||
adapter.info('info-message');
|
||||
expect(internalLogger.info).toHaveBeenCalledTimes(1);
|
||||
expect(internalLogger.info).toHaveBeenCalledWith('info-message', undefined);
|
||||
adapter.debug('debug-message');
|
||||
expect(internalLogger.debug).toHaveBeenCalledTimes(1);
|
||||
expect(internalLogger.debug).toHaveBeenCalledWith('debug-message', undefined);
|
||||
|
||||
adapter.warn('warn-message');
|
||||
expect(internalLogger.warn).toHaveBeenCalledTimes(1);
|
||||
expect(internalLogger.warn).toHaveBeenCalledWith('warn-message', undefined);
|
||||
adapter.info('info-message');
|
||||
expect(internalLogger.info).toHaveBeenCalledTimes(1);
|
||||
expect(internalLogger.info).toHaveBeenCalledWith('info-message', undefined);
|
||||
|
||||
adapter.error('error-message');
|
||||
expect(internalLogger.error).toHaveBeenCalledTimes(1);
|
||||
expect(internalLogger.error).toHaveBeenCalledWith('error-message', undefined);
|
||||
adapter.warn('warn-message');
|
||||
expect(internalLogger.warn).toHaveBeenCalledTimes(1);
|
||||
expect(internalLogger.warn).toHaveBeenCalledWith('warn-message', undefined);
|
||||
|
||||
adapter.fatal('fatal-message');
|
||||
expect(internalLogger.fatal).toHaveBeenCalledTimes(1);
|
||||
expect(internalLogger.fatal).toHaveBeenCalledWith('fatal-message', undefined);
|
||||
adapter.error('error-message');
|
||||
expect(internalLogger.error).toHaveBeenCalledTimes(1);
|
||||
expect(internalLogger.error).toHaveBeenCalledWith('error-message', undefined);
|
||||
|
||||
adapter.get('context');
|
||||
expect(internalLogger.get).toHaveBeenCalledTimes(1);
|
||||
expect(internalLogger.get).toHaveBeenCalledWith('context');
|
||||
});
|
||||
|
||||
test('forwards all method calls to new internal logger if it is updated.', () => {
|
||||
const oldInternalLogger: Logger = {
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
fatal: jest.fn(),
|
||||
info: jest.fn(),
|
||||
log: jest.fn(),
|
||||
trace: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
get: jest.fn(),
|
||||
};
|
||||
|
||||
const newInternalLogger: Logger = {
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
fatal: jest.fn(),
|
||||
info: jest.fn(),
|
||||
log: jest.fn(),
|
||||
trace: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
get: jest.fn(),
|
||||
};
|
||||
|
||||
const adapter = new LoggerAdapter(oldInternalLogger);
|
||||
|
||||
adapter.trace('trace-message');
|
||||
expect(oldInternalLogger.trace).toHaveBeenCalledTimes(1);
|
||||
expect(oldInternalLogger.trace).toHaveBeenCalledWith('trace-message', undefined);
|
||||
(oldInternalLogger.trace as jest.Mock<() => void>).mockReset();
|
||||
|
||||
adapter.updateLogger(newInternalLogger);
|
||||
adapter.trace('trace-message');
|
||||
expect(oldInternalLogger.trace).not.toHaveBeenCalled();
|
||||
expect(newInternalLogger.trace).toHaveBeenCalledTimes(1);
|
||||
expect(newInternalLogger.trace).toHaveBeenCalledWith('trace-message', undefined);
|
||||
adapter.fatal('fatal-message');
|
||||
expect(internalLogger.fatal).toHaveBeenCalledTimes(1);
|
||||
expect(internalLogger.fatal).toHaveBeenCalledWith('fatal-message', undefined);
|
||||
|
||||
adapter.get('context');
|
||||
expect(internalLogger.get).toHaveBeenCalledTimes(1);
|
||||
expect(internalLogger.get).toHaveBeenCalledWith('context');
|
||||
});
|
||||
|
||||
test('forwards all method calls to new internal logger if it is updated.', () => {
|
||||
const newInternalLogger = loggerMock.create();
|
||||
|
||||
const adapter = new LoggerAdapter(internalLogger);
|
||||
|
||||
adapter.trace('trace-message');
|
||||
expect(internalLogger.trace).toHaveBeenCalledTimes(1);
|
||||
expect(internalLogger.trace).toHaveBeenCalledWith('trace-message', undefined);
|
||||
(internalLogger.trace as jest.Mock<() => void>).mockReset();
|
||||
|
||||
adapter.updateLogger(newInternalLogger);
|
||||
adapter.trace('trace-message');
|
||||
expect(internalLogger.trace).not.toHaveBeenCalled();
|
||||
expect(newInternalLogger.trace).toHaveBeenCalledTimes(1);
|
||||
expect(newInternalLogger.trace).toHaveBeenCalledWith('trace-message', undefined);
|
||||
});
|
||||
|
||||
describe('global context', () => {
|
||||
['trace', 'debug', 'info', 'warn', 'error', 'fatal'].forEach((method) => {
|
||||
test(`inserts global context in ${method} entries`, () => {
|
||||
const adapter = new LoggerAdapter(internalLogger, { 'a.b.c': `${method}: d` });
|
||||
|
||||
// @ts-expect-error Custom ECS field
|
||||
adapter[method](`new ${method} message`, { hello: 'world' });
|
||||
expect(internalLogger[method as keyof Logger]).toHaveBeenCalledTimes(1);
|
||||
expect(internalLogger[method as keyof Logger]).toHaveBeenCalledWith(
|
||||
`new ${method} message`,
|
||||
{
|
||||
hello: 'world',
|
||||
a: { b: { c: `${method}: d` } },
|
||||
}
|
||||
);
|
||||
|
||||
adapter.updateGlobalContext({ e: true });
|
||||
|
||||
// @ts-expect-error Custom ECS field
|
||||
adapter[method](`another new ${method} message`, { hello: 'world' });
|
||||
expect(internalLogger[method as keyof Logger]).toHaveBeenCalledTimes(2);
|
||||
expect(internalLogger[method as keyof Logger]).toHaveBeenCalledWith(
|
||||
`another new ${method} message`,
|
||||
{
|
||||
hello: 'world',
|
||||
e: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('inserts global meta in log entries', () => {
|
||||
const adapter = new LoggerAdapter(internalLogger, { 'a.b.c': 'd' });
|
||||
|
||||
adapter.log({
|
||||
message: 'message',
|
||||
meta: {
|
||||
// @ts-expect-error Custom ECS field
|
||||
hello: 'world',
|
||||
},
|
||||
});
|
||||
expect(internalLogger.log).toHaveBeenCalledTimes(1);
|
||||
expect(internalLogger.log).toHaveBeenCalledWith({
|
||||
message: 'message',
|
||||
meta: {
|
||||
hello: 'world',
|
||||
a: { b: { c: 'd' } },
|
||||
},
|
||||
});
|
||||
|
||||
adapter.updateGlobalContext({ e: true });
|
||||
|
||||
adapter.log({
|
||||
message: 'another message',
|
||||
meta: {
|
||||
// @ts-expect-error Custom ECS field
|
||||
hello: 'world',
|
||||
},
|
||||
});
|
||||
expect(internalLogger.log).toHaveBeenCalledTimes(2);
|
||||
expect(internalLogger.log).toHaveBeenCalledWith({
|
||||
message: 'another message',
|
||||
meta: {
|
||||
hello: 'world',
|
||||
e: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('does not overwrite user-provided meta with global meta if the path already exists', () => {
|
||||
const adapter = new LoggerAdapter(internalLogger, { hello: 'there' });
|
||||
|
||||
// @ts-expect-error Custom ECS field
|
||||
adapter.info('message', { hello: 'world' });
|
||||
expect(internalLogger.info).toHaveBeenCalledTimes(1);
|
||||
expect(internalLogger.info).toHaveBeenCalledWith('message', {
|
||||
hello: 'world',
|
||||
});
|
||||
});
|
||||
|
||||
test('does nothing if no global meta has been set', () => {
|
||||
const adapter = new LoggerAdapter(internalLogger);
|
||||
|
||||
// @ts-expect-error Custom ECS field
|
||||
adapter.info('message', { hello: 'world' });
|
||||
expect(internalLogger.info).toHaveBeenCalledTimes(1);
|
||||
expect(internalLogger.info).toHaveBeenCalledWith('message', {
|
||||
hello: 'world',
|
||||
});
|
||||
});
|
||||
|
||||
test('adds global meta even if no user-provided meta exists', () => {
|
||||
const adapter = new LoggerAdapter(internalLogger, { hello: 'there' });
|
||||
|
||||
adapter.info('message');
|
||||
expect(internalLogger.info).toHaveBeenCalledTimes(1);
|
||||
expect(internalLogger.info).toHaveBeenCalledWith('message', {
|
||||
hello: 'there',
|
||||
});
|
||||
});
|
||||
|
||||
test('does nothing if no global meta or user-provided meta has been set', () => {
|
||||
const adapter = new LoggerAdapter(internalLogger);
|
||||
|
||||
adapter.info('message');
|
||||
expect(internalLogger.info).toHaveBeenCalledTimes(1);
|
||||
expect(internalLogger.info).toHaveBeenCalledWith('message', undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,10 +7,11 @@
|
|||
*/
|
||||
|
||||
import { LogRecord, Logger, LogMeta } from '@kbn/logging';
|
||||
import { GlobalContext, mergeGlobalContext } from './global_context';
|
||||
|
||||
/** @internal */
|
||||
export class LoggerAdapter implements Logger {
|
||||
constructor(private logger: Logger) {}
|
||||
constructor(private logger: Logger, private globalContext: GlobalContext = {}) {}
|
||||
|
||||
/**
|
||||
* The current logger can be updated "on the fly", e.g. when the log config
|
||||
|
@ -24,32 +25,44 @@ export class LoggerAdapter implements Logger {
|
|||
this.logger = logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* The current record of {@link GlobalContext} that can be updated on the fly.
|
||||
* Any updates via this method will be applied to all subsequent log entries.
|
||||
*
|
||||
* This is not intended for external use, only internally in Kibana
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public updateGlobalContext(context: GlobalContext) {
|
||||
this.globalContext = context;
|
||||
}
|
||||
|
||||
public trace(message: string, meta?: LogMeta): void {
|
||||
this.logger.trace(message, meta);
|
||||
this.logger.trace(message, mergeGlobalContext(this.globalContext, meta));
|
||||
}
|
||||
|
||||
public debug(message: string, meta?: LogMeta): void {
|
||||
this.logger.debug(message, meta);
|
||||
this.logger.debug(message, mergeGlobalContext(this.globalContext, meta));
|
||||
}
|
||||
|
||||
public info(message: string, meta?: LogMeta): void {
|
||||
this.logger.info(message, meta);
|
||||
this.logger.info(message, mergeGlobalContext(this.globalContext, meta));
|
||||
}
|
||||
|
||||
public warn(errorOrMessage: string | Error, meta?: LogMeta): void {
|
||||
this.logger.warn(errorOrMessage, meta);
|
||||
this.logger.warn(errorOrMessage, mergeGlobalContext(this.globalContext, meta));
|
||||
}
|
||||
|
||||
public error(errorOrMessage: string | Error, meta?: LogMeta): void {
|
||||
this.logger.error(errorOrMessage, meta);
|
||||
this.logger.error(errorOrMessage, mergeGlobalContext(this.globalContext, meta));
|
||||
}
|
||||
|
||||
public fatal(errorOrMessage: string | Error, meta?: LogMeta): void {
|
||||
this.logger.fatal(errorOrMessage, meta);
|
||||
this.logger.fatal(errorOrMessage, mergeGlobalContext(this.globalContext, meta));
|
||||
}
|
||||
|
||||
public log(record: LogRecord) {
|
||||
this.logger.log(record);
|
||||
this.logger.log({ ...record, meta: mergeGlobalContext(this.globalContext, record.meta) });
|
||||
}
|
||||
|
||||
public get(...contextParts: string[]): Logger {
|
||||
|
|
|
@ -22,6 +22,7 @@ const createLoggingSystemMock = () => {
|
|||
get: jest.fn().mockImplementation(() => loggerMock.create()),
|
||||
asLoggerFactory: jest.fn().mockImplementation(() => loggerMock.create()),
|
||||
setContextConfig: jest.fn(),
|
||||
setGlobalContext: jest.fn(),
|
||||
upgrade: jest.fn(),
|
||||
stop: jest.fn(),
|
||||
};
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 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 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { merge, getFlattenedObject } from '@kbn/std';
|
||||
|
||||
export const mockStreamWrite = jest.fn();
|
||||
jest.doMock('fs', () => ({
|
||||
...(jest.requireActual('fs') as any),
|
||||
constants: {},
|
||||
createWriteStream: jest.fn(() => ({ write: mockStreamWrite })),
|
||||
}));
|
||||
|
||||
export const mockGetFlattenedObject = jest.fn().mockImplementation(getFlattenedObject);
|
||||
jest.doMock('@kbn/std', () => ({
|
||||
merge: jest.fn().mockImplementation(merge),
|
||||
getFlattenedObject: mockGetFlattenedObject,
|
||||
}));
|
|
@ -6,12 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
const mockStreamWrite = jest.fn();
|
||||
jest.mock('fs', () => ({
|
||||
...(jest.requireActual('fs') as any),
|
||||
constants: {},
|
||||
createWriteStream: jest.fn(() => ({ write: mockStreamWrite })),
|
||||
}));
|
||||
import { mockStreamWrite, mockGetFlattenedObject } from './logging_system.test.mocks';
|
||||
|
||||
const dynamicProps = { process: { pid: expect.any(Number) } };
|
||||
|
||||
|
@ -34,6 +29,7 @@ afterEach(() => {
|
|||
jest.restoreAllMocks();
|
||||
mockCreateWriteStream.mockClear();
|
||||
mockStreamWrite.mockClear();
|
||||
mockGetFlattenedObject.mockClear();
|
||||
});
|
||||
|
||||
test('uses default memory buffer logger until config is provided', () => {
|
||||
|
@ -521,3 +517,115 @@ test('buffers log records for appenders created during config upgrade', async ()
|
|||
await upgradePromise;
|
||||
expect(JSON.parse(mockConsoleLog.mock.calls[0][0]).message).toBe('message to a new context');
|
||||
});
|
||||
|
||||
test('setGlobalContext() applies meta to new and existing loggers', async () => {
|
||||
await system.upgrade(
|
||||
config.schema.validate({
|
||||
appenders: { default: { type: 'console', layout: { type: 'json' } } },
|
||||
root: { level: 'info' },
|
||||
})
|
||||
);
|
||||
|
||||
const existingLogger = system.get('some-existing-context');
|
||||
// @ts-expect-error Custom ECS field
|
||||
system.setGlobalContext({ a: { b: { c: true } } });
|
||||
const newLogger = system.get('some-new-context');
|
||||
|
||||
existingLogger.info('You know, just for your info.');
|
||||
newLogger.info('You know, just for your info.');
|
||||
// @ts-expect-error Custom ECS field
|
||||
existingLogger.warn('You have been warned.', { someMeta: 'goes here' });
|
||||
// @ts-expect-error Custom ECS field
|
||||
newLogger.warn('You have been warned.', { someMeta: 'goes here' });
|
||||
|
||||
expect(mockConsoleLog).toHaveBeenCalledTimes(4);
|
||||
expect(JSON.parse(mockConsoleLog.mock.calls[0][0])).toMatchObject({
|
||||
log: { logger: 'some-existing-context' },
|
||||
message: 'You know, just for your info.',
|
||||
a: { b: { c: true } },
|
||||
});
|
||||
expect(JSON.parse(mockConsoleLog.mock.calls[1][0])).toMatchObject({
|
||||
log: { logger: 'some-new-context' },
|
||||
message: 'You know, just for your info.',
|
||||
a: { b: { c: true } },
|
||||
});
|
||||
expect(JSON.parse(mockConsoleLog.mock.calls[2][0])).toMatchObject({
|
||||
log: { logger: 'some-existing-context' },
|
||||
message: 'You have been warned.',
|
||||
someMeta: 'goes here',
|
||||
a: { b: { c: true } },
|
||||
});
|
||||
expect(JSON.parse(mockConsoleLog.mock.calls[3][0])).toMatchObject({
|
||||
log: { logger: 'some-new-context' },
|
||||
message: 'You have been warned.',
|
||||
someMeta: 'goes here',
|
||||
a: { b: { c: true } },
|
||||
});
|
||||
});
|
||||
|
||||
test('new global context always overwrites existing context', async () => {
|
||||
await system.upgrade(
|
||||
config.schema.validate({
|
||||
appenders: { default: { type: 'console', layout: { type: 'json' } } },
|
||||
root: { level: 'info' },
|
||||
})
|
||||
);
|
||||
|
||||
const logger = system.get('some-context');
|
||||
|
||||
// @ts-expect-error Custom ECS field
|
||||
system.setGlobalContext({ a: { b: { c: true } }, d: false });
|
||||
logger.info('You know, just for your info.');
|
||||
|
||||
// @ts-expect-error Custom ECS field
|
||||
system.setGlobalContext({ a: false, d: true });
|
||||
logger.info('You know, just for your info, again.');
|
||||
|
||||
expect(mockConsoleLog).toHaveBeenCalledTimes(2);
|
||||
expect(JSON.parse(mockConsoleLog.mock.calls[0][0])).toMatchObject({
|
||||
log: { logger: 'some-context' },
|
||||
message: 'You know, just for your info.',
|
||||
a: { b: { c: true } },
|
||||
d: false,
|
||||
});
|
||||
expect(JSON.parse(mockConsoleLog.mock.calls[1][0])).toMatchObject({
|
||||
log: { logger: 'some-context' },
|
||||
message: 'You know, just for your info, again.',
|
||||
a: false,
|
||||
d: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('flattens global context objects before passing to LoggerAdapter', async () => {
|
||||
await system.upgrade(
|
||||
config.schema.validate({
|
||||
appenders: { default: { type: 'console', layout: { type: 'json' } } },
|
||||
root: { level: 'info' },
|
||||
})
|
||||
);
|
||||
|
||||
// @ts-expect-error Custom ECS field
|
||||
system.setGlobalContext({ a: { b: { c: true } }, d: false });
|
||||
|
||||
const logger = system.get('some-context');
|
||||
|
||||
// @ts-expect-error Custom ECS field
|
||||
system.setGlobalContext({ d: true, e: false });
|
||||
|
||||
logger.info('You know, just for your info.');
|
||||
|
||||
expect(mockGetFlattenedObject).toHaveBeenCalledTimes(3);
|
||||
expect(mockGetFlattenedObject.mock.calls[0][0]).toEqual({
|
||||
a: { b: { c: true } },
|
||||
d: false,
|
||||
});
|
||||
expect(mockGetFlattenedObject.mock.calls[1][0]).toEqual({
|
||||
a: { b: { c: true } },
|
||||
d: false,
|
||||
});
|
||||
expect(mockGetFlattenedObject.mock.calls[2][0]).toEqual({
|
||||
a: { b: { c: true } },
|
||||
d: true,
|
||||
e: false,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { DisposableAppender, LogLevel, Logger, LoggerFactory } from '@kbn/logging';
|
||||
import { getFlattenedObject, merge } from '@kbn/std';
|
||||
import { DisposableAppender, LogLevel, Logger, LoggerFactory, LogMeta } from '@kbn/logging';
|
||||
import type { LoggerConfigType, LoggerContextConfigInput } from '@kbn/core-logging-server';
|
||||
import { Appenders } from './appenders/appenders';
|
||||
import { BufferAppender } from './appenders/buffer/buffer_appender';
|
||||
|
@ -25,6 +26,7 @@ export interface ILoggingSystem extends LoggerFactory {
|
|||
asLoggerFactory(): LoggerFactory;
|
||||
upgrade(rawConfig?: LoggingConfigType): Promise<void>;
|
||||
setContextConfig(baseContextParts: string[], rawConfig: LoggerContextConfigInput): Promise<void>;
|
||||
setGlobalContext(meta: Partial<LogMeta>): void;
|
||||
stop(): Promise<void>;
|
||||
}
|
||||
|
||||
|
@ -41,13 +43,20 @@ export class LoggingSystem implements ILoggingSystem {
|
|||
private readonly bufferAppender = new BufferAppender();
|
||||
private readonly loggers: Map<string, LoggerAdapter> = new Map();
|
||||
private readonly contextConfigs = new Map<string, LoggerContextConfigType>();
|
||||
private globalContext: Partial<LogMeta> = {};
|
||||
|
||||
constructor() {}
|
||||
|
||||
public get(...contextParts: string[]): Logger {
|
||||
const context = LoggingConfig.getLoggerContext(contextParts);
|
||||
if (!this.loggers.has(context)) {
|
||||
this.loggers.set(context, new LoggerAdapter(this.createLogger(context, this.computedConfig)));
|
||||
this.loggers.set(
|
||||
context,
|
||||
new LoggerAdapter(
|
||||
this.createLogger(context, this.computedConfig),
|
||||
getFlattenedObject(this.globalContext)
|
||||
)
|
||||
);
|
||||
}
|
||||
return this.loggers.get(context)!;
|
||||
}
|
||||
|
@ -110,6 +119,23 @@ export class LoggingSystem implements ILoggingSystem {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A mechanism for specifying some "global" {@link LogMeta} that we want
|
||||
* to inject into all log entries.
|
||||
*
|
||||
* @remarks
|
||||
* The provided context will be merged with the meta of each individual log
|
||||
* entry. In the case of conflicting keys, the global context will always be
|
||||
* overridden by the log entry.
|
||||
*/
|
||||
public setGlobalContext(meta: Partial<LogMeta>) {
|
||||
this.globalContext = merge(this.globalContext, meta);
|
||||
const flattenedContext = getFlattenedObject(this.globalContext);
|
||||
for (const loggerAdapter of this.loggers.values()) {
|
||||
loggerAdapter.updateGlobalContext(flattenedContext);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes all loggers (closes log files, clears buffers etc.). Service is not usable after
|
||||
* calling of this method until new config is provided via `upgrade` method.
|
||||
|
|
|
@ -23,6 +23,7 @@ const createLoggingSystemMock = () => {
|
|||
get: jest.fn(),
|
||||
asLoggerFactory: jest.fn(),
|
||||
setContextConfig: jest.fn(),
|
||||
setGlobalContext: jest.fn(),
|
||||
upgrade: jest.fn(),
|
||||
stop: jest.fn(),
|
||||
};
|
||||
|
|
|
@ -39,6 +39,7 @@ TYPES_DEPS = [
|
|||
"//packages/kbn-config-schema:npm_module_types",
|
||||
"//packages/kbn-logging:npm_module_types",
|
||||
"//packages/core/base/core-base-server-internal:npm_module_types",
|
||||
"//packages/core/logging/core-logging-server-internal:npm_module_types",
|
||||
"//packages/core/node/core-node-server:npm_module_types",
|
||||
]
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ describe('NodeService', () => {
|
|||
coreContext = mockCoreContext.create({ logger, configService });
|
||||
|
||||
service = new NodeService(coreContext);
|
||||
const { roles } = await service.preboot();
|
||||
const { roles } = await service.preboot({ loggingSystem: logger });
|
||||
|
||||
expect(roles.backgroundTasks).toBe(true);
|
||||
expect(roles.ui).toBe(true);
|
||||
|
@ -58,7 +58,7 @@ describe('NodeService', () => {
|
|||
coreContext = mockCoreContext.create({ logger, configService });
|
||||
|
||||
service = new NodeService(coreContext);
|
||||
const { roles } = await service.preboot();
|
||||
const { roles } = await service.preboot({ loggingSystem: logger });
|
||||
|
||||
expect(roles.backgroundTasks).toBe(true);
|
||||
expect(roles.ui).toBe(false);
|
||||
|
@ -69,7 +69,7 @@ describe('NodeService', () => {
|
|||
coreContext = mockCoreContext.create({ logger, configService });
|
||||
|
||||
service = new NodeService(coreContext);
|
||||
const { roles } = await service.preboot();
|
||||
const { roles } = await service.preboot({ loggingSystem: logger });
|
||||
|
||||
expect(roles.backgroundTasks).toBe(false);
|
||||
expect(roles.ui).toBe(true);
|
||||
|
@ -80,7 +80,7 @@ describe('NodeService', () => {
|
|||
coreContext = mockCoreContext.create({ logger, configService });
|
||||
|
||||
service = new NodeService(coreContext);
|
||||
const { roles } = await service.preboot();
|
||||
const { roles } = await service.preboot({ loggingSystem: logger });
|
||||
|
||||
expect(roles.backgroundTasks).toBe(true);
|
||||
expect(roles.ui).toBe(true);
|
||||
|
@ -94,7 +94,7 @@ describe('NodeService', () => {
|
|||
coreContext = mockCoreContext.create({ logger, configService });
|
||||
|
||||
service = new NodeService(coreContext);
|
||||
await service.preboot();
|
||||
await service.preboot({ loggingSystem: logger });
|
||||
|
||||
expect(logger.get).toHaveBeenCalledTimes(1);
|
||||
expect(logger.get).toHaveBeenCalledWith('node');
|
||||
|
@ -103,5 +103,18 @@ describe('NodeService', () => {
|
|||
`"Kibana process configured with roles: [background_tasks, ui]"`
|
||||
);
|
||||
});
|
||||
|
||||
it('sets the node roles in the global context', async () => {
|
||||
configService = getMockedConfigService({ roles: ['*'] });
|
||||
coreContext = mockCoreContext.create({ logger, configService });
|
||||
|
||||
service = new NodeService(coreContext);
|
||||
await service.preboot({ loggingSystem: logger });
|
||||
|
||||
expect(logger.setGlobalContext).toHaveBeenCalledTimes(1);
|
||||
expect(logger.setGlobalContext).toHaveBeenCalledWith({
|
||||
service: { node: { roles: ['background_tasks', 'ui'] } },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,6 +10,7 @@ import { firstValueFrom } from 'rxjs';
|
|||
import { camelCase } from 'lodash';
|
||||
import type { IConfigService } from '@kbn/config';
|
||||
import type { CoreContext } from '@kbn/core-base-server-internal';
|
||||
import type { ILoggingSystem } from '@kbn/core-logging-server-internal';
|
||||
import type { NodeRoles } from '@kbn/core-node-server';
|
||||
import type { Logger } from '@kbn/logging';
|
||||
import {
|
||||
|
@ -32,6 +33,10 @@ export interface InternalNodeServicePreboot {
|
|||
roles: NodeRoles;
|
||||
}
|
||||
|
||||
interface PrebootDeps {
|
||||
loggingSystem: ILoggingSystem;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class NodeService {
|
||||
private readonly configService: IConfigService;
|
||||
|
@ -42,18 +47,15 @@ export class NodeService {
|
|||
this.log = core.logger.get('node');
|
||||
}
|
||||
|
||||
public async preboot(): Promise<InternalNodeServicePreboot> {
|
||||
const nodeRoles = await this.getNodeRoles();
|
||||
this.log.info(`Kibana process configured with roles: [${nodeRoles.join(', ')}]`, {
|
||||
service: {
|
||||
// @ts-expect-error Field not available in ECS until 8.4
|
||||
node: { roles: nodeRoles },
|
||||
},
|
||||
});
|
||||
public async preboot({ loggingSystem }: PrebootDeps): Promise<InternalNodeServicePreboot> {
|
||||
const roles = await this.getNodeRoles();
|
||||
// @ts-expect-error Custom ECS field
|
||||
loggingSystem.setGlobalContext({ service: { node: { roles } } });
|
||||
this.log.info(`Kibana process configured with roles: [${roles.join(', ')}]`);
|
||||
|
||||
return {
|
||||
roles: NODE_ACCEPTED_ROLES.reduce((acc, curr) => {
|
||||
return { ...acc, [camelCase(curr)]: nodeRoles.includes(curr) };
|
||||
return { ...acc, [camelCase(curr)]: roles.includes(curr) };
|
||||
}, {} as NodeRoles),
|
||||
};
|
||||
}
|
||||
|
|
70
src/core/server/node/integration_tests/logging.test.ts
Normal file
70
src/core/server/node/integration_tests/logging.test.ts
Normal file
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 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 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import * as kbnTestServer from '../../../test_helpers/kbn_server';
|
||||
|
||||
function createRootWithRoles(roles: string[]) {
|
||||
return kbnTestServer.createRoot({
|
||||
node: {
|
||||
roles,
|
||||
},
|
||||
logging: {
|
||||
appenders: {
|
||||
'test-console': {
|
||||
type: 'console',
|
||||
layout: {
|
||||
type: 'json',
|
||||
},
|
||||
},
|
||||
},
|
||||
root: {
|
||||
appenders: ['test-console'],
|
||||
level: 'info',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe('node service global context', () => {
|
||||
const validRoles = [['ui', 'background_tasks'], ['ui'], ['background_tasks']];
|
||||
|
||||
validRoles.forEach((roles) => {
|
||||
describe(`with node.roles: ${roles}`, () => {
|
||||
let root: ReturnType<typeof createRootWithRoles>;
|
||||
let mockConsoleLog: jest.SpyInstance;
|
||||
|
||||
beforeAll(async () => {
|
||||
mockConsoleLog = jest.spyOn(global.console, 'log');
|
||||
root = createRootWithRoles(roles);
|
||||
|
||||
await root.preboot();
|
||||
await root.setup();
|
||||
}, 30000);
|
||||
|
||||
beforeEach(() => {
|
||||
mockConsoleLog.mockClear();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
mockConsoleLog.mockRestore();
|
||||
await root.shutdown();
|
||||
});
|
||||
|
||||
it('logs the correct roles in service.node.roles', () => {
|
||||
const logger = root.logger.get('foo.bar');
|
||||
|
||||
logger.info('test info');
|
||||
|
||||
expect(mockConsoleLog).toHaveBeenCalledTimes(1);
|
||||
expect(JSON.parse(mockConsoleLog.mock.calls[0][0])).toEqual(
|
||||
expect.objectContaining({ service: { node: { roles } } })
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -167,7 +167,7 @@ export class Server {
|
|||
const analyticsPreboot = this.analytics.preboot();
|
||||
|
||||
const environmentPreboot = await this.environment.preboot({ analytics: analyticsPreboot });
|
||||
const nodePreboot = await this.node.preboot();
|
||||
const nodePreboot = await this.node.preboot({ loggingSystem: this.loggingSystem });
|
||||
|
||||
// Discover any plugins before continuing. This allows other systems to utilize the plugin dependency graph.
|
||||
this.discoveredPlugins = await this.plugins.discover({
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue