[logging] Add mechanism for setting global meta & set service.node.roles for all logs. (#136243)

This commit is contained in:
Luke Elmers 2022-07-14 14:03:59 -06:00 committed by GitHub
parent 8a5bf42bc0
commit 71d375848e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 645 additions and 101 deletions

View file

@ -31,6 +31,7 @@ RUNTIME_DEPS = [
"@npm//elastic-apm-node",
"//packages/elastic-safer-lodash-set",
"//packages/kbn-config-schema",
"//packages/kbn-std",
]
TYPES_DEPS = [

View file

@ -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';

View file

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

View file

@ -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;
}

View file

@ -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>;

View file

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

View file

@ -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 {

View file

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

View file

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

View file

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

View file

@ -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.

View file

@ -23,6 +23,7 @@ const createLoggingSystemMock = () => {
get: jest.fn(),
asLoggerFactory: jest.fn(),
setContextConfig: jest.fn(),
setGlobalContext: jest.fn(),
upgrade: jest.fn(),
stop: jest.fn(),
};

View file

@ -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",
]

View file

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

View file

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

View 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 } } })
);
});
});
});
});

View file

@ -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({