+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
{},
closeNav: () => {},
navigateToApp: () => Promise.resolve(),
+ customNavLink$: new BehaviorSubject(undefined),
};
}
@@ -120,12 +121,14 @@ describe('CollapsibleNav', () => {
mockRecentNavLink({ label: 'recent 1' }),
mockRecentNavLink({ label: 'recent 2' }),
];
+ const customNavLink = mockLink({ title: 'Custom link' });
const component = mount(
);
expect(component).toMatchSnapshot();
diff --git a/src/core/public/chrome/ui/header/collapsible_nav.tsx b/src/core/public/chrome/ui/header/collapsible_nav.tsx
index 9494e22920de8..07541b1adff16 100644
--- a/src/core/public/chrome/ui/header/collapsible_nav.tsx
+++ b/src/core/public/chrome/ui/header/collapsible_nav.tsx
@@ -30,7 +30,7 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { groupBy, sortBy } from 'lodash';
-import React, { useRef } from 'react';
+import React, { Fragment, useRef } from 'react';
import { useObservable } from 'react-use';
import * as Rx from 'rxjs';
import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem } from '../..';
@@ -88,6 +88,7 @@ interface Props {
onIsLockedUpdate: OnIsLockedUpdate;
closeNav: () => void;
navigateToApp: InternalApplicationStart['navigateToApp'];
+ customNavLink$: Rx.Observable;
}
export function CollapsibleNav({
@@ -105,6 +106,7 @@ export function CollapsibleNav({
}: Props) {
const navLinks = useObservable(observables.navLinks$, []).filter((link) => !link.hidden);
const recentlyAccessed = useObservable(observables.recentlyAccessed$, []);
+ const customNavLink = useObservable(observables.customNavLink$, undefined);
const appId = useObservable(observables.appId$, '');
const lockRef = useRef(null);
const groupedNavLinks = groupBy(navLinks, (link) => link?.category?.id);
@@ -134,6 +136,38 @@ export function CollapsibleNav({
isDocked={isLocked}
onClose={closeNav}
>
+ {customNavLink && (
+
+
+
+
+
+
+
+
+
+ )}
+
{/* Pinned items */}
{
const navLinks$ = new BehaviorSubject([
{ id: 'kibana', title: 'kibana', baseUrl: '', legacy: false },
]);
+ const customNavLink$ = new BehaviorSubject({
+ id: 'cloud-deployment-link',
+ title: 'Manage cloud deployment',
+ baseUrl: '',
+ legacy: false,
+ });
const recentlyAccessed$ = new BehaviorSubject([
{ link: '', label: 'dashboard', id: 'dashboard' },
]);
@@ -87,6 +94,7 @@ describe('Header', () => {
recentlyAccessed$={recentlyAccessed$}
isLocked$={isLocked$}
navType$={navType$}
+ customNavLink$={customNavLink$}
/>
);
expect(component).toMatchSnapshot();
diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx
index d24b342e0386b..3da3caaaa4a4f 100644
--- a/src/core/public/chrome/ui/header/header.tsx
+++ b/src/core/public/chrome/ui/header/header.tsx
@@ -58,6 +58,7 @@ export interface HeaderProps {
appTitle$: Observable;
badge$: Observable;
breadcrumbs$: Observable;
+ customNavLink$: Observable;
homeHref: string;
isVisible$: Observable;
kibanaDocLink: string;
@@ -203,6 +204,7 @@ export function Header({
toggleCollapsibleNavRef.current.focus();
}
}}
+ customNavLink$={observables.customNavLink$}
/>
) : (
// TODO #64541
diff --git a/src/core/public/chrome/ui/header/nav_link.tsx b/src/core/public/chrome/ui/header/nav_link.tsx
index 969b6728e0263..6b5cecd138376 100644
--- a/src/core/public/chrome/ui/header/nav_link.tsx
+++ b/src/core/public/chrome/ui/header/nav_link.tsx
@@ -35,11 +35,12 @@ function LinkIcon({ url }: { url: string }) {
interface Props {
link: ChromeNavLink;
legacyMode: boolean;
- appId: string | undefined;
+ appId?: string;
basePath?: HttpStart['basePath'];
dataTestSubj: string;
onClick?: Function;
navigateToApp: CoreStart['application']['navigateToApp'];
+ externalLink?: boolean;
}
// TODO #64541
@@ -54,6 +55,7 @@ export function createEuiListItem({
onClick = () => {},
navigateToApp,
dataTestSubj,
+ externalLink = false,
}: Props) {
const { legacy, active, id, title, disabled, euiIconType, icon, tooltip } = link;
let { href } = link;
@@ -69,6 +71,7 @@ export function createEuiListItem({
onClick(event: React.MouseEvent) {
onClick();
if (
+ !externalLink && // ignore external links
!legacyMode && // ignore when in legacy mode
!legacy && // ignore links to legacy apps
!event.defaultPrevented && // onClick prevented default
diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts
index d6172b77d3ca5..00fabc2b6f2f1 100644
--- a/src/core/public/core_system.ts
+++ b/src/core/public/core_system.ts
@@ -163,7 +163,7 @@ export class CoreSystem {
i18n: this.i18n.getContext(),
});
await this.integrations.setup();
- const docLinks = this.docLinks.setup({ injectedMetadata });
+ this.docLinks.setup();
const http = this.http.setup({ injectedMetadata, fatalErrors: this.fatalErrorsSetup });
const uiSettings = this.uiSettings.setup({ http, injectedMetadata });
const notifications = this.notifications.setup({ uiSettings });
@@ -185,7 +185,6 @@ export class CoreSystem {
const core: InternalCoreSetup = {
application,
context,
- docLinks,
fatalErrors: this.fatalErrorsSetup,
http,
injectedMetadata,
@@ -217,7 +216,7 @@ export class CoreSystem {
try {
const injectedMetadata = await this.injectedMetadata.start();
const uiSettings = await this.uiSettings.start();
- const docLinks = this.docLinks.start();
+ const docLinks = this.docLinks.start({ injectedMetadata });
const http = await this.http.start();
const savedObjects = await this.savedObjects.start({ http });
const i18n = await this.i18n.start();
diff --git a/src/core/public/doc_links/doc_links_service.mock.ts b/src/core/public/doc_links/doc_links_service.mock.ts
index 9edcf2e3c7990..105c13f96cef6 100644
--- a/src/core/public/doc_links/doc_links_service.mock.ts
+++ b/src/core/public/doc_links/doc_links_service.mock.ts
@@ -18,25 +18,23 @@
*/
import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock';
-import { DocLinksService, DocLinksSetup, DocLinksStart } from './doc_links_service';
+import { DocLinksService, DocLinksStart } from './doc_links_service';
-const createSetupContractMock = (): DocLinksSetup => {
+const createStartContractMock = (): DocLinksStart => {
// This service is so simple that we actually use the real implementation
const injectedMetadata = injectedMetadataServiceMock.createStartContract();
injectedMetadata.getKibanaBranch.mockReturnValue('mocked-test-branch');
- return new DocLinksService().setup({ injectedMetadata });
+ return new DocLinksService().start({ injectedMetadata });
};
-const createStartContractMock: () => DocLinksStart = createSetupContractMock;
-
type DocLinksServiceContract = PublicMethodsOf;
const createMock = (): jest.Mocked => ({
- setup: jest.fn().mockReturnValue(createSetupContractMock()),
+ setup: jest.fn().mockReturnValue(undefined),
start: jest.fn().mockReturnValue(createStartContractMock()),
});
export const docLinksServiceMock = {
create: createMock,
- createSetupContract: createSetupContractMock,
+ createSetupContract: () => jest.fn(),
createStartContract: createStartContractMock,
};
diff --git a/src/core/public/doc_links/doc_links_service.test.ts b/src/core/public/doc_links/doc_links_service.test.ts
index 4c5d6bcde8b77..c430ae7655040 100644
--- a/src/core/public/doc_links/doc_links_service.test.ts
+++ b/src/core/public/doc_links/doc_links_service.test.ts
@@ -20,33 +20,15 @@
import { DocLinksService } from './doc_links_service';
import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock';
-describe('DocLinksService#setup()', () => {
+describe('DocLinksService#start()', () => {
it('templates the doc links with the branch information from injectedMetadata', () => {
const injectedMetadata = injectedMetadataServiceMock.createStartContract();
injectedMetadata.getKibanaBranch.mockReturnValue('test-branch');
const service = new DocLinksService();
- const setup = service.setup({ injectedMetadata });
- expect(setup.DOC_LINK_VERSION).toEqual('test-branch');
- expect(setup.links.kibana).toEqual(
+ const api = service.start({ injectedMetadata });
+ expect(api.DOC_LINK_VERSION).toEqual('test-branch');
+ expect(api.links.kibana).toEqual(
'https://www.elastic.co/guide/en/kibana/test-branch/index.html'
);
});
});
-
-describe('DocLinksService#start()', () => {
- it('returns the same data as setup', () => {
- const injectedMetadata = injectedMetadataServiceMock.createStartContract();
- injectedMetadata.getKibanaBranch.mockReturnValue('test-branch');
- const service = new DocLinksService();
- const setup = service.setup({ injectedMetadata });
- const start = service.start();
- expect(setup).toEqual(start);
- });
-
- it('must be called after setup', () => {
- const service = new DocLinksService();
- expect(() => {
- service.start();
- }).toThrowErrorMatchingInlineSnapshot(`"DocLinksService#setup() must be called first!"`);
- });
-});
diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts
index f2bc90a5b08d4..0662586797164 100644
--- a/src/core/public/doc_links/doc_links_service.ts
+++ b/src/core/public/doc_links/doc_links_service.ts
@@ -20,20 +20,19 @@
import { InjectedMetadataSetup } from '../injected_metadata';
import { deepFreeze } from '../../utils';
-interface SetupDeps {
+interface StartDeps {
injectedMetadata: InjectedMetadataSetup;
}
/** @internal */
export class DocLinksService {
- private service?: DocLinksSetup;
-
- public setup({ injectedMetadata }: SetupDeps): DocLinksSetup {
+ public setup() {}
+ public start({ injectedMetadata }: StartDeps): DocLinksStart {
const DOC_LINK_VERSION = injectedMetadata.getKibanaBranch();
const ELASTIC_WEBSITE_URL = 'https://www.elastic.co/';
const ELASTICSEARCH_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`;
- this.service = deepFreeze({
+ return deepFreeze({
DOC_LINK_VERSION,
ELASTIC_WEBSITE_URL,
links: {
@@ -129,21 +128,11 @@ export class DocLinksService {
},
},
});
-
- return this.service;
- }
-
- public start(): DocLinksStart {
- if (!this.service) {
- throw new Error(`DocLinksService#setup() must be called first!`);
- }
-
- return this.service;
}
}
/** @public */
-export interface DocLinksSetup {
+export interface DocLinksStart {
readonly DOC_LINK_VERSION: string;
readonly ELASTIC_WEBSITE_URL: string;
readonly links: {
@@ -236,6 +225,3 @@ export interface DocLinksSetup {
readonly management: Record;
};
}
-
-/** @public */
-export type DocLinksStart = DocLinksSetup;
diff --git a/src/core/public/doc_links/index.ts b/src/core/public/doc_links/index.ts
index fbfa9db5635dd..fe49d4a7c6a58 100644
--- a/src/core/public/doc_links/index.ts
+++ b/src/core/public/doc_links/index.ts
@@ -17,4 +17,4 @@
* under the License.
*/
-export { DocLinksService, DocLinksSetup, DocLinksStart } from './doc_links_service';
+export { DocLinksService, DocLinksStart } from './doc_links_service';
diff --git a/src/core/public/index.ts b/src/core/public/index.ts
index 99b75f85340f3..41af0f1b8395f 100644
--- a/src/core/public/index.ts
+++ b/src/core/public/index.ts
@@ -67,7 +67,7 @@ import { OverlayStart } from './overlays';
import { Plugin, PluginInitializer, PluginInitializerContext, PluginOpaqueId } from './plugins';
import { UiSettingsState, IUiSettingsClient } from './ui_settings';
import { ApplicationSetup, Capabilities, ApplicationStart } from './application';
-import { DocLinksSetup, DocLinksStart } from './doc_links';
+import { DocLinksStart } from './doc_links';
import { SavedObjectsStart } from './saved_objects';
export { PackageInfo, EnvironmentMode } from '../server/types';
import {
@@ -216,8 +216,6 @@ export interface CoreSetup {
mockSetupDeps = {
application: applicationServiceMock.createInternalSetupContract(),
context: contextServiceMock.createSetupContract(),
- docLinks: docLinksServiceMock.createSetupContract(),
fatalErrors: fatalErrorsServiceMock.createSetupContract(),
http: httpServiceMock.createSetupContract(),
injectedMetadata: pick(injectedMetadataServiceMock.createStartContract(), 'getInjectedVar'),
diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md
index 7970d9f3f86bb..d10e351f4d13e 100644
--- a/src/core/public/public.api.md
+++ b/src/core/public/public.api.md
@@ -154,6 +154,7 @@ export function __kbnBootstrap__(): void;
export interface App extends AppBase {
appRoute?: string;
chromeless?: boolean;
+ exactRoute?: boolean;
mount: AppMount | AppMountDeprecated;
}
@@ -466,6 +467,7 @@ export interface ChromeStart {
getBadge$(): Observable;
getBrand$(): Observable;
getBreadcrumbs$(): Observable;
+ getCustomNavLink$(): Observable | undefined>;
getHelpExtension$(): Observable;
getIsNavDrawerLocked$(): Observable;
getIsVisible$(): Observable;
@@ -478,6 +480,7 @@ export interface ChromeStart {
setBadge(badge?: ChromeBadge): void;
setBrand(brand: ChromeBrand): void;
setBreadcrumbs(newBreadcrumbs: ChromeBreadcrumb[]): void;
+ setCustomNavLink(newCustomNavLink?: Partial): void;
setHelpExtension(helpExtension?: ChromeHelpExtension): void;
setHelpSupportUrl(url: string): void;
setIsVisible(isVisible: boolean): void;
@@ -508,8 +511,6 @@ export interface CoreSetup;
@@ -600,7 +601,7 @@ export const DEFAULT_APP_CATEGORIES: Readonly<{
}>;
// @public (undocumented)
-export interface DocLinksSetup {
+export interface DocLinksStart {
// (undocumented)
readonly DOC_LINK_VERSION: string;
// (undocumented)
@@ -697,9 +698,6 @@ export interface DocLinksSetup {
};
}
-// @public (undocumented)
-export type DocLinksStart = DocLinksSetup;
-
// @public (undocumented)
export interface EnvironmentMode {
// (undocumented)
@@ -1594,6 +1592,6 @@ export interface UserProvidedValues {
// Warnings were encountered during analysis:
//
-// src/core/public/core_system.ts:216:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts
+// src/core/public/core_system.ts:215:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts
```
diff --git a/src/core/server/capabilities/integration_tests/capabilities_service.test.ts b/src/core/server/capabilities/integration_tests/capabilities_service.test.ts
index 6be9846f5a86a..b4d620965b047 100644
--- a/src/core/server/capabilities/integration_tests/capabilities_service.test.ts
+++ b/src/core/server/capabilities/integration_tests/capabilities_service.test.ts
@@ -20,7 +20,7 @@
import supertest from 'supertest';
import { HttpService, InternalHttpServiceSetup } from '../../http';
import { contextServiceMock } from '../../context/context_service.mock';
-import { loggingServiceMock } from '../../logging/logging_service.mock';
+import { loggingSystemMock } from '../../logging/logging_system.mock';
import { Env } from '../../config';
import { getEnvOptions } from '../../config/__mocks__/env';
import { CapabilitiesService, CapabilitiesSetup } from '..';
@@ -44,7 +44,7 @@ describe('CapabilitiesService', () => {
service = new CapabilitiesService({
coreId,
env,
- logger: loggingServiceMock.create(),
+ logger: loggingSystemMock.create(),
configService: {} as any,
});
serviceSetup = await service.setup({ http: httpSetup });
diff --git a/src/core/server/config/config_service.test.ts b/src/core/server/config/config_service.test.ts
index 5f28fca1371b0..236cf6579d7c8 100644
--- a/src/core/server/config/config_service.test.ts
+++ b/src/core/server/config/config_service.test.ts
@@ -28,12 +28,12 @@ import { rawConfigServiceMock } from './raw_config_service.mock';
import { schema } from '@kbn/config-schema';
import { ConfigService, Env } from '.';
-import { loggingServiceMock } from '../logging/logging_service.mock';
+import { loggingSystemMock } from '../logging/logging_system.mock';
import { getEnvOptions } from './__mocks__/env';
const emptyArgv = getEnvOptions();
const defaultEnv = new Env('/kibana', emptyArgv);
-const logger = loggingServiceMock.create();
+const logger = loggingSystemMock.create();
const getRawConfigProvider = (rawConfig: Record) =>
rawConfigServiceMock.create({ rawConfig });
@@ -443,9 +443,9 @@ test('logs deprecation warning during validation', async () => {
return config;
});
- loggingServiceMock.clear(logger);
+ loggingSystemMock.clear(logger);
await configService.validate();
- expect(loggingServiceMock.collect(logger).warn).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).warn).toMatchInlineSnapshot(`
Array [
Array [
"some deprecation message",
diff --git a/src/core/server/config/integration_tests/config_deprecation.test.mocks.ts b/src/core/server/config/integration_tests/config_deprecation.test.mocks.ts
index 58b2da926b7c3..1d42c7667a34d 100644
--- a/src/core/server/config/integration_tests/config_deprecation.test.mocks.ts
+++ b/src/core/server/config/integration_tests/config_deprecation.test.mocks.ts
@@ -17,9 +17,9 @@
* under the License.
*/
-import { loggingServiceMock } from '../../logging/logging_service.mock';
-export const mockLoggingService = loggingServiceMock.create();
-mockLoggingService.asLoggerFactory.mockImplementation(() => mockLoggingService);
-jest.doMock('../../logging/logging_service', () => ({
- LoggingService: jest.fn(() => mockLoggingService),
+import { loggingSystemMock } from '../../logging/logging_system.mock';
+export const mockLoggingSystem = loggingSystemMock.create();
+mockLoggingSystem.asLoggerFactory.mockImplementation(() => mockLoggingSystem);
+jest.doMock('../../logging/logging_system', () => ({
+ LoggingSystem: jest.fn(() => mockLoggingSystem),
}));
diff --git a/src/core/server/config/integration_tests/config_deprecation.test.ts b/src/core/server/config/integration_tests/config_deprecation.test.ts
index 3523b074ea5b4..56385f3b171c9 100644
--- a/src/core/server/config/integration_tests/config_deprecation.test.ts
+++ b/src/core/server/config/integration_tests/config_deprecation.test.ts
@@ -17,8 +17,8 @@
* under the License.
*/
-import { mockLoggingService } from './config_deprecation.test.mocks';
-import { loggingServiceMock } from '../../logging/logging_service.mock';
+import { mockLoggingSystem } from './config_deprecation.test.mocks';
+import { loggingSystemMock } from '../../logging/logging_system.mock';
import * as kbnTestServer from '../../../../test_utils/kbn_server';
describe('configuration deprecations', () => {
@@ -35,7 +35,7 @@ describe('configuration deprecations', () => {
await root.setup();
- const logs = loggingServiceMock.collect(mockLoggingService);
+ const logs = loggingSystemMock.collect(mockLoggingSystem);
const warnings = logs.warn.flatMap((i) => i);
expect(warnings).not.toContain(
'"optimize.lazy" is deprecated and has been replaced by "optimize.watch"'
@@ -55,7 +55,7 @@ describe('configuration deprecations', () => {
await root.setup();
- const logs = loggingServiceMock.collect(mockLoggingService);
+ const logs = loggingSystemMock.collect(mockLoggingSystem);
const warnings = logs.warn.flatMap((i) => i);
expect(warnings).toContain(
'"optimize.lazy" is deprecated and has been replaced by "optimize.watch"'
diff --git a/src/core/server/core_context.mock.ts b/src/core/server/core_context.mock.ts
index d287348e19079..f870d30528df4 100644
--- a/src/core/server/core_context.mock.ts
+++ b/src/core/server/core_context.mock.ts
@@ -20,17 +20,17 @@
import { CoreContext } from './core_context';
import { getEnvOptions } from './config/__mocks__/env';
import { Env, IConfigService } from './config';
-import { loggingServiceMock } from './logging/logging_service.mock';
+import { loggingSystemMock } from './logging/logging_system.mock';
import { configServiceMock } from './config/config_service.mock';
-import { ILoggingService } from './logging';
+import { ILoggingSystem } from './logging';
function create({
env = Env.createDefault(getEnvOptions()),
- logger = loggingServiceMock.create(),
+ logger = loggingSystemMock.create(),
configService = configServiceMock.create(),
}: {
env?: Env;
- logger?: jest.Mocked;
+ logger?: jest.Mocked;
configService?: jest.Mocked;
} = {}): DeeplyMockedKeys {
return { coreId: Symbol(), env, logger, configService };
diff --git a/src/core/server/elasticsearch/cluster_client.test.ts b/src/core/server/elasticsearch/cluster_client.test.ts
index db277fa0e0607..820272bdf14b8 100644
--- a/src/core/server/elasticsearch/cluster_client.test.ts
+++ b/src/core/server/elasticsearch/cluster_client.test.ts
@@ -28,11 +28,11 @@ import {
import { errors } from 'elasticsearch';
import { get } from 'lodash';
import { Logger } from '../logging';
-import { loggingServiceMock } from '../logging/logging_service.mock';
+import { loggingSystemMock } from '../logging/logging_system.mock';
import { httpServerMock } from '../http/http_server.mocks';
import { ClusterClient } from './cluster_client';
-const logger = loggingServiceMock.create();
+const logger = loggingSystemMock.create();
afterEach(() => jest.clearAllMocks());
test('#constructor creates client with parsed config', () => {
diff --git a/src/core/server/elasticsearch/elasticsearch_client_config.test.ts b/src/core/server/elasticsearch/elasticsearch_client_config.test.ts
index 20c10459e0e8a..77d1e41c9ad83 100644
--- a/src/core/server/elasticsearch/elasticsearch_client_config.test.ts
+++ b/src/core/server/elasticsearch/elasticsearch_client_config.test.ts
@@ -18,12 +18,12 @@
*/
import { duration } from 'moment';
-import { loggingServiceMock } from '../logging/logging_service.mock';
+import { loggingSystemMock } from '../logging/logging_system.mock';
import {
ElasticsearchClientConfig,
parseElasticsearchClientConfig,
} from './elasticsearch_client_config';
-const logger = loggingServiceMock.create();
+const logger = loggingSystemMock.create();
afterEach(() => jest.clearAllMocks());
test('parses minimally specified config', () => {
@@ -360,7 +360,7 @@ describe('#log', () => {
expect(typeof esLogger.close).toBe('function');
- expect(loggingServiceMock.collect(logger)).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger)).toMatchInlineSnapshot(`
Object {
"debug": Array [],
"error": Array [
@@ -406,7 +406,7 @@ Object {
expect(typeof esLogger.close).toBe('function');
- expect(loggingServiceMock.collect(logger)).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger)).toMatchInlineSnapshot(`
Object {
"debug": Array [
Array [
diff --git a/src/core/server/elasticsearch/elasticsearch_service.test.ts b/src/core/server/elasticsearch/elasticsearch_service.test.ts
index 8bf0df74186a9..0a7068903e15c 100644
--- a/src/core/server/elasticsearch/elasticsearch_service.test.ts
+++ b/src/core/server/elasticsearch/elasticsearch_service.test.ts
@@ -26,7 +26,7 @@ import { Env } from '../config';
import { getEnvOptions } from '../config/__mocks__/env';
import { CoreContext } from '../core_context';
import { configServiceMock } from '../config/config_service.mock';
-import { loggingServiceMock } from '../logging/logging_service.mock';
+import { loggingSystemMock } from '../logging/logging_system.mock';
import { httpServiceMock } from '../http/http_service.mock';
import { ElasticsearchConfig } from './elasticsearch_config';
import { ElasticsearchService } from './elasticsearch_service';
@@ -55,7 +55,7 @@ configService.atPath.mockReturnValue(
let env: Env;
let coreContext: CoreContext;
-const logger = loggingServiceMock.create();
+const logger = loggingSystemMock.create();
beforeEach(() => {
env = Env.createDefault(getEnvOptions());
diff --git a/src/core/server/elasticsearch/retry_call_cluster.test.ts b/src/core/server/elasticsearch/retry_call_cluster.test.ts
index 8be138e6752d2..18ffa95048c4d 100644
--- a/src/core/server/elasticsearch/retry_call_cluster.test.ts
+++ b/src/core/server/elasticsearch/retry_call_cluster.test.ts
@@ -19,7 +19,7 @@
import * as legacyElasticsearch from 'elasticsearch';
import { retryCallCluster, migrationsRetryCallCluster } from './retry_call_cluster';
-import { loggingServiceMock } from '../logging/logging_service.mock';
+import { loggingSystemMock } from '../logging/logging_system.mock';
describe('retryCallCluster', () => {
it('retries ES API calls that rejects with NoConnections', () => {
@@ -69,10 +69,10 @@ describe('migrationsRetryCallCluster', () => {
'Gone',
];
- const mockLogger = loggingServiceMock.create();
+ const mockLogger = loggingSystemMock.create();
beforeEach(() => {
- loggingServiceMock.clear(mockLogger);
+ loggingSystemMock.clear(mockLogger);
});
errors.forEach((errorName) => {
@@ -133,7 +133,7 @@ describe('migrationsRetryCallCluster', () => {
callEsApi.mockResolvedValueOnce('done');
const retried = migrationsRetryCallCluster(callEsApi, mockLogger.get('mock log'), 1);
await retried('endpoint');
- expect(loggingServiceMock.collect(mockLogger).warn).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(mockLogger).warn).toMatchInlineSnapshot(`
Array [
Array [
"Unable to connect to Elasticsearch. Error: No Living connections",
diff --git a/src/core/server/elasticsearch/version_check/ensure_es_version.test.ts b/src/core/server/elasticsearch/version_check/ensure_es_version.test.ts
index a2090ed111ca1..3d1218d4a8e8b 100644
--- a/src/core/server/elasticsearch/version_check/ensure_es_version.test.ts
+++ b/src/core/server/elasticsearch/version_check/ensure_es_version.test.ts
@@ -17,12 +17,12 @@
* under the License.
*/
import { mapNodesVersionCompatibility, pollEsNodesVersion, NodesInfo } from './ensure_es_version';
-import { loggingServiceMock } from '../../logging/logging_service.mock';
+import { loggingSystemMock } from '../../logging/logging_system.mock';
import { take, delay } from 'rxjs/operators';
import { TestScheduler } from 'rxjs/testing';
import { of } from 'rxjs';
-const mockLoggerFactory = loggingServiceMock.create();
+const mockLoggerFactory = loggingSystemMock.create();
const mockLogger = mockLoggerFactory.get('mock logger');
const KIBANA_VERSION = '5.1.0';
diff --git a/src/core/server/http/__snapshots__/http_config.test.ts.snap b/src/core/server/http/__snapshots__/http_config.test.ts.snap
index 07c153a7a8a20..d48ead3cec8e1 100644
--- a/src/core/server/http/__snapshots__/http_config.test.ts.snap
+++ b/src/core/server/http/__snapshots__/http_config.test.ts.snap
@@ -83,6 +83,8 @@ Object {
exports[`throws if basepath appends a slash 1`] = `"[basePath]: must start with a slash, don't end with one"`;
+exports[`throws if basepath is an empty string 1`] = `"[basePath]: must start with a slash, don't end with one"`;
+
exports[`throws if basepath is missing prepended slash 1`] = `"[basePath]: must start with a slash, don't end with one"`;
exports[`throws if basepath is not specified, but rewriteBasePath is set 1`] = `"cannot use [rewriteBasePath] when [basePath] is not specified"`;
diff --git a/src/core/server/http/cookie_session_storage.test.ts b/src/core/server/http/cookie_session_storage.test.ts
index 3afe5e0c4dfc7..1fb2b5693bb61 100644
--- a/src/core/server/http/cookie_session_storage.test.ts
+++ b/src/core/server/http/cookie_session_storage.test.ts
@@ -29,14 +29,14 @@ import { Env } from '../config';
import { getEnvOptions } from '../config/__mocks__/env';
import { configServiceMock } from '../config/config_service.mock';
import { contextServiceMock } from '../context/context_service.mock';
-import { loggingServiceMock } from '../logging/logging_service.mock';
+import { loggingSystemMock } from '../logging/logging_system.mock';
import { httpServerMock } from './http_server.mocks';
import { createCookieSessionStorageFactory } from './cookie_session_storage';
let server: HttpService;
-let logger: ReturnType;
+let logger: ReturnType;
let env: Env;
let coreContext: CoreContext;
const configService = configServiceMock.create();
@@ -67,7 +67,7 @@ configService.atPath.mockReturnValue(
);
beforeEach(() => {
- logger = loggingServiceMock.create();
+ logger = loggingSystemMock.create();
env = Env.createDefault(getEnvOptions());
coreContext = { coreId: Symbol(), env, logger, configService: configService as any };
@@ -324,7 +324,7 @@ describe('Cookie based SessionStorage', () => {
expect(mockServer.auth.test).toBeCalledTimes(1);
expect(mockServer.auth.test).toHaveBeenCalledWith('security-cookie', mockRequest);
- expect(loggingServiceMock.collect(logger).warn).toEqual([
+ expect(loggingSystemMock.collect(logger).warn).toEqual([
['Found 2 auth sessions when we were only expecting 1.'],
]);
});
@@ -381,7 +381,7 @@ describe('Cookie based SessionStorage', () => {
const session = await factory.asScoped(KibanaRequest.from(mockRequest)).get();
expect(session).toBe(null);
- expect(loggingServiceMock.collect(logger).debug).toEqual([['Error: Invalid cookie.']]);
+ expect(loggingSystemMock.collect(logger).debug).toEqual([['Error: Invalid cookie.']]);
});
});
diff --git a/src/core/server/http/http_config.test.ts b/src/core/server/http/http_config.test.ts
index eaf66219d08dc..0698f118be03f 100644
--- a/src/core/server/http/http_config.test.ts
+++ b/src/core/server/http/http_config.test.ts
@@ -78,6 +78,14 @@ test('throws if basepath appends a slash', () => {
expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot();
});
+test('throws if basepath is an empty string', () => {
+ const httpSchema = config.schema;
+ const obj = {
+ basePath: '',
+ };
+ expect(() => httpSchema.validate(obj)).toThrowErrorMatchingSnapshot();
+});
+
test('throws if basepath is not specified, but rewriteBasePath is set', () => {
const httpSchema = config.schema;
const obj = {
diff --git a/src/core/server/http/http_config.ts b/src/core/server/http/http_config.ts
index 289b6539fd762..83a2e712b424f 100644
--- a/src/core/server/http/http_config.ts
+++ b/src/core/server/http/http_config.ts
@@ -23,7 +23,7 @@ import { hostname } from 'os';
import { CspConfigType, CspConfig, ICspConfig } from '../csp';
import { SslConfig, sslSchema } from './ssl_config';
-const validBasePathRegex = /(^$|^\/.*[^\/]$)/;
+const validBasePathRegex = /^\/.*[^\/]$/;
const uuidRegexp = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const match = (regex: RegExp, errorMsg: string) => (str: string) =>
diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts
index 9a5deb9b45562..4520851bb460c 100644
--- a/src/core/server/http/http_server.test.ts
+++ b/src/core/server/http/http_server.test.ts
@@ -31,7 +31,7 @@ import {
RouteValidationResultFactory,
RouteValidationFunction,
} from './router';
-import { loggingServiceMock } from '../logging/logging_service.mock';
+import { loggingSystemMock } from '../logging/logging_system.mock';
import { HttpServer } from './http_server';
import { Readable } from 'stream';
import { RequestHandlerContext } from 'kibana/server';
@@ -48,7 +48,7 @@ let server: HttpServer;
let config: HttpConfig;
let configWithSSL: HttpConfig;
-const loggingService = loggingServiceMock.create();
+const loggingService = loggingSystemMock.create();
const logger = loggingService.get();
const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, {});
@@ -97,7 +97,7 @@ test('log listening address after started', async () => {
await server.start();
expect(server.isListening()).toBe(true);
- expect(loggingServiceMock.collect(loggingService).info).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(loggingService).info).toMatchInlineSnapshot(`
Array [
Array [
"http server running at http://127.0.0.1:10002",
@@ -113,7 +113,7 @@ test('log listening address after started when configured with BasePath and rewr
await server.start();
expect(server.isListening()).toBe(true);
- expect(loggingServiceMock.collect(loggingService).info).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(loggingService).info).toMatchInlineSnapshot(`
Array [
Array [
"http server running at http://127.0.0.1:10002",
@@ -129,7 +129,7 @@ test('log listening address after started when configured with BasePath and rewr
await server.start();
expect(server.isListening()).toBe(true);
- expect(loggingServiceMock.collect(loggingService).info).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(loggingService).info).toMatchInlineSnapshot(`
Array [
Array [
"http server running at http://127.0.0.1:10002/bar",
diff --git a/src/core/server/http/http_service.test.ts b/src/core/server/http/http_service.test.ts
index 8b500caf217dc..3d759b427d9fb 100644
--- a/src/core/server/http/http_service.test.ts
+++ b/src/core/server/http/http_service.test.ts
@@ -25,12 +25,12 @@ import { HttpService } from '.';
import { HttpConfigType, config } from './http_config';
import { httpServerMock } from './http_server.mocks';
import { ConfigService, Env } from '../config';
-import { loggingServiceMock } from '../logging/logging_service.mock';
+import { loggingSystemMock } from '../logging/logging_system.mock';
import { contextServiceMock } from '../context/context_service.mock';
import { getEnvOptions } from '../config/__mocks__/env';
import { config as cspConfig } from '../csp';
-const logger = loggingServiceMock.create();
+const logger = loggingSystemMock.create();
const env = Env.createDefault(getEnvOptions());
const coreId = Symbol();
@@ -159,7 +159,7 @@ test('logs error if already set up', async () => {
await service.setup(setupDeps);
- expect(loggingServiceMock.collect(logger).warn).toMatchSnapshot();
+ expect(loggingSystemMock.collect(logger).warn).toMatchSnapshot();
});
test('stops http server', async () => {
diff --git a/src/core/server/http/http_tools.test.ts b/src/core/server/http/http_tools.test.ts
index 7d5a7277a767a..f09d862f9edac 100644
--- a/src/core/server/http/http_tools.test.ts
+++ b/src/core/server/http/http_tools.test.ts
@@ -34,7 +34,7 @@ import { defaultValidationErrorHandler, HapiValidationError, getServerOptions }
import { HttpServer } from './http_server';
import { HttpConfig, config } from './http_config';
import { Router } from './router';
-import { loggingServiceMock } from '../logging/logging_service.mock';
+import { loggingSystemMock } from '../logging/logging_system.mock';
import { ByteSizeValue } from '@kbn/config-schema';
const emptyOutput = {
@@ -77,7 +77,7 @@ describe('defaultValidationErrorHandler', () => {
});
describe('timeouts', () => {
- const logger = loggingServiceMock.create();
+ const logger = loggingSystemMock.create();
const server = new HttpServer(logger, 'foo');
const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, {});
diff --git a/src/core/server/http/https_redirect_server.test.ts b/src/core/server/http/https_redirect_server.test.ts
index a7d3cbe41aa3d..f35456f01c19b 100644
--- a/src/core/server/http/https_redirect_server.test.ts
+++ b/src/core/server/http/https_redirect_server.test.ts
@@ -27,7 +27,7 @@ import supertest from 'supertest';
import { ByteSizeValue } from '@kbn/config-schema';
import { HttpConfig } from '.';
-import { loggingServiceMock } from '../logging/logging_service.mock';
+import { loggingSystemMock } from '../logging/logging_system.mock';
import { HttpsRedirectServer } from './https_redirect_server';
const chance = new Chance();
@@ -50,7 +50,7 @@ beforeEach(() => {
},
} as HttpConfig;
- server = new HttpsRedirectServer(loggingServiceMock.create().get());
+ server = new HttpsRedirectServer(loggingSystemMock.create().get());
});
afterEach(async () => {
diff --git a/src/core/server/http/integration_tests/lifecycle.test.ts b/src/core/server/http/integration_tests/lifecycle.test.ts
index 73ed4e5de4b04..879cbc689f8e7 100644
--- a/src/core/server/http/integration_tests/lifecycle.test.ts
+++ b/src/core/server/http/integration_tests/lifecycle.test.ts
@@ -24,12 +24,12 @@ import { ensureRawRequest } from '../router';
import { HttpService } from '../http_service';
import { contextServiceMock } from '../../context/context_service.mock';
-import { loggingServiceMock } from '../../logging/logging_service.mock';
+import { loggingSystemMock } from '../../logging/logging_system.mock';
import { createHttpServer } from '../test_utils';
let server: HttpService;
-let logger: ReturnType;
+let logger: ReturnType;
const contextSetup = contextServiceMock.createSetupContract();
@@ -38,7 +38,7 @@ const setupDeps = {
};
beforeEach(() => {
- logger = loggingServiceMock.create();
+ logger = loggingSystemMock.create();
server = createHttpServer({ logger });
});
@@ -167,7 +167,7 @@ describe('OnPreAuth', () => {
const result = await supertest(innerServer.listener).get('/').expect(500);
expect(result.body.message).toBe('An internal server error occurred.');
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: reason],
@@ -188,7 +188,7 @@ describe('OnPreAuth', () => {
const result = await supertest(innerServer.listener).get('/').expect(500);
expect(result.body.message).toBe('An internal server error occurred.');
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: Unexpected result from OnPreAuth. Expected OnPreAuthResult or KibanaResponse, but given: [object Object].],
@@ -301,7 +301,7 @@ describe('OnPostAuth', () => {
const result = await supertest(innerServer.listener).get('/').expect(500);
expect(result.body.message).toBe('An internal server error occurred.');
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: reason],
@@ -321,7 +321,7 @@ describe('OnPostAuth', () => {
const result = await supertest(innerServer.listener).get('/').expect(500);
expect(result.body.message).toBe('An internal server error occurred.');
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: Unexpected result from OnPostAuth. Expected OnPostAuthResult or KibanaResponse, but given: [object Object].],
@@ -506,7 +506,7 @@ describe('Auth', () => {
const result = await supertest(innerServer.listener).get('/').expect(500);
expect(result.body.message).toBe('An internal server error occurred.');
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: reason],
@@ -703,7 +703,7 @@ describe('Auth', () => {
const response = await supertest(innerServer.listener).get('/').expect(200);
expect(response.header['www-authenticate']).toBe('from auth interceptor');
- expect(loggingServiceMock.collect(logger).warn).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).warn).toMatchInlineSnapshot(`
Array [
Array [
"onPreResponseHandler rewrote a response header [www-authenticate].",
@@ -736,7 +736,7 @@ describe('Auth', () => {
const response = await supertest(innerServer.listener).get('/').expect(400);
expect(response.header['www-authenticate']).toBe('from auth interceptor');
- expect(loggingServiceMock.collect(logger).warn).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).warn).toMatchInlineSnapshot(`
Array [
Array [
"onPreResponseHandler rewrote a response header [www-authenticate].",
@@ -798,7 +798,7 @@ describe('Auth', () => {
const result = await supertest(innerServer.listener).get('/').expect(500);
expect(result.body.message).toBe('An internal server error occurred.');
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: reason],
@@ -818,7 +818,7 @@ describe('Auth', () => {
const result = await supertest(innerServer.listener).get('/').expect(500);
expect(result.body.message).toBe('An internal server error occurred.');
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: Unexpected result from OnPostAuth. Expected OnPostAuthResult or KibanaResponse, but given: [object Object].],
@@ -929,7 +929,7 @@ describe('OnPreResponse', () => {
await supertest(innerServer.listener).get('/').expect(200);
- expect(loggingServiceMock.collect(logger).warn).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).warn).toMatchInlineSnapshot(`
Array [
Array [
"onPreResponseHandler rewrote a response header [x-kibana-header].",
@@ -953,7 +953,7 @@ describe('OnPreResponse', () => {
const result = await supertest(innerServer.listener).get('/').expect(500);
expect(result.body.message).toBe('An internal server error occurred.');
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: reason],
@@ -975,7 +975,7 @@ describe('OnPreResponse', () => {
const result = await supertest(innerServer.listener).get('/').expect(500);
expect(result.body.message).toBe('An internal server error occurred.');
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: Unexpected result from OnPreResponse. Expected OnPreResponseResult, but given: [object Object].],
diff --git a/src/core/server/http/integration_tests/request.test.ts b/src/core/server/http/integration_tests/request.test.ts
index d33757273042b..2d018f7f464b5 100644
--- a/src/core/server/http/integration_tests/request.test.ts
+++ b/src/core/server/http/integration_tests/request.test.ts
@@ -21,12 +21,12 @@ import supertest from 'supertest';
import { HttpService } from '../http_service';
import { contextServiceMock } from '../../context/context_service.mock';
-import { loggingServiceMock } from '../../logging/logging_service.mock';
+import { loggingSystemMock } from '../../logging/logging_system.mock';
import { createHttpServer } from '../test_utils';
let server: HttpService;
-let logger: ReturnType;
+let logger: ReturnType;
const contextSetup = contextServiceMock.createSetupContract();
const setupDeps = {
@@ -34,7 +34,7 @@ const setupDeps = {
};
beforeEach(() => {
- logger = loggingServiceMock.create();
+ logger = loggingSystemMock.create();
server = createHttpServer({ logger });
});
diff --git a/src/core/server/http/integration_tests/router.test.ts b/src/core/server/http/integration_tests/router.test.ts
index 8f3799b12eccb..bb36fefa96611 100644
--- a/src/core/server/http/integration_tests/router.test.ts
+++ b/src/core/server/http/integration_tests/router.test.ts
@@ -24,12 +24,12 @@ import { schema } from '@kbn/config-schema';
import { HttpService } from '../http_service';
import { contextServiceMock } from '../../context/context_service.mock';
-import { loggingServiceMock } from '../../logging/logging_service.mock';
+import { loggingSystemMock } from '../../logging/logging_system.mock';
import { createHttpServer } from '../test_utils';
let server: HttpService;
-let logger: ReturnType;
+let logger: ReturnType;
const contextSetup = contextServiceMock.createSetupContract();
const setupDeps = {
@@ -37,7 +37,7 @@ const setupDeps = {
};
beforeEach(() => {
- logger = loggingServiceMock.create();
+ logger = loggingSystemMock.create();
server = createHttpServer({ logger });
});
@@ -347,7 +347,7 @@ describe('Handler', () => {
const result = await supertest(innerServer.listener).get('/').expect(500);
expect(result.body.message).toBe('An internal server error occurred.');
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: unexpected error],
@@ -368,7 +368,7 @@ describe('Handler', () => {
const result = await supertest(innerServer.listener).get('/').expect(500);
expect(result.body.message).toBe('An internal server error occurred.');
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: Unauthorized],
@@ -387,7 +387,7 @@ describe('Handler', () => {
const result = await supertest(innerServer.listener).get('/').expect(500);
expect(result.body.message).toBe('An internal server error occurred.');
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: Unexpected result from Route Handler. Expected KibanaResponse, but given: string.],
@@ -763,7 +763,7 @@ describe('Response factory', () => {
await supertest(innerServer.listener).get('/').expect(500);
// error happens within hapi when route handler already finished execution.
- expect(loggingServiceMock.collect(logger).error).toHaveLength(0);
+ expect(loggingSystemMock.collect(logger).error).toHaveLength(0);
});
it('200 OK with body', async () => {
@@ -855,7 +855,7 @@ describe('Response factory', () => {
const result = await supertest(innerServer.listener).get('/').expect(500);
expect(result.body.message).toBe('An internal server error occurred.');
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: expected 'location' header to be set],
@@ -1261,7 +1261,7 @@ describe('Response factory', () => {
message: 'An internal server error occurred.',
statusCode: 500,
});
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: Unexpected Http status code. Expected from 400 to 599, but given: 200],
@@ -1330,7 +1330,7 @@ describe('Response factory', () => {
await supertest(innerServer.listener).get('/').expect(500);
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: expected 'location' header to be set],
@@ -1445,7 +1445,7 @@ describe('Response factory', () => {
const result = await supertest(innerServer.listener).get('/').expect(500);
expect(result.body.message).toBe('reason');
- expect(loggingServiceMock.collect(logger).error).toHaveLength(0);
+ expect(loggingSystemMock.collect(logger).error).toHaveLength(0);
});
it('throws an error if not valid error is provided', async () => {
@@ -1464,7 +1464,7 @@ describe('Response factory', () => {
const result = await supertest(innerServer.listener).get('/').expect(500);
expect(result.body.message).toBe('An internal server error occurred.');
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: expected error message to be provided],
@@ -1488,7 +1488,7 @@ describe('Response factory', () => {
const result = await supertest(innerServer.listener).get('/').expect(500);
expect(result.body.message).toBe('An internal server error occurred.');
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: expected error message to be provided],
@@ -1511,7 +1511,7 @@ describe('Response factory', () => {
const result = await supertest(innerServer.listener).get('/').expect(500);
expect(result.body.message).toBe('An internal server error occurred.');
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: options.statusCode is expected to be set. given options: undefined],
@@ -1534,7 +1534,7 @@ describe('Response factory', () => {
const result = await supertest(innerServer.listener).get('/').expect(500);
expect(result.body.message).toBe('An internal server error occurred.');
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: Unexpected Http status code. Expected from 100 to 599, but given: 20.],
diff --git a/src/core/server/http/router/router.test.ts b/src/core/server/http/router/router.test.ts
index 9655e2153b863..fa38c7bd6b336 100644
--- a/src/core/server/http/router/router.test.ts
+++ b/src/core/server/http/router/router.test.ts
@@ -18,10 +18,10 @@
*/
import { Router } from './router';
-import { loggingServiceMock } from '../../logging/logging_service.mock';
+import { loggingSystemMock } from '../../logging/logging_system.mock';
import { schema } from '@kbn/config-schema';
-const logger = loggingServiceMock.create().get();
+const logger = loggingSystemMock.create().get();
const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, {});
describe('Router', () => {
diff --git a/src/core/server/http/test_utils.ts b/src/core/server/http/test_utils.ts
index 0e639aa72a825..bda66e1de8168 100644
--- a/src/core/server/http/test_utils.ts
+++ b/src/core/server/http/test_utils.ts
@@ -24,12 +24,12 @@ import { getEnvOptions } from '../config/__mocks__/env';
import { HttpService } from './http_service';
import { CoreContext } from '../core_context';
import { configServiceMock } from '../config/config_service.mock';
-import { loggingServiceMock } from '../logging/logging_service.mock';
+import { loggingSystemMock } from '../logging/logging_system.mock';
const coreId = Symbol('core');
const env = Env.createDefault(getEnvOptions());
-const logger = loggingServiceMock.create();
+const logger = loggingSystemMock.create();
const configService = configServiceMock.create();
configService.atPath.mockReturnValue(
diff --git a/src/core/server/index.ts b/src/core/server/index.ts
index 0da7e5d66cf2a..e0afd5e57f041 100644
--- a/src/core/server/index.ts
+++ b/src/core/server/index.ts
@@ -62,6 +62,12 @@ import { CapabilitiesSetup, CapabilitiesStart } from './capabilities';
import { UuidServiceSetup } from './uuid';
import { MetricsServiceSetup } from './metrics';
import { StatusServiceSetup } from './status';
+import {
+ LoggingServiceSetup,
+ appendersSchema,
+ loggerContextConfigSchema,
+ loggerSchema,
+} from './logging';
export { bootstrap } from './bootstrap';
export { Capabilities, CapabilitiesProvider, CapabilitiesSwitcher } from './capabilities';
@@ -187,7 +193,17 @@ export {
} from './http_resources';
export { IRenderOptions } from './rendering';
-export { Logger, LoggerFactory, LogMeta, LogRecord, LogLevel } from './logging';
+export {
+ Logger,
+ LoggerFactory,
+ LogMeta,
+ LogRecord,
+ LogLevel,
+ LoggingServiceSetup,
+ LoggerContextConfigInput,
+ LoggerConfigType,
+ AppenderConfigType,
+} from './logging';
export {
DiscoveredPlugin,
@@ -385,6 +401,8 @@ export interface CoreSetup = KbnServer as any;
@@ -64,7 +65,7 @@ let setupDeps: LegacyServiceSetupDeps;
let startDeps: LegacyServiceStartDeps;
-const logger = loggingServiceMock.create();
+const logger = loggingSystemMock.create();
let configService: ReturnType;
let uuidSetup: ReturnType;
@@ -100,6 +101,7 @@ beforeEach(() => {
metrics: metricsServiceMock.createInternalSetupContract(),
uuid: uuidSetup,
status: statusServiceMock.createInternalSetupContract(),
+ logging: loggingServiceMock.createInternalSetupContract(),
},
plugins: { 'plugin-id': 'plugin-value' },
uiPlugins: {
@@ -281,7 +283,7 @@ describe('once LegacyService is set up with connection info', () => {
const [mockKbnServer] = MockKbnServer.mock.instances as Array>;
expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled();
- expect(loggingServiceMock.collect(logger).error).toEqual([]);
+ expect(loggingSystemMock.collect(logger).error).toEqual([]);
const configError = new Error('something went wrong');
mockKbnServer.applyLoggingConfiguration.mockImplementation(() => {
@@ -290,7 +292,7 @@ describe('once LegacyService is set up with connection info', () => {
config$.next(new ObjectToConfigAdapter({ logging: { verbose: true } }));
- expect(loggingServiceMock.collect(logger).error).toEqual([[configError]]);
+ expect(loggingSystemMock.collect(logger).error).toEqual([[configError]]);
});
test('logs error if config service fails.', async () => {
@@ -306,13 +308,13 @@ describe('once LegacyService is set up with connection info', () => {
const [mockKbnServer] = MockKbnServer.mock.instances;
expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled();
- expect(loggingServiceMock.collect(logger).error).toEqual([]);
+ expect(loggingSystemMock.collect(logger).error).toEqual([]);
const configError = new Error('something went wrong');
config$.error(configError);
expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled();
- expect(loggingServiceMock.collect(logger).error).toEqual([[configError]]);
+ expect(loggingSystemMock.collect(logger).error).toEqual([[configError]]);
});
});
diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts
index cfc53b10d91f0..be737f6593c02 100644
--- a/src/core/server/legacy/legacy_service.ts
+++ b/src/core/server/legacy/legacy_service.ts
@@ -309,6 +309,9 @@ export class LegacyService implements CoreService {
csp: setupDeps.core.http.csp,
getServerInfo: setupDeps.core.http.getServerInfo,
},
+ logging: {
+ configure: (config$) => setupDeps.core.logging.configure([], config$),
+ },
metrics: {
getOpsMetrics$: setupDeps.core.metrics.getOpsMetrics$,
},
diff --git a/src/core/server/logging/__snapshots__/logging_service.test.ts.snap b/src/core/server/logging/__snapshots__/logging_system.test.ts.snap
similarity index 100%
rename from src/core/server/logging/__snapshots__/logging_service.test.ts.snap
rename to src/core/server/logging/__snapshots__/logging_system.test.ts.snap
diff --git a/src/core/server/logging/appenders/appenders.ts b/src/core/server/logging/appenders/appenders.ts
index 3aa86495e4d82..3b90a10a1a76c 100644
--- a/src/core/server/logging/appenders/appenders.ts
+++ b/src/core/server/logging/appenders/appenders.ts
@@ -26,13 +26,19 @@ import { LogRecord } from '../log_record';
import { ConsoleAppender } from './console/console_appender';
import { FileAppender } from './file/file_appender';
-const appendersSchema = schema.oneOf([
+/**
+ * Config schema for validting the shape of the `appenders` key in in {@link LoggerContextConfigType} or
+ * {@link LoggingConfigType}.
+ *
+ * @public
+ */
+export const appendersSchema = schema.oneOf([
ConsoleAppender.configSchema,
FileAppender.configSchema,
LegacyAppender.configSchema,
]);
-/** @internal */
+/** @public */
export type AppenderConfigType = TypeOf;
/**
diff --git a/src/core/server/logging/index.ts b/src/core/server/logging/index.ts
index fd35ed39092b3..9471972030281 100644
--- a/src/core/server/logging/index.ts
+++ b/src/core/server/logging/index.ts
@@ -21,7 +21,18 @@ export { Logger, LogMeta } from './logger';
export { LoggerFactory } from './logger_factory';
export { LogRecord } from './log_record';
export { LogLevel } from './log_level';
-/** @internal */
-export { config, LoggingConfigType } from './logging_config';
-/** @internal */
-export { LoggingService, ILoggingService } from './logging_service';
+export {
+ config,
+ LoggingConfigType,
+ LoggerContextConfigInput,
+ LoggerConfigType,
+ loggerContextConfigSchema,
+ loggerSchema,
+} from './logging_config';
+export { LoggingSystem, ILoggingSystem } from './logging_system';
+export {
+ InternalLoggingServiceSetup,
+ LoggingServiceSetup,
+ LoggingService,
+} from './logging_service';
+export { appendersSchema, AppenderConfigType } from './appenders/appenders';
diff --git a/src/core/server/logging/logging_config.test.ts b/src/core/server/logging/logging_config.test.ts
index 75f571d34c25c..e2ce3e1983aa1 100644
--- a/src/core/server/logging/logging_config.test.ts
+++ b/src/core/server/logging/logging_config.test.ts
@@ -171,3 +171,127 @@ test('fails if loggers use unknown appenders.', () => {
expect(() => new LoggingConfig(validateConfig)).toThrowErrorMatchingSnapshot();
});
+
+describe('extend', () => {
+ it('adds new appenders', () => {
+ const configValue = new LoggingConfig(
+ config.schema.validate({
+ appenders: {
+ file1: {
+ kind: 'file',
+ layout: { kind: 'pattern' },
+ path: 'path',
+ },
+ },
+ })
+ );
+
+ const mergedConfigValue = configValue.extend(
+ config.schema.validate({
+ appenders: {
+ file2: {
+ kind: 'file',
+ layout: { kind: 'pattern' },
+ path: 'path',
+ },
+ },
+ })
+ );
+
+ expect([...mergedConfigValue.appenders.keys()]).toEqual([
+ 'default',
+ 'console',
+ 'file1',
+ 'file2',
+ ]);
+ });
+
+ it('overrides appenders', () => {
+ const configValue = new LoggingConfig(
+ config.schema.validate({
+ appenders: {
+ file1: {
+ kind: 'file',
+ layout: { kind: 'pattern' },
+ path: 'path',
+ },
+ },
+ })
+ );
+
+ const mergedConfigValue = configValue.extend(
+ config.schema.validate({
+ appenders: {
+ file1: {
+ kind: 'file',
+ layout: { kind: 'json' },
+ path: 'updatedPath',
+ },
+ },
+ })
+ );
+
+ expect(mergedConfigValue.appenders.get('file1')).toEqual({
+ kind: 'file',
+ layout: { kind: 'json' },
+ path: 'updatedPath',
+ });
+ });
+
+ it('adds new loggers', () => {
+ const configValue = new LoggingConfig(
+ config.schema.validate({
+ loggers: [
+ {
+ context: 'plugins',
+ level: 'warn',
+ },
+ ],
+ })
+ );
+
+ const mergedConfigValue = configValue.extend(
+ config.schema.validate({
+ loggers: [
+ {
+ context: 'plugins.pid',
+ level: 'trace',
+ },
+ ],
+ })
+ );
+
+ expect([...mergedConfigValue.loggers.keys()]).toEqual(['root', 'plugins', 'plugins.pid']);
+ });
+
+ it('overrides loggers', () => {
+ const configValue = new LoggingConfig(
+ config.schema.validate({
+ loggers: [
+ {
+ context: 'plugins',
+ level: 'warn',
+ },
+ ],
+ })
+ );
+
+ const mergedConfigValue = configValue.extend(
+ config.schema.validate({
+ loggers: [
+ {
+ appenders: ['console'],
+ context: 'plugins',
+ level: 'trace',
+ },
+ ],
+ })
+ );
+
+ expect(mergedConfigValue.loggers.get('plugins')).toEqual({
+ appenders: ['console'],
+ context: 'plugins',
+ level: 'trace',
+ });
+ });
+});
diff --git a/src/core/server/logging/logging_config.ts b/src/core/server/logging/logging_config.ts
index 772909ce584e5..a6aafabeb970c 100644
--- a/src/core/server/logging/logging_config.ts
+++ b/src/core/server/logging/logging_config.ts
@@ -39,7 +39,7 @@ const ROOT_CONTEXT_NAME = 'root';
*/
const DEFAULT_APPENDER_NAME = 'default';
-const createLevelSchema = schema.oneOf(
+const levelSchema = schema.oneOf(
[
schema.literal('all'),
schema.literal('fatal'),
@@ -55,21 +55,26 @@ const createLevelSchema = schema.oneOf(
}
);
-const createLoggerSchema = schema.object({
+/**
+ * Config schema for validating the `loggers` key in {@link LoggerContextConfigType} or {@link LoggingConfigType}.
+ *
+ * @public
+ */
+export const loggerSchema = schema.object({
appenders: schema.arrayOf(schema.string(), { defaultValue: [] }),
context: schema.string(),
- level: createLevelSchema,
+ level: levelSchema,
});
-/** @internal */
-export type LoggerConfigType = TypeOf;
+/** @public */
+export type LoggerConfigType = TypeOf;
export const config = {
path: 'logging',
schema: schema.object({
appenders: schema.mapOf(schema.string(), Appenders.configSchema, {
defaultValue: new Map(),
}),
- loggers: schema.arrayOf(createLoggerSchema, {
+ loggers: schema.arrayOf(loggerSchema, {
defaultValue: [],
}),
root: schema.object(
@@ -78,7 +83,7 @@ export const config = {
defaultValue: [DEFAULT_APPENDER_NAME],
minSize: 1,
}),
- level: createLevelSchema,
+ level: levelSchema,
},
{
validate(rawConfig) {
@@ -93,6 +98,29 @@ export const config = {
export type LoggingConfigType = TypeOf;
+/**
+ * Config schema for validating the inputs to the {@link LoggingServiceStart.configure} API.
+ * See {@link LoggerContextConfigType}.
+ *
+ * @public
+ */
+export const loggerContextConfigSchema = schema.object({
+ appenders: schema.mapOf(schema.string(), Appenders.configSchema, {
+ defaultValue: new Map(),
+ }),
+
+ loggers: schema.arrayOf(loggerSchema, { defaultValue: [] }),
+});
+
+/** @public */
+export type LoggerContextConfigType = TypeOf;
+/** @public */
+export interface LoggerContextConfigInput {
+ // config-schema knows how to handle either Maps or Records
+ appenders?: Record | Map;
+ loggers?: LoggerConfigType[];
+}
+
/**
* Describes the config used to fully setup logging subsystem.
* @internal
@@ -147,11 +175,35 @@ export class LoggingConfig {
*/
public readonly loggers: Map = new Map();
- constructor(configType: LoggingConfigType) {
+ constructor(private readonly configType: LoggingConfigType) {
this.fillAppendersConfig(configType);
this.fillLoggersConfig(configType);
}
+ /**
+ * Returns a new LoggingConfig that merges the existing config with the specified config.
+ *
+ * @remarks
+ * Does not support merging the `root` config property.
+ *
+ * @param contextConfig
+ */
+ public extend(contextConfig: LoggerContextConfigType) {
+ // Use a Map to de-dupe any loggers for the same context. contextConfig overrides existing config.
+ const mergedLoggers = new Map([
+ ...this.configType.loggers.map((l) => [l.context, l] as [string, LoggerConfigType]),
+ ...contextConfig.loggers.map((l) => [l.context, l] as [string, LoggerConfigType]),
+ ]);
+
+ const mergedConfig: LoggingConfigType = {
+ appenders: new Map([...this.configType.appenders, ...contextConfig.appenders]),
+ loggers: [...mergedLoggers.values()],
+ root: this.configType.root,
+ };
+
+ return new LoggingConfig(mergedConfig);
+ }
+
private fillAppendersConfig(loggingConfig: LoggingConfigType) {
for (const [appenderKey, appenderSchema] of loggingConfig.appenders) {
this.appenders.set(appenderKey, appenderSchema);
diff --git a/src/core/server/logging/logging_service.mock.ts b/src/core/server/logging/logging_service.mock.ts
index 15d66c2e8535c..21edbe670eaec 100644
--- a/src/core/server/logging/logging_service.mock.ts
+++ b/src/core/server/logging/logging_service.mock.ts
@@ -17,67 +17,35 @@
* under the License.
*/
-// Test helpers to simplify mocking logs and collecting all their outputs
-import { ILoggingService } from './logging_service';
-import { LoggerFactory } from './logger_factory';
-import { loggerMock, MockedLogger } from './logger.mock';
-
-const createLoggingServiceMock = () => {
- const mockLog = loggerMock.create();
-
- mockLog.get.mockImplementation((...context) => ({
- ...mockLog,
- context,
- }));
-
- const mocked: jest.Mocked = {
- get: jest.fn(),
- asLoggerFactory: jest.fn(),
- upgrade: jest.fn(),
+import {
+ LoggingService,
+ LoggingServiceSetup,
+ InternalLoggingServiceSetup,
+} from './logging_service';
+
+const createInternalSetupMock = (): jest.Mocked => ({
+ configure: jest.fn(),
+});
+
+const createSetupMock = (): jest.Mocked => ({
+ configure: jest.fn(),
+});
+
+type LoggingServiceContract = PublicMethodsOf;
+const createMock = (): jest.Mocked => {
+ const service: jest.Mocked = {
+ setup: jest.fn(),
+ start: jest.fn(),
stop: jest.fn(),
};
- mocked.get.mockImplementation((...context) => ({
- ...mockLog,
- context,
- }));
- mocked.asLoggerFactory.mockImplementation(() => mocked);
- mocked.stop.mockResolvedValue();
- return mocked;
-};
-
-const collectLoggingServiceMock = (loggerFactory: LoggerFactory) => {
- const mockLog = loggerFactory.get() as MockedLogger;
- return {
- debug: mockLog.debug.mock.calls,
- error: mockLog.error.mock.calls,
- fatal: mockLog.fatal.mock.calls,
- info: mockLog.info.mock.calls,
- log: mockLog.log.mock.calls,
- trace: mockLog.trace.mock.calls,
- warn: mockLog.warn.mock.calls,
- };
-};
-const clearLoggingServiceMock = (loggerFactory: LoggerFactory) => {
- const mockedLoggerFactory = (loggerFactory as unknown) as jest.Mocked;
- mockedLoggerFactory.get.mockClear();
- mockedLoggerFactory.asLoggerFactory.mockClear();
- mockedLoggerFactory.upgrade.mockClear();
- mockedLoggerFactory.stop.mockClear();
+ service.setup.mockReturnValue(createInternalSetupMock());
- const mockLog = loggerFactory.get() as MockedLogger;
- mockLog.debug.mockClear();
- mockLog.info.mockClear();
- mockLog.warn.mockClear();
- mockLog.error.mockClear();
- mockLog.trace.mockClear();
- mockLog.fatal.mockClear();
- mockLog.log.mockClear();
+ return service;
};
export const loggingServiceMock = {
- create: createLoggingServiceMock,
- collect: collectLoggingServiceMock,
- clear: clearLoggingServiceMock,
- createLogger: loggerMock.create,
+ create: createMock,
+ createSetupContract: createSetupMock,
+ createInternalSetupContract: createInternalSetupMock,
};
diff --git a/src/core/server/logging/logging_service.test.ts b/src/core/server/logging/logging_service.test.ts
index 1e6c253c56c7b..5107db77304fc 100644
--- a/src/core/server/logging/logging_service.test.ts
+++ b/src/core/server/logging/logging_service.test.ts
@@ -16,167 +16,85 @@
* specific language governing permissions and limitations
* under the License.
*/
-
-const mockStreamWrite = jest.fn();
-jest.mock('fs', () => ({
- constants: {},
- createWriteStream: jest.fn(() => ({ write: mockStreamWrite })),
-}));
-
-const dynamicProps = { pid: expect.any(Number) };
-
-jest.mock('../../../legacy/server/logging/rotate', () => ({
- setupLoggingRotate: jest.fn().mockImplementation(() => Promise.resolve({})),
-}));
-
-const timestamp = new Date(Date.UTC(2012, 1, 1, 14, 33, 22, 11));
-let mockConsoleLog: jest.SpyInstance;
-
-import { createWriteStream } from 'fs';
-const mockCreateWriteStream = (createWriteStream as unknown) as jest.Mock;
-
-import { LoggingService, config } from '.';
-
-let service: LoggingService;
-beforeEach(() => {
- mockConsoleLog = jest.spyOn(global.console, 'log').mockReturnValue(undefined);
- jest.spyOn(global, 'Date').mockImplementation(() => timestamp);
- service = new LoggingService();
-});
-
-afterEach(() => {
- jest.restoreAllMocks();
- mockCreateWriteStream.mockClear();
- mockStreamWrite.mockClear();
-});
-
-test('uses default memory buffer logger until config is provided', () => {
- const bufferAppendSpy = jest.spyOn((service as any).bufferAppender, 'append');
-
- const logger = service.get('test', 'context');
- logger.trace('trace message');
-
- // We shouldn't create new buffer appender for another context.
- const anotherLogger = service.get('test', 'context2');
- anotherLogger.fatal('fatal message', { some: 'value' });
-
- expect(bufferAppendSpy).toHaveBeenCalledTimes(2);
- expect(bufferAppendSpy.mock.calls[0][0]).toMatchSnapshot(dynamicProps);
- expect(bufferAppendSpy.mock.calls[1][0]).toMatchSnapshot(dynamicProps);
-});
-
-test('flushes memory buffer logger and switches to real logger once config is provided', () => {
- const logger = service.get('test', 'context');
-
- logger.trace('buffered trace message');
- logger.info('buffered info message', { some: 'value' });
- logger.fatal('buffered fatal message');
-
- const bufferAppendSpy = jest.spyOn((service as any).bufferAppender, 'append');
-
- // Switch to console appender with `info` level, so that `trace` message won't go through.
- service.upgrade(
- config.schema.validate({
- appenders: { default: { kind: 'console', layout: { kind: 'json' } } },
- root: { level: 'info' },
- })
- );
-
- expect(JSON.parse(mockConsoleLog.mock.calls[0][0])).toMatchSnapshot(
- dynamicProps,
- 'buffered messages'
- );
- mockConsoleLog.mockClear();
-
- // Now message should go straight to thew newly configured appender, not buffered one.
- logger.info('some new info message');
- expect(JSON.parse(mockConsoleLog.mock.calls[0][0])).toMatchSnapshot(dynamicProps, 'new messages');
- expect(bufferAppendSpy).not.toHaveBeenCalled();
-});
-
-test('appends records via multiple appenders.', () => {
- const loggerWithoutConfig = service.get('some-context');
- const testsLogger = service.get('tests');
- const testsChildLogger = service.get('tests', 'child');
-
- loggerWithoutConfig.info('You know, just for your info.');
- testsLogger.warn('Config is not ready!');
- testsChildLogger.error('Too bad that config is not ready :/');
- testsChildLogger.info('Just some info that should not be logged.');
-
- expect(mockConsoleLog).not.toHaveBeenCalled();
- expect(mockCreateWriteStream).not.toHaveBeenCalled();
-
- service.upgrade(
- config.schema.validate({
- appenders: {
- default: { kind: 'console', layout: { kind: 'pattern' } },
- file: { kind: 'file', layout: { kind: 'pattern' }, path: 'path' },
- },
- loggers: [
- { appenders: ['file'], context: 'tests', level: 'warn' },
- { context: 'tests.child', level: 'error' },
- ],
- })
- );
-
- // Now all logs should added to configured appenders.
- expect(mockConsoleLog).toHaveBeenCalledTimes(1);
- expect(mockConsoleLog.mock.calls[0][0]).toMatchSnapshot('console logs');
-
- expect(mockStreamWrite).toHaveBeenCalledTimes(2);
- expect(mockStreamWrite.mock.calls[0][0]).toMatchSnapshot('file logs');
- expect(mockStreamWrite.mock.calls[1][0]).toMatchSnapshot('file logs');
-});
-
-test('uses `root` logger if context is not specified.', () => {
- service.upgrade(
- config.schema.validate({
- appenders: { default: { kind: 'console', layout: { kind: 'pattern' } } },
- })
- );
-
- const rootLogger = service.get();
- rootLogger.info('This message goes to a root context.');
-
- expect(mockConsoleLog.mock.calls).toMatchSnapshot();
-});
-
-test('`stop()` disposes all appenders.', async () => {
- service.upgrade(
- config.schema.validate({
- appenders: { default: { kind: 'console', layout: { kind: 'json' } } },
- root: { level: 'info' },
- })
- );
-
- const bufferDisposeSpy = jest.spyOn((service as any).bufferAppender, 'dispose');
- const consoleDisposeSpy = jest.spyOn((service as any).appenders.get('default'), 'dispose');
-
- await service.stop();
-
- expect(bufferDisposeSpy).toHaveBeenCalledTimes(1);
- expect(consoleDisposeSpy).toHaveBeenCalledTimes(1);
-});
-
-test('asLoggerFactory() only allows to create new loggers.', () => {
- const logger = service.asLoggerFactory().get('test', 'context');
-
- service.upgrade(
- config.schema.validate({
- appenders: { default: { kind: 'console', layout: { kind: 'json' } } },
- root: { level: 'all' },
- })
- );
-
- logger.trace('buffered trace message');
- logger.info('buffered info message', { some: 'value' });
- logger.fatal('buffered fatal message');
-
- expect(Object.keys(service.asLoggerFactory())).toEqual(['get']);
-
- expect(mockConsoleLog).toHaveBeenCalledTimes(3);
- expect(JSON.parse(mockConsoleLog.mock.calls[0][0])).toMatchSnapshot(dynamicProps);
- expect(JSON.parse(mockConsoleLog.mock.calls[1][0])).toMatchSnapshot(dynamicProps);
- expect(JSON.parse(mockConsoleLog.mock.calls[2][0])).toMatchSnapshot(dynamicProps);
+import { of, Subject } from 'rxjs';
+
+import { LoggingService, InternalLoggingServiceSetup } from './logging_service';
+import { loggingSystemMock } from './logging_system.mock';
+import { LoggerContextConfigType } from './logging_config';
+
+describe('LoggingService', () => {
+ let loggingSystem: ReturnType;
+ let service: LoggingService;
+ let setup: InternalLoggingServiceSetup;
+
+ beforeEach(() => {
+ loggingSystem = loggingSystemMock.create();
+ service = new LoggingService({ logger: loggingSystem.asLoggerFactory() } as any);
+ setup = service.setup({ loggingSystem });
+ });
+ afterEach(() => {
+ service.stop();
+ });
+
+ describe('setup', () => {
+ it('forwards configuration changes to logging system', () => {
+ const config1: LoggerContextConfigType = {
+ appenders: new Map(),
+ loggers: [{ context: 'subcontext', appenders: ['console'], level: 'warn' }],
+ };
+ const config2: LoggerContextConfigType = {
+ appenders: new Map(),
+ loggers: [{ context: 'subcontext', appenders: ['default'], level: 'all' }],
+ };
+
+ setup.configure(['test', 'context'], of(config1, config2));
+ expect(loggingSystem.setContextConfig).toHaveBeenNthCalledWith(
+ 1,
+ ['test', 'context'],
+ config1
+ );
+ expect(loggingSystem.setContextConfig).toHaveBeenNthCalledWith(
+ 2,
+ ['test', 'context'],
+ config2
+ );
+ });
+
+ it('stops forwarding first observable when called a second time', () => {
+ const updates$ = new Subject();
+ const config1: LoggerContextConfigType = {
+ appenders: new Map(),
+ loggers: [{ context: 'subcontext', appenders: ['console'], level: 'warn' }],
+ };
+ const config2: LoggerContextConfigType = {
+ appenders: new Map(),
+ loggers: [{ context: 'subcontext', appenders: ['default'], level: 'all' }],
+ };
+
+ setup.configure(['test', 'context'], updates$);
+ setup.configure(['test', 'context'], of(config1));
+ updates$.next(config2);
+ expect(loggingSystem.setContextConfig).toHaveBeenNthCalledWith(
+ 1,
+ ['test', 'context'],
+ config1
+ );
+ expect(loggingSystem.setContextConfig).not.toHaveBeenCalledWith(['test', 'context'], config2);
+ });
+ });
+
+ describe('stop', () => {
+ it('stops forwarding updates to logging system', () => {
+ const updates$ = new Subject();
+ const config1: LoggerContextConfigType = {
+ appenders: new Map(),
+ loggers: [{ context: 'subcontext', appenders: ['console'], level: 'warn' }],
+ };
+
+ setup.configure(['test', 'context'], updates$);
+ service.stop();
+ updates$.next(config1);
+ expect(loggingSystem.setContextConfig).not.toHaveBeenCalledWith(['test', 'context'], config1);
+ });
+ });
});
diff --git a/src/core/server/logging/logging_service.ts b/src/core/server/logging/logging_service.ts
index 2e6f895724122..09051f8f07702 100644
--- a/src/core/server/logging/logging_service.ts
+++ b/src/core/server/logging/logging_service.ts
@@ -16,112 +16,88 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { Appenders, DisposableAppender } from './appenders/appenders';
-import { BufferAppender } from './appenders/buffer/buffer_appender';
-import { LogLevel } from './log_level';
-import { BaseLogger, Logger } from './logger';
-import { LoggerAdapter } from './logger_adapter';
-import { LoggerFactory } from './logger_factory';
-import { LoggingConfigType, LoggerConfigType, LoggingConfig } from './logging_config';
-export type ILoggingService = PublicMethodsOf;
+import { Observable, Subscription } from 'rxjs';
+import { CoreService } from '../../types';
+import { LoggingConfig, LoggerContextConfigInput } from './logging_config';
+import { ILoggingSystem } from './logging_system';
+import { Logger } from './logger';
+import { CoreContext } from '../core_context';
+
/**
- * Service that is responsible for maintaining loggers and logger appenders.
- * @internal
+ * Provides APIs to plugins for customizing the plugin's logger.
+ * @public
*/
-export class LoggingService implements LoggerFactory {
- private config?: LoggingConfig;
- private readonly appenders: Map = new Map();
- private readonly bufferAppender = new BufferAppender();
- private readonly loggers: Map = new Map();
-
- 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.config)));
- }
- return this.loggers.get(context)!;
- }
-
- /**
- * Safe wrapper that allows passing logging service as immutable LoggerFactory.
- */
- public asLoggerFactory(): LoggerFactory {
- return { get: (...contextParts: string[]) => this.get(...contextParts) };
- }
-
+export interface LoggingServiceSetup {
/**
- * Updates all current active loggers with the new config values.
- * @param rawConfig New config instance.
+ * Customizes the logging config for the plugin's context.
+ *
+ * @remarks
+ * Assumes that that the `context` property of the individual `logger` items emitted by `config$`
+ * are relative to the plugin's logging context (defaults to `plugins.`).
+ *
+ * @example
+ * Customize the configuration for the plugins.data.search context.
+ * ```ts
+ * core.logging.configure(
+ * of({
+ * appenders: new Map(),
+ * loggers: [{ context: 'search', appenders: ['default'] }]
+ * })
+ * )
+ * ```
+ *
+ * @param config$
*/
- public upgrade(rawConfig: LoggingConfigType) {
- const config = new LoggingConfig(rawConfig);
- // Config update is asynchronous and may require some time to complete, so we should invalidate
- // config so that new loggers will be using BufferAppender until newly configured appenders are ready.
- this.config = undefined;
-
- // Appenders must be reset, so we first dispose of the current ones, then
- // build up a new set of appenders.
- for (const appender of this.appenders.values()) {
- appender.dispose();
- }
- this.appenders.clear();
+ configure(config$: Observable): void;
+}
- for (const [appenderKey, appenderConfig] of config.appenders) {
- this.appenders.set(appenderKey, Appenders.create(appenderConfig));
- }
+/** @internal */
+export interface InternalLoggingServiceSetup {
+ configure(contextParts: string[], config$: Observable): void;
+}
- for (const [loggerKey, loggerAdapter] of this.loggers) {
- loggerAdapter.updateLogger(this.createLogger(loggerKey, config));
- }
+interface SetupDeps {
+ loggingSystem: ILoggingSystem;
+}
- this.config = config;
+/** @internal */
+export class LoggingService implements CoreService {
+ private readonly subscriptions = new Map();
+ private readonly log: Logger;
- // Re-log all buffered log records with newly configured appenders.
- for (const logRecord of this.bufferAppender.flush()) {
- this.get(logRecord.context).log(logRecord);
- }
+ constructor(coreContext: CoreContext) {
+ this.log = coreContext.logger.get('logging');
}
- /**
- * 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.
- * @returns Promise that is resolved once all loggers are successfully disposed.
- */
- public async stop() {
- for (const appender of this.appenders.values()) {
- await appender.dispose();
- }
-
- await this.bufferAppender.dispose();
-
- this.appenders.clear();
- this.loggers.clear();
+ public setup({ loggingSystem }: SetupDeps) {
+ return {
+ configure: (contextParts: string[], config$: Observable) => {
+ const contextName = LoggingConfig.getLoggerContext(contextParts);
+ this.log.debug(`Setting custom config for context [${contextName}]`);
+
+ const existingSubscription = this.subscriptions.get(contextName);
+ if (existingSubscription) {
+ existingSubscription.unsubscribe();
+ }
+
+ // Might be fancier way to do this with rxjs, but this works and is simple to understand
+ this.subscriptions.set(
+ contextName,
+ config$.subscribe((config) => {
+ this.log.debug(`Updating logging config for context [${contextName}]`);
+ loggingSystem.setContextConfig(contextParts, config);
+ })
+ );
+ },
+ };
}
- private createLogger(context: string, config: LoggingConfig | undefined) {
- if (config === undefined) {
- // If we don't have config yet, use `buffered` appender that will store all logged messages in the memory
- // until the config is ready.
- return new BaseLogger(context, LogLevel.All, [this.bufferAppender], this.asLoggerFactory());
- }
-
- const { level, appenders } = this.getLoggerConfigByContext(config, context);
- const loggerLevel = LogLevel.fromId(level);
- const loggerAppenders = appenders.map((appenderKey) => this.appenders.get(appenderKey)!);
+ public start() {}
- return new BaseLogger(context, loggerLevel, loggerAppenders, this.asLoggerFactory());
- }
-
- private getLoggerConfigByContext(config: LoggingConfig, context: string): LoggerConfigType {
- const loggerConfig = config.loggers.get(context);
- if (loggerConfig !== undefined) {
- return loggerConfig;
+ public stop() {
+ for (const [, subscription] of this.subscriptions) {
+ subscription.unsubscribe();
}
-
- // If we don't have configuration for the specified context and it's the "nested" one (eg. `foo.bar.baz`),
- // let's move up to the parent context (eg. `foo.bar`) and check if it has config we can rely on. Otherwise
- // we fallback to the `root` context that should always be defined (enforced by configuration schema).
- return this.getLoggerConfigByContext(config, LoggingConfig.getParentLoggerContext(context));
}
}
diff --git a/src/core/server/logging/logging_system.mock.ts b/src/core/server/logging/logging_system.mock.ts
new file mode 100644
index 0000000000000..ac1e9b5196002
--- /dev/null
+++ b/src/core/server/logging/logging_system.mock.ts
@@ -0,0 +1,84 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+// Test helpers to simplify mocking logs and collecting all their outputs
+import { ILoggingSystem } from './logging_system';
+import { LoggerFactory } from './logger_factory';
+import { loggerMock, MockedLogger } from './logger.mock';
+
+const createLoggingSystemMock = () => {
+ const mockLog = loggerMock.create();
+
+ mockLog.get.mockImplementation((...context) => ({
+ ...mockLog,
+ context,
+ }));
+
+ const mocked: jest.Mocked = {
+ get: jest.fn(),
+ asLoggerFactory: jest.fn(),
+ setContextConfig: jest.fn(),
+ upgrade: jest.fn(),
+ stop: jest.fn(),
+ };
+ mocked.get.mockImplementation((...context) => ({
+ ...mockLog,
+ context,
+ }));
+ mocked.asLoggerFactory.mockImplementation(() => mocked);
+ mocked.stop.mockResolvedValue();
+ return mocked;
+};
+
+const collectLoggingSystemMock = (loggerFactory: LoggerFactory) => {
+ const mockLog = loggerFactory.get() as MockedLogger;
+ return {
+ debug: mockLog.debug.mock.calls,
+ error: mockLog.error.mock.calls,
+ fatal: mockLog.fatal.mock.calls,
+ info: mockLog.info.mock.calls,
+ log: mockLog.log.mock.calls,
+ trace: mockLog.trace.mock.calls,
+ warn: mockLog.warn.mock.calls,
+ };
+};
+
+const clearLoggingSystemMock = (loggerFactory: LoggerFactory) => {
+ const mockedLoggerFactory = (loggerFactory as unknown) as jest.Mocked;
+ mockedLoggerFactory.get.mockClear();
+ mockedLoggerFactory.asLoggerFactory.mockClear();
+ mockedLoggerFactory.upgrade.mockClear();
+ mockedLoggerFactory.stop.mockClear();
+
+ const mockLog = loggerFactory.get() as MockedLogger;
+ mockLog.debug.mockClear();
+ mockLog.info.mockClear();
+ mockLog.warn.mockClear();
+ mockLog.error.mockClear();
+ mockLog.trace.mockClear();
+ mockLog.fatal.mockClear();
+ mockLog.log.mockClear();
+};
+
+export const loggingSystemMock = {
+ create: createLoggingSystemMock,
+ collect: collectLoggingSystemMock,
+ clear: clearLoggingSystemMock,
+ createLogger: loggerMock.create,
+};
diff --git a/src/core/server/logging/logging_system.test.ts b/src/core/server/logging/logging_system.test.ts
new file mode 100644
index 0000000000000..f73e40fe320dc
--- /dev/null
+++ b/src/core/server/logging/logging_system.test.ts
@@ -0,0 +1,348 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+const mockStreamWrite = jest.fn();
+jest.mock('fs', () => ({
+ constants: {},
+ createWriteStream: jest.fn(() => ({ write: mockStreamWrite })),
+}));
+
+const dynamicProps = { pid: expect.any(Number) };
+
+jest.mock('../../../legacy/server/logging/rotate', () => ({
+ setupLoggingRotate: jest.fn().mockImplementation(() => Promise.resolve({})),
+}));
+
+const timestamp = new Date(Date.UTC(2012, 1, 1, 14, 33, 22, 11));
+let mockConsoleLog: jest.SpyInstance;
+
+import { createWriteStream } from 'fs';
+const mockCreateWriteStream = (createWriteStream as unknown) as jest.Mock;
+
+import { LoggingSystem, config } from '.';
+
+let system: LoggingSystem;
+beforeEach(() => {
+ mockConsoleLog = jest.spyOn(global.console, 'log').mockReturnValue(undefined);
+ jest.spyOn(global, 'Date').mockImplementation(() => timestamp);
+ system = new LoggingSystem();
+});
+
+afterEach(() => {
+ jest.restoreAllMocks();
+ mockCreateWriteStream.mockClear();
+ mockStreamWrite.mockClear();
+});
+
+test('uses default memory buffer logger until config is provided', () => {
+ const bufferAppendSpy = jest.spyOn((system as any).bufferAppender, 'append');
+
+ const logger = system.get('test', 'context');
+ logger.trace('trace message');
+
+ // We shouldn't create new buffer appender for another context.
+ const anotherLogger = system.get('test', 'context2');
+ anotherLogger.fatal('fatal message', { some: 'value' });
+
+ expect(bufferAppendSpy).toHaveBeenCalledTimes(2);
+ expect(bufferAppendSpy.mock.calls[0][0]).toMatchSnapshot(dynamicProps);
+ expect(bufferAppendSpy.mock.calls[1][0]).toMatchSnapshot(dynamicProps);
+});
+
+test('flushes memory buffer logger and switches to real logger once config is provided', () => {
+ const logger = system.get('test', 'context');
+
+ logger.trace('buffered trace message');
+ logger.info('buffered info message', { some: 'value' });
+ logger.fatal('buffered fatal message');
+
+ const bufferAppendSpy = jest.spyOn((system as any).bufferAppender, 'append');
+
+ // Switch to console appender with `info` level, so that `trace` message won't go through.
+ system.upgrade(
+ config.schema.validate({
+ appenders: { default: { kind: 'console', layout: { kind: 'json' } } },
+ root: { level: 'info' },
+ })
+ );
+
+ expect(JSON.parse(mockConsoleLog.mock.calls[0][0])).toMatchSnapshot(
+ dynamicProps,
+ 'buffered messages'
+ );
+ mockConsoleLog.mockClear();
+
+ // Now message should go straight to thew newly configured appender, not buffered one.
+ logger.info('some new info message');
+ expect(JSON.parse(mockConsoleLog.mock.calls[0][0])).toMatchSnapshot(dynamicProps, 'new messages');
+ expect(bufferAppendSpy).not.toHaveBeenCalled();
+});
+
+test('appends records via multiple appenders.', () => {
+ const loggerWithoutConfig = system.get('some-context');
+ const testsLogger = system.get('tests');
+ const testsChildLogger = system.get('tests', 'child');
+
+ loggerWithoutConfig.info('You know, just for your info.');
+ testsLogger.warn('Config is not ready!');
+ testsChildLogger.error('Too bad that config is not ready :/');
+ testsChildLogger.info('Just some info that should not be logged.');
+
+ expect(mockConsoleLog).not.toHaveBeenCalled();
+ expect(mockCreateWriteStream).not.toHaveBeenCalled();
+
+ system.upgrade(
+ config.schema.validate({
+ appenders: {
+ default: { kind: 'console', layout: { kind: 'pattern' } },
+ file: { kind: 'file', layout: { kind: 'pattern' }, path: 'path' },
+ },
+ loggers: [
+ { appenders: ['file'], context: 'tests', level: 'warn' },
+ { context: 'tests.child', level: 'error' },
+ ],
+ })
+ );
+
+ // Now all logs should added to configured appenders.
+ expect(mockConsoleLog).toHaveBeenCalledTimes(1);
+ expect(mockConsoleLog.mock.calls[0][0]).toMatchSnapshot('console logs');
+
+ expect(mockStreamWrite).toHaveBeenCalledTimes(2);
+ expect(mockStreamWrite.mock.calls[0][0]).toMatchSnapshot('file logs');
+ expect(mockStreamWrite.mock.calls[1][0]).toMatchSnapshot('file logs');
+});
+
+test('uses `root` logger if context is not specified.', () => {
+ system.upgrade(
+ config.schema.validate({
+ appenders: { default: { kind: 'console', layout: { kind: 'pattern' } } },
+ })
+ );
+
+ const rootLogger = system.get();
+ rootLogger.info('This message goes to a root context.');
+
+ expect(mockConsoleLog.mock.calls).toMatchSnapshot();
+});
+
+test('`stop()` disposes all appenders.', async () => {
+ system.upgrade(
+ config.schema.validate({
+ appenders: { default: { kind: 'console', layout: { kind: 'json' } } },
+ root: { level: 'info' },
+ })
+ );
+
+ const bufferDisposeSpy = jest.spyOn((system as any).bufferAppender, 'dispose');
+ const consoleDisposeSpy = jest.spyOn((system as any).appenders.get('default'), 'dispose');
+
+ await system.stop();
+
+ expect(bufferDisposeSpy).toHaveBeenCalledTimes(1);
+ expect(consoleDisposeSpy).toHaveBeenCalledTimes(1);
+});
+
+test('asLoggerFactory() only allows to create new loggers.', () => {
+ const logger = system.asLoggerFactory().get('test', 'context');
+
+ system.upgrade(
+ config.schema.validate({
+ appenders: { default: { kind: 'console', layout: { kind: 'json' } } },
+ root: { level: 'all' },
+ })
+ );
+
+ logger.trace('buffered trace message');
+ logger.info('buffered info message', { some: 'value' });
+ logger.fatal('buffered fatal message');
+
+ expect(Object.keys(system.asLoggerFactory())).toEqual(['get']);
+
+ expect(mockConsoleLog).toHaveBeenCalledTimes(3);
+ expect(JSON.parse(mockConsoleLog.mock.calls[0][0])).toMatchSnapshot(dynamicProps);
+ expect(JSON.parse(mockConsoleLog.mock.calls[1][0])).toMatchSnapshot(dynamicProps);
+ expect(JSON.parse(mockConsoleLog.mock.calls[2][0])).toMatchSnapshot(dynamicProps);
+});
+
+test('setContextConfig() updates config with relative contexts', () => {
+ const testsLogger = system.get('tests');
+ const testsChildLogger = system.get('tests', 'child');
+ const testsGrandchildLogger = system.get('tests', 'child', 'grandchild');
+
+ system.upgrade(
+ config.schema.validate({
+ appenders: { default: { kind: 'console', layout: { kind: 'json' } } },
+ root: { level: 'info' },
+ })
+ );
+
+ system.setContextConfig(['tests', 'child'], {
+ appenders: new Map([
+ [
+ 'custom',
+ { kind: 'console', layout: { kind: 'pattern', pattern: '[%level][%logger] %message' } },
+ ],
+ ]),
+ loggers: [{ context: 'grandchild', appenders: ['default', 'custom'], level: 'debug' }],
+ });
+
+ testsLogger.warn('tests log to default!');
+ testsChildLogger.error('tests.child log to default!');
+ testsGrandchildLogger.debug('tests.child.grandchild log to default and custom!');
+
+ expect(mockConsoleLog).toHaveBeenCalledTimes(4);
+ // Parent contexts are unaffected
+ expect(JSON.parse(mockConsoleLog.mock.calls[0][0])).toMatchObject({
+ context: 'tests',
+ message: 'tests log to default!',
+ level: 'WARN',
+ });
+ expect(JSON.parse(mockConsoleLog.mock.calls[1][0])).toMatchObject({
+ context: 'tests.child',
+ message: 'tests.child log to default!',
+ level: 'ERROR',
+ });
+ // Customized context is logged in both appender formats
+ expect(JSON.parse(mockConsoleLog.mock.calls[2][0])).toMatchObject({
+ context: 'tests.child.grandchild',
+ message: 'tests.child.grandchild log to default and custom!',
+ level: 'DEBUG',
+ });
+ expect(mockConsoleLog.mock.calls[3][0]).toMatchInlineSnapshot(
+ `"[DEBUG][tests.child.grandchild] tests.child.grandchild log to default and custom!"`
+ );
+});
+
+test('custom context configs are applied on subsequent calls to update()', () => {
+ system.setContextConfig(['tests', 'child'], {
+ appenders: new Map([
+ [
+ 'custom',
+ { kind: 'console', layout: { kind: 'pattern', pattern: '[%level][%logger] %message' } },
+ ],
+ ]),
+ loggers: [{ context: 'grandchild', appenders: ['default', 'custom'], level: 'debug' }],
+ });
+
+ // Calling upgrade after setContextConfig should not throw away the context-specific config
+ system.upgrade(
+ config.schema.validate({
+ appenders: { default: { kind: 'console', layout: { kind: 'json' } } },
+ root: { level: 'info' },
+ })
+ );
+
+ system
+ .get('tests', 'child', 'grandchild')
+ .debug('tests.child.grandchild log to default and custom!');
+
+ // Customized context is logged in both appender formats still
+ expect(mockConsoleLog).toHaveBeenCalledTimes(2);
+ expect(JSON.parse(mockConsoleLog.mock.calls[0][0])).toMatchObject({
+ context: 'tests.child.grandchild',
+ message: 'tests.child.grandchild log to default and custom!',
+ level: 'DEBUG',
+ });
+ expect(mockConsoleLog.mock.calls[1][0]).toMatchInlineSnapshot(
+ `"[DEBUG][tests.child.grandchild] tests.child.grandchild log to default and custom!"`
+ );
+});
+
+test('subsequent calls to setContextConfig() for the same context override the previous config', () => {
+ system.upgrade(
+ config.schema.validate({
+ appenders: { default: { kind: 'console', layout: { kind: 'json' } } },
+ root: { level: 'info' },
+ })
+ );
+
+ system.setContextConfig(['tests', 'child'], {
+ appenders: new Map([
+ [
+ 'custom',
+ { kind: 'console', layout: { kind: 'pattern', pattern: '[%level][%logger] %message' } },
+ ],
+ ]),
+ loggers: [{ context: 'grandchild', appenders: ['default', 'custom'], level: 'debug' }],
+ });
+
+ // Call again, this time with level: 'warn' and a different pattern
+ system.setContextConfig(['tests', 'child'], {
+ appenders: new Map([
+ [
+ 'custom',
+ {
+ kind: 'console',
+ layout: { kind: 'pattern', pattern: '[%level][%logger] second pattern! %message' },
+ },
+ ],
+ ]),
+ loggers: [{ context: 'grandchild', appenders: ['default', 'custom'], level: 'warn' }],
+ });
+
+ const logger = system.get('tests', 'child', 'grandchild');
+ logger.debug('this should not show anywhere!');
+ logger.warn('tests.child.grandchild log to default and custom!');
+
+ // Only the warn log should have been logged
+ expect(mockConsoleLog).toHaveBeenCalledTimes(2);
+ expect(JSON.parse(mockConsoleLog.mock.calls[0][0])).toMatchObject({
+ context: 'tests.child.grandchild',
+ message: 'tests.child.grandchild log to default and custom!',
+ level: 'WARN',
+ });
+ expect(mockConsoleLog.mock.calls[1][0]).toMatchInlineSnapshot(
+ `"[WARN ][tests.child.grandchild] second pattern! tests.child.grandchild log to default and custom!"`
+ );
+});
+
+test('subsequent calls to setContextConfig() for the same context can disable the previous config', () => {
+ system.upgrade(
+ config.schema.validate({
+ appenders: { default: { kind: 'console', layout: { kind: 'json' } } },
+ root: { level: 'info' },
+ })
+ );
+
+ system.setContextConfig(['tests', 'child'], {
+ appenders: new Map([
+ [
+ 'custom',
+ { kind: 'console', layout: { kind: 'pattern', pattern: '[%level][%logger] %message' } },
+ ],
+ ]),
+ loggers: [{ context: 'grandchild', appenders: ['default', 'custom'], level: 'debug' }],
+ });
+
+ // Call again, this time no customizations (effectively disabling)
+ system.setContextConfig(['tests', 'child'], {});
+
+ const logger = system.get('tests', 'child', 'grandchild');
+ logger.debug('this should not show anywhere!');
+ logger.warn('tests.child.grandchild log to default!');
+
+ // Only the warn log should have been logged once on the default appender
+ expect(mockConsoleLog).toHaveBeenCalledTimes(1);
+ expect(JSON.parse(mockConsoleLog.mock.calls[0][0])).toMatchObject({
+ context: 'tests.child.grandchild',
+ message: 'tests.child.grandchild log to default!',
+ level: 'WARN',
+ });
+});
diff --git a/src/core/server/logging/logging_system.ts b/src/core/server/logging/logging_system.ts
new file mode 100644
index 0000000000000..0bab9534d2d05
--- /dev/null
+++ b/src/core/server/logging/logging_system.ts
@@ -0,0 +1,185 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { Appenders, DisposableAppender } from './appenders/appenders';
+import { BufferAppender } from './appenders/buffer/buffer_appender';
+import { LogLevel } from './log_level';
+import { BaseLogger, Logger } from './logger';
+import { LoggerAdapter } from './logger_adapter';
+import { LoggerFactory } from './logger_factory';
+import {
+ LoggingConfigType,
+ LoggerConfigType,
+ LoggingConfig,
+ LoggerContextConfigType,
+ LoggerContextConfigInput,
+ loggerContextConfigSchema,
+} from './logging_config';
+
+export type ILoggingSystem = PublicMethodsOf;
+
+/**
+ * System that is responsible for maintaining loggers and logger appenders.
+ * @internal
+ */
+export class LoggingSystem implements LoggerFactory {
+ /** The configuration set by the user. */
+ private baseConfig?: LoggingConfig;
+ /** The fully computed configuration extended by context-specific configurations set programmatically */
+ private computedConfig?: LoggingConfig;
+ private readonly appenders: Map = new Map();
+ private readonly bufferAppender = new BufferAppender();
+ private readonly loggers: Map = new Map();
+ private readonly contextConfigs = new Map();
+
+ 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)));
+ }
+ return this.loggers.get(context)!;
+ }
+
+ /**
+ * Safe wrapper that allows passing logging service as immutable LoggerFactory.
+ */
+ public asLoggerFactory(): LoggerFactory {
+ return { get: (...contextParts: string[]) => this.get(...contextParts) };
+ }
+
+ /**
+ * Updates all current active loggers with the new config values.
+ * @param rawConfig New config instance.
+ */
+ public upgrade(rawConfig: LoggingConfigType) {
+ const config = new LoggingConfig(rawConfig)!;
+ this.applyBaseConfig(config);
+ }
+
+ /**
+ * Customizes the logging config for a specific context.
+ *
+ * @remarks
+ * Assumes that that the `context` property of the individual items in `rawConfig.loggers`
+ * are relative to the `baseContextParts`.
+ *
+ * @example
+ * Customize the configuration for the plugins.data.search context.
+ * ```ts
+ * loggingSystem.setContextConfig(
+ * ['plugins', 'data'],
+ * {
+ * loggers: [{ context: 'search', appenders: ['default'] }]
+ * }
+ * )
+ * ```
+ *
+ * @param baseContextParts
+ * @param rawConfig
+ */
+ public setContextConfig(baseContextParts: string[], rawConfig: LoggerContextConfigInput) {
+ const context = LoggingConfig.getLoggerContext(baseContextParts);
+ const contextConfig = loggerContextConfigSchema.validate(rawConfig);
+ this.contextConfigs.set(context, {
+ ...contextConfig,
+ // Automatically prepend the base context to the logger sub-contexts
+ loggers: contextConfig.loggers.map((l) => ({
+ ...l,
+ context: LoggingConfig.getLoggerContext([context, l.context]),
+ })),
+ });
+
+ // If we already have a base config, apply the config. If not, custom context configs
+ // will be picked up on next call to `upgrade`.
+ if (this.baseConfig) {
+ this.applyBaseConfig(this.baseConfig);
+ }
+ }
+
+ /**
+ * 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.
+ * @returns Promise that is resolved once all loggers are successfully disposed.
+ */
+ public async stop() {
+ await Promise.all([...this.appenders.values()].map((a) => a.dispose()));
+
+ await this.bufferAppender.dispose();
+
+ this.appenders.clear();
+ this.loggers.clear();
+ }
+
+ private createLogger(context: string, config: LoggingConfig | undefined) {
+ if (config === undefined) {
+ // If we don't have config yet, use `buffered` appender that will store all logged messages in the memory
+ // until the config is ready.
+ return new BaseLogger(context, LogLevel.All, [this.bufferAppender], this.asLoggerFactory());
+ }
+
+ const { level, appenders } = this.getLoggerConfigByContext(config, context);
+ const loggerLevel = LogLevel.fromId(level);
+ const loggerAppenders = appenders.map((appenderKey) => this.appenders.get(appenderKey)!);
+
+ return new BaseLogger(context, loggerLevel, loggerAppenders, this.asLoggerFactory());
+ }
+
+ private getLoggerConfigByContext(config: LoggingConfig, context: string): LoggerConfigType {
+ const loggerConfig = config.loggers.get(context);
+ if (loggerConfig !== undefined) {
+ return loggerConfig;
+ }
+
+ // If we don't have configuration for the specified context and it's the "nested" one (eg. `foo.bar.baz`),
+ // let's move up to the parent context (eg. `foo.bar`) and check if it has config we can rely on. Otherwise
+ // we fallback to the `root` context that should always be defined (enforced by configuration schema).
+ return this.getLoggerConfigByContext(config, LoggingConfig.getParentLoggerContext(context));
+ }
+
+ private applyBaseConfig(newBaseConfig: LoggingConfig) {
+ const computedConfig = [...this.contextConfigs.values()].reduce(
+ (baseConfig, contextConfig) => baseConfig.extend(contextConfig),
+ newBaseConfig
+ );
+
+ // Appenders must be reset, so we first dispose of the current ones, then
+ // build up a new set of appenders.
+ for (const appender of this.appenders.values()) {
+ appender.dispose();
+ }
+ this.appenders.clear();
+
+ for (const [appenderKey, appenderConfig] of computedConfig.appenders) {
+ this.appenders.set(appenderKey, Appenders.create(appenderConfig));
+ }
+
+ for (const [loggerKey, loggerAdapter] of this.loggers) {
+ loggerAdapter.updateLogger(this.createLogger(loggerKey, computedConfig));
+ }
+
+ // We keep a reference to the base config so we can properly extend it
+ // on each config change.
+ this.baseConfig = newBaseConfig;
+ this.computedConfig = computedConfig;
+
+ // Re-log all buffered log records with newly configured appenders.
+ for (const logRecord of this.bufferAppender.flush()) {
+ this.get(logRecord.context).log(logRecord);
+ }
+ }
+}
diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts
index f3ae5462f1631..0770e8843e2f6 100644
--- a/src/core/server/mocks.ts
+++ b/src/core/server/mocks.ts
@@ -19,6 +19,7 @@
import { of } from 'rxjs';
import { duration } from 'moment';
import { PluginInitializerContext, CoreSetup, CoreStart, StartServicesAccessor } from '.';
+import { loggingSystemMock } from './logging/logging_system.mock';
import { loggingServiceMock } from './logging/logging_service.mock';
import { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service.mock';
import { httpServiceMock } from './http/http_service.mock';
@@ -42,7 +43,7 @@ export { sessionStorageMock } from './http/cookie_session_storage.mocks';
export { configServiceMock } from './config/config_service.mock';
export { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service.mock';
export { httpServiceMock } from './http/http_service.mock';
-export { loggingServiceMock } from './logging/logging_service.mock';
+export { loggingSystemMock } from './logging/logging_system.mock';
export { savedObjectsRepositoryMock } from './saved_objects/service/lib/repository.mock';
export { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock';
export { typeRegistryMock as savedObjectsTypeRegistryMock } from './saved_objects/saved_objects_type_registry.mock';
@@ -78,7 +79,7 @@ export function pluginInitializerContextConfigMock(config: T) {
function pluginInitializerContextMock(config: T = {} as T) {
const mock: PluginInitializerContext = {
opaqueId: Symbol(),
- logger: loggingServiceMock.create(),
+ logger: loggingSystemMock.create(),
env: {
mode: {
dev: true,
@@ -130,6 +131,7 @@ function createCoreSetupMock({
metrics: metricsServiceMock.createSetupContract(),
uiSettings: uiSettingsMock,
uuid: uuidServiceMock.createSetupContract(),
+ logging: loggingServiceMock.createSetupContract(),
getStartServices: jest
.fn, object, any]>, []>()
.mockResolvedValue([createCoreStartMock(), pluginStartDeps, pluginStartContract]),
@@ -163,6 +165,7 @@ function createInternalCoreSetupMock() {
httpResources: httpResourcesMock.createSetupContract(),
rendering: renderingMock.createSetupContract(),
uiSettings: uiSettingsServiceMock.createSetupContract(),
+ logging: loggingServiceMock.createInternalSetupContract(),
};
return setupDeps;
}
diff --git a/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts b/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts
index 979accb1f769e..5ffdef88104c8 100644
--- a/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts
+++ b/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts
@@ -20,12 +20,12 @@
import { PluginDiscoveryErrorType } from './plugin_discovery_error';
import { mockReadFile } from './plugin_manifest_parser.test.mocks';
-import { loggingServiceMock } from '../../logging/logging_service.mock';
+import { loggingSystemMock } from '../../logging/logging_system.mock';
import { resolve } from 'path';
import { parseManifest } from './plugin_manifest_parser';
-const logger = loggingServiceMock.createLogger();
+const logger = loggingSystemMock.createLogger();
const pluginPath = resolve('path', 'existent-dir');
const pluginManifestPath = resolve(pluginPath, 'kibana.json');
const packageInfo = {
@@ -105,9 +105,9 @@ test('logs warning if pluginId is not in camelCase format', async () => {
cb(null, Buffer.from(JSON.stringify({ id: 'some_name', version: 'kibana', server: true })));
});
- expect(loggingServiceMock.collect(logger).warn).toHaveLength(0);
+ expect(loggingSystemMock.collect(logger).warn).toHaveLength(0);
await parseManifest(pluginPath, packageInfo, logger);
- expect(loggingServiceMock.collect(logger).warn).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).warn).toMatchInlineSnapshot(`
Array [
Array [
"Expect plugin \\"id\\" in camelCase, but found: some_name",
diff --git a/src/core/server/plugins/discovery/plugins_discovery.test.ts b/src/core/server/plugins/discovery/plugins_discovery.test.ts
index 73f274957cbc4..1c42f5dcfc7a7 100644
--- a/src/core/server/plugins/discovery/plugins_discovery.test.ts
+++ b/src/core/server/plugins/discovery/plugins_discovery.test.ts
@@ -19,7 +19,7 @@
import { mockPackage, mockReaddir, mockReadFile, mockStat } from './plugins_discovery.test.mocks';
import { rawConfigServiceMock } from '../../config/raw_config_service.mock';
-import { loggingServiceMock } from '../../logging/logging_service.mock';
+import { loggingSystemMock } from '../../logging/logging_system.mock';
import { resolve } from 'path';
import { first, map, toArray } from 'rxjs/operators';
@@ -37,7 +37,7 @@ const TEST_PLUGIN_SEARCH_PATHS = {
};
const TEST_EXTRA_PLUGIN_PATH = resolve(process.cwd(), 'my-extra-plugin');
-const logger = loggingServiceMock.create();
+const logger = loggingSystemMock.create();
beforeEach(() => {
mockReaddir.mockImplementation((path, cb) => {
@@ -221,7 +221,7 @@ test('logs a warning about --plugin-path when used in development', async () =>
logger,
});
- expect(loggingServiceMock.collect(logger).warn).toEqual([
+ expect(loggingSystemMock.collect(logger).warn).toEqual([
[
`Explicit plugin paths [${TEST_EXTRA_PLUGIN_PATH}] should only be used in development. Relative imports may not work properly in production.`,
],
@@ -263,5 +263,5 @@ test('does not log a warning about --plugin-path when used in production', async
logger,
});
- expect(loggingServiceMock.collect(logger).warn).toEqual([]);
+ expect(loggingSystemMock.collect(logger).warn).toEqual([]);
});
diff --git a/src/core/server/plugins/integration_tests/plugins_service.test.ts b/src/core/server/plugins/integration_tests/plugins_service.test.ts
index 04f570cca489b..e676c789449ca 100644
--- a/src/core/server/plugins/integration_tests/plugins_service.test.ts
+++ b/src/core/server/plugins/integration_tests/plugins_service.test.ts
@@ -27,13 +27,13 @@ import { getEnvOptions } from '../../config/__mocks__/env';
import { BehaviorSubject, from } from 'rxjs';
import { rawConfigServiceMock } from '../../config/raw_config_service.mock';
import { config } from '../plugins_config';
-import { loggingServiceMock } from '../../logging/logging_service.mock';
+import { loggingSystemMock } from '../../logging/logging_system.mock';
import { coreMock } from '../../mocks';
import { Plugin } from '../types';
import { PluginWrapper } from '../plugin';
describe('PluginsService', () => {
- const logger = loggingServiceMock.create();
+ const logger = loggingSystemMock.create();
let pluginsService: PluginsService;
const createPlugin = (
diff --git a/src/core/server/plugins/plugin.test.ts b/src/core/server/plugins/plugin.test.ts
index 8d82d96f949c7..ec0a3986b4877 100644
--- a/src/core/server/plugins/plugin.test.ts
+++ b/src/core/server/plugins/plugin.test.ts
@@ -26,14 +26,14 @@ import { getEnvOptions } from '../config/__mocks__/env';
import { CoreContext } from '../core_context';
import { coreMock } from '../mocks';
import { configServiceMock } from '../config/config_service.mock';
-import { loggingServiceMock } from '../logging/logging_service.mock';
+import { loggingSystemMock } from '../logging/logging_system.mock';
import { PluginWrapper } from './plugin';
import { PluginManifest } from './types';
import { createPluginInitializerContext, createPluginSetupContext } from './plugin_context';
const mockPluginInitializer = jest.fn();
-const logger = loggingServiceMock.create();
+const logger = loggingSystemMock.create();
jest.doMock(
join('plugin-with-initializer-path', 'server'),
() => ({ plugin: mockPluginInitializer }),
diff --git a/src/core/server/plugins/plugin.ts b/src/core/server/plugins/plugin.ts
index d7cfaa14d2343..2e5881c651843 100644
--- a/src/core/server/plugins/plugin.ts
+++ b/src/core/server/plugins/plugin.ts
@@ -95,8 +95,6 @@ export class PluginWrapper<
public async setup(setupContext: CoreSetup, plugins: TPluginsSetup) {
this.instance = this.createPluginInstance();
- this.log.debug('Setting up plugin');
-
return this.instance.setup(setupContext, plugins);
}
@@ -112,8 +110,6 @@ export class PluginWrapper<
throw new Error(`Plugin "${this.name}" can't be started since it isn't set up.`);
}
- this.log.debug('Starting plugin');
-
const startContract = await this.instance.start(startContext, plugins);
this.startDependencies$.next([startContext, plugins, startContract]);
return startContract;
@@ -127,8 +123,6 @@ export class PluginWrapper<
throw new Error(`Plugin "${this.name}" can't be stopped since it isn't set up.`);
}
- this.log.info('Stopping plugin');
-
if (typeof this.instance.stop === 'function') {
await this.instance.stop();
}
diff --git a/src/core/server/plugins/plugin_context.test.ts b/src/core/server/plugins/plugin_context.test.ts
index 54350d96984b4..69b354661abc9 100644
--- a/src/core/server/plugins/plugin_context.test.ts
+++ b/src/core/server/plugins/plugin_context.test.ts
@@ -22,14 +22,14 @@ import { first } from 'rxjs/operators';
import { createPluginInitializerContext } from './plugin_context';
import { CoreContext } from '../core_context';
import { Env } from '../config';
-import { loggingServiceMock } from '../logging/logging_service.mock';
+import { loggingSystemMock } from '../logging/logging_system.mock';
import { rawConfigServiceMock } from '../config/raw_config_service.mock';
import { getEnvOptions } from '../config/__mocks__/env';
import { PluginManifest } from './types';
import { Server } from '../server';
import { fromRoot } from '../utils';
-const logger = loggingServiceMock.create();
+const logger = loggingSystemMock.create();
let coreId: symbol;
let env: Env;
diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts
index 31e36db49223a..32bc8dc088cad 100644
--- a/src/core/server/plugins/plugin_context.ts
+++ b/src/core/server/plugins/plugin_context.ts
@@ -166,6 +166,9 @@ export function createPluginSetupContext(
csp: deps.http.csp,
getServerInfo: deps.http.getServerInfo,
},
+ logging: {
+ configure: (config$) => deps.logging.configure(['plugins', plugin.name], config$),
+ },
metrics: {
getOpsMetrics$: deps.metrics.getOpsMetrics$,
},
diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts
index 6f8d15838641f..c277dc85e5e04 100644
--- a/src/core/server/plugins/plugins_service.test.ts
+++ b/src/core/server/plugins/plugins_service.test.ts
@@ -28,7 +28,7 @@ import { ConfigPath, ConfigService, Env } from '../config';
import { rawConfigServiceMock } from '../config/raw_config_service.mock';
import { getEnvOptions } from '../config/__mocks__/env';
import { coreMock } from '../mocks';
-import { loggingServiceMock } from '../logging/logging_service.mock';
+import { loggingSystemMock } from '../logging/logging_system.mock';
import { PluginDiscoveryError } from './discovery';
import { PluginWrapper } from './plugin';
import { PluginsService } from './plugins_service';
@@ -47,7 +47,7 @@ let env: Env;
let mockPluginSystem: jest.Mocked;
const setupDeps = coreMock.createInternalSetup();
-const logger = loggingServiceMock.create();
+const logger = loggingSystemMock.create();
expect.addSnapshotSerializer(createAbsolutePathSerializer());
@@ -138,7 +138,7 @@ describe('PluginsService', () => {
[Error: Failed to initialize plugins:
Invalid JSON (invalid-manifest, path-1)]
`);
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: Invalid JSON (invalid-manifest, path-1)],
@@ -159,7 +159,7 @@ describe('PluginsService', () => {
[Error: Failed to initialize plugins:
Incompatible version (incompatible-version, path-3)]
`);
- expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
Array [
Array [
[Error: Incompatible version (incompatible-version, path-3)],
@@ -238,7 +238,7 @@ describe('PluginsService', () => {
expect(mockPluginSystem.setupPlugins).toHaveBeenCalledTimes(1);
expect(mockPluginSystem.setupPlugins).toHaveBeenCalledWith(setupDeps);
- expect(loggingServiceMock.collect(logger).info).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).info).toMatchInlineSnapshot(`
Array [
Array [
"Plugin \\"explicitly-disabled-plugin\\" is disabled.",
@@ -360,7 +360,7 @@ describe('PluginsService', () => {
{ coreId, env, logger, configService }
);
- const logs = loggingServiceMock.collect(logger);
+ const logs = loggingSystemMock.collect(logger);
expect(logs.info).toHaveLength(0);
expect(logs.error).toHaveLength(0);
});
diff --git a/src/core/server/plugins/plugins_system.test.ts b/src/core/server/plugins/plugins_system.test.ts
index 70983e4fd087b..a40df70228ff3 100644
--- a/src/core/server/plugins/plugins_system.test.ts
+++ b/src/core/server/plugins/plugins_system.test.ts
@@ -28,7 +28,7 @@ import { Env } from '../config';
import { getEnvOptions } from '../config/__mocks__/env';
import { CoreContext } from '../core_context';
import { configServiceMock } from '../config/config_service.mock';
-import { loggingServiceMock } from '../logging/logging_service.mock';
+import { loggingSystemMock } from '../logging/logging_system.mock';
import { PluginWrapper } from './plugin';
import { PluginName } from './types';
@@ -36,7 +36,7 @@ import { PluginsSystem } from './plugins_system';
import { coreMock } from '../mocks';
import { Logger } from '../logging';
-const logger = loggingServiceMock.create();
+const logger = loggingSystemMock.create();
function createPlugin(
id: string,
{
diff --git a/src/core/server/root/index.test.mocks.ts b/src/core/server/root/index.test.mocks.ts
index 1d3add66d7c22..ef4a40fa3db2d 100644
--- a/src/core/server/root/index.test.mocks.ts
+++ b/src/core/server/root/index.test.mocks.ts
@@ -17,10 +17,10 @@
* under the License.
*/
-import { loggingServiceMock } from '../logging/logging_service.mock';
-export const logger = loggingServiceMock.create();
-jest.doMock('../logging/logging_service', () => ({
- LoggingService: jest.fn(() => logger),
+import { loggingSystemMock } from '../logging/logging_system.mock';
+export const logger = loggingSystemMock.create();
+jest.doMock('../logging/logging_system', () => ({
+ LoggingSystem: jest.fn(() => logger),
}));
import { configServiceMock } from '../config/config_service.mock';
diff --git a/src/core/server/root/index.ts b/src/core/server/root/index.ts
index d6d0c641e00b0..5e9722de03dee 100644
--- a/src/core/server/root/index.ts
+++ b/src/core/server/root/index.ts
@@ -21,7 +21,7 @@ import { ConnectableObservable, Subscription } from 'rxjs';
import { first, map, publishReplay, switchMap, tap } from 'rxjs/operators';
import { Env, RawConfigurationProvider } from '../config';
-import { Logger, LoggerFactory, LoggingConfigType, LoggingService } from '../logging';
+import { Logger, LoggerFactory, LoggingConfigType, LoggingSystem } from '../logging';
import { Server } from '../server';
/**
@@ -30,7 +30,7 @@ import { Server } from '../server';
export class Root {
public readonly logger: LoggerFactory;
private readonly log: Logger;
- private readonly loggingService: LoggingService;
+ private readonly loggingSystem: LoggingSystem;
private readonly server: Server;
private loggingConfigSubscription?: Subscription;
@@ -39,10 +39,10 @@ export class Root {
env: Env,
private readonly onShutdown?: (reason?: Error | string) => void
) {
- this.loggingService = new LoggingService();
- this.logger = this.loggingService.asLoggerFactory();
+ this.loggingSystem = new LoggingSystem();
+ this.logger = this.loggingSystem.asLoggerFactory();
this.log = this.logger.get('root');
- this.server = new Server(rawConfigProvider, env, this.logger);
+ this.server = new Server(rawConfigProvider, env, this.loggingSystem);
}
public async setup() {
@@ -86,7 +86,7 @@ export class Root {
this.loggingConfigSubscription.unsubscribe();
this.loggingConfigSubscription = undefined;
}
- await this.loggingService.stop();
+ await this.loggingSystem.stop();
if (this.onShutdown !== undefined) {
this.onShutdown(reason);
@@ -99,7 +99,7 @@ export class Root {
const update$ = configService.getConfig$().pipe(
// always read the logging config when the underlying config object is re-read
switchMap(() => configService.atPath('logging')),
- map((config) => this.loggingService.upgrade(config)),
+ map((config) => this.loggingSystem.upgrade(config)),
// This specifically console.logs because we were not able to configure the logger.
// eslint-disable-next-line no-console
tap({ error: (err) => console.error('Configuring logger failed:', err) }),
diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts
index a364710322524..6287d47f99f62 100644
--- a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts
+++ b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts
@@ -20,11 +20,11 @@
import _ from 'lodash';
import { SavedObjectUnsanitizedDoc } from '../../serialization';
import { DocumentMigrator } from './document_migrator';
-import { loggingServiceMock } from '../../../logging/logging_service.mock';
+import { loggingSystemMock } from '../../../logging/logging_system.mock';
import { SavedObjectsType } from '../../types';
import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry';
-const mockLoggerFactory = loggingServiceMock.create();
+const mockLoggerFactory = loggingSystemMock.create();
const mockLogger = mockLoggerFactory.get('mock logger');
const createRegistry = (...types: Array>) => {
@@ -572,7 +572,7 @@ describe('DocumentMigrator', () => {
expect('Did not throw').toEqual('But it should have!');
} catch (error) {
expect(error.message).toMatch(/Dang diggity!/);
- const warning = loggingServiceMock.collect(mockLoggerFactory).warn[0][0];
+ const warning = loggingSystemMock.collect(mockLoggerFactory).warn[0][0];
expect(warning).toContain(JSON.stringify(failedDoc));
expect(warning).toContain('dog:1.2.3');
}
@@ -601,8 +601,8 @@ describe('DocumentMigrator', () => {
migrationVersion: {},
};
migrator.migrate(doc);
- expect(loggingServiceMock.collect(mockLoggerFactory).info[0][0]).toEqual(logTestMsg);
- expect(loggingServiceMock.collect(mockLoggerFactory).warn[1][0]).toEqual(logTestMsg);
+ expect(loggingSystemMock.collect(mockLoggerFactory).info[0][0]).toEqual(logTestMsg);
+ expect(loggingSystemMock.collect(mockLoggerFactory).warn[1][0]).toEqual(logTestMsg);
});
test('extracts the latest migration version info', () => {
diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts
index 392089c69f5a0..86c79cbfb5824 100644
--- a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts
+++ b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts
@@ -21,7 +21,7 @@ import _ from 'lodash';
import { SavedObjectUnsanitizedDoc, SavedObjectsSerializer } from '../../serialization';
import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry';
import { IndexMigrator } from './index_migrator';
-import { loggingServiceMock } from '../../../logging/logging_service.mock';
+import { loggingSystemMock } from '../../../logging/logging_system.mock';
describe('IndexMigrator', () => {
let testOpts: any;
@@ -31,7 +31,7 @@ describe('IndexMigrator', () => {
batchSize: 10,
callCluster: jest.fn(),
index: '.kibana',
- log: loggingServiceMock.create().get(),
+ log: loggingSystemMock.create().get(),
mappingProperties: {},
pollInterval: 1,
scrollDuration: '1m',
diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts
index 7a5c044924d0e..01b0d1cd0ba3a 100644
--- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts
+++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts
@@ -19,7 +19,7 @@
import { take } from 'rxjs/operators';
import { KibanaMigratorOptions, KibanaMigrator } from './kibana_migrator';
-import { loggingServiceMock } from '../../../logging/logging_service.mock';
+import { loggingSystemMock } from '../../../logging/logging_system.mock';
import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry';
import { SavedObjectsType } from '../../types';
@@ -110,7 +110,7 @@ describe('KibanaMigrator', () => {
function mockOptions(): KibanaMigratorOptions {
const callCluster = jest.fn();
return {
- logger: loggingServiceMock.create().get(),
+ logger: loggingSystemMock.create().get(),
kibanaVersion: '8.2.3',
savedObjectValidations: {},
typeRegistry: createRegistry([
diff --git a/src/core/server/saved_objects/routes/integration_tests/log_legacy_import.test.ts b/src/core/server/saved_objects/routes/integration_tests/log_legacy_import.test.ts
index 0fe07245dda20..8d021580da36c 100644
--- a/src/core/server/saved_objects/routes/integration_tests/log_legacy_import.test.ts
+++ b/src/core/server/saved_objects/routes/integration_tests/log_legacy_import.test.ts
@@ -20,7 +20,7 @@
import supertest from 'supertest';
import { UnwrapPromise } from '@kbn/utility-types';
import { registerLogLegacyImportRoute } from '../log_legacy_import';
-import { loggingServiceMock } from '../../../logging/logging_service.mock';
+import { loggingSystemMock } from '../../../logging/logging_system.mock';
import { setupServer } from '../test_utils';
type setupServerReturn = UnwrapPromise>;
@@ -28,11 +28,11 @@ type setupServerReturn = UnwrapPromise>;
describe('POST /api/saved_objects/_log_legacy_import', () => {
let server: setupServerReturn['server'];
let httpSetup: setupServerReturn['httpSetup'];
- let logger: ReturnType;
+ let logger: ReturnType;
beforeEach(async () => {
({ server, httpSetup } = await setupServer());
- logger = loggingServiceMock.createLogger();
+ logger = loggingSystemMock.createLogger();
const router = httpSetup.createRouter('/api/saved_objects/');
registerLogLegacyImportRoute(router, logger);
@@ -50,7 +50,7 @@ describe('POST /api/saved_objects/_log_legacy_import', () => {
.expect(200);
expect(result.body).toEqual({ success: true });
- expect(loggingServiceMock.collect(logger).warn).toMatchInlineSnapshot(`
+ expect(loggingSystemMock.collect(logger).warn).toMatchInlineSnapshot(`
Array [
Array [
"Importing saved objects from a .json file has been deprecated",
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 9dc3ac9b94d96..4d6316fceb568 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -388,6 +388,11 @@ export interface APICaller {
(endpoint: string, clientParams?: Record, options?: CallAPIOptions): Promise;
}
+// Warning: (ae-forgotten-export) The symbol "appendersSchema" needs to be exported by the entry point index.d.ts
+//
+// @public (undocumented)
+export type AppenderConfigType = TypeOf;
+
// @public
export function assertNever(x: never): never;
@@ -574,6 +579,72 @@ export const config: {
ignoreVersionMismatch: import("@kbn/config-schema/target/types/types").ConditionalType;
}>;
};
+ logging: {
+ appenders: import("@kbn/config-schema").Type | Readonly<{
+ pattern?: string | undefined;
+ highlight?: boolean | undefined;
+ } & {
+ kind: "pattern";
+ }>;
+ kind: "console";
+ }> | Readonly<{} & {
+ path: string;
+ layout: Readonly<{} & {
+ kind: "json";
+ }> | Readonly<{
+ pattern?: string | undefined;
+ highlight?: boolean | undefined;
+ } & {
+ kind: "pattern";
+ }>;
+ kind: "file";
+ }> | Readonly<{
+ legacyLoggingConfig?: any;
+ } & {
+ kind: "legacy-appender";
+ }>>;
+ loggers: import("@kbn/config-schema").ObjectType<{
+ appenders: import("@kbn/config-schema").Type;
+ context: import("@kbn/config-schema").Type;
+ level: import("@kbn/config-schema").Type;
+ }>;
+ loggerContext: import("@kbn/config-schema").ObjectType<{
+ appenders: import("@kbn/config-schema").Type
);
}
-
-Agg.propTypes = {
- disableDelete: PropTypes.bool,
- fields: PropTypes.object,
- model: PropTypes.object,
- onAdd: PropTypes.func,
- onChange: PropTypes.func,
- onDelete: PropTypes.func,
- panel: PropTypes.object,
- series: PropTypes.object,
- siblings: PropTypes.array,
- uiRestrictions: PropTypes.object,
- dragHandleProps: PropTypes.object,
-};
diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_row.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_row.tsx
similarity index 86%
rename from src/plugins/vis_type_timeseries/public/application/components/aggs/agg_row.js
rename to src/plugins/vis_type_timeseries/public/application/components/aggs/agg_row.tsx
index a2f1640904dd0..0363ba486a775 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_row.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_row.tsx
@@ -17,15 +17,26 @@
* under the License.
*/
-import PropTypes from 'prop-types';
import React from 'react';
import { last } from 'lodash';
-import { AddDeleteButtons } from '../add_delete_buttons';
import { EuiIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
-import { SeriesDragHandler } from '../series_drag_handler';
import { i18n } from '@kbn/i18n';
+import { AddDeleteButtons } from '../add_delete_buttons';
+import { SeriesDragHandler } from '../series_drag_handler';
+import { MetricsItemsSchema } from '../../../../common/types';
+import { DragHandleProps } from '../../../types';
-export function AggRow(props) {
+interface AggRowProps {
+ disableDelete: boolean;
+ model: MetricsItemsSchema;
+ siblings: MetricsItemsSchema[];
+ dragHandleProps: DragHandleProps;
+ children: React.ReactNode;
+ onAdd: () => void;
+ onDelete: () => void;
+}
+
+export function AggRow(props: AggRowProps) {
let iconType = 'eyeClosed';
let iconColor = 'subdued';
const lastSibling = last(props.siblings);
@@ -71,12 +82,3 @@ export function AggRow(props) {
);
}
-
-AggRow.propTypes = {
- disableDelete: PropTypes.bool,
- model: PropTypes.object,
- onAdd: PropTypes.func,
- onDelete: PropTypes.func,
- siblings: PropTypes.array,
- dragHandleProps: PropTypes.object,
-};
diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.tsx
similarity index 88%
rename from src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.js
rename to src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.tsx
index 7ff6b6eb56692..6fa1a2adaa08e 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.tsx
@@ -17,14 +17,17 @@
* under the License.
*/
-import PropTypes from 'prop-types';
import React from 'react';
-import { EuiComboBox } from '@elastic/eui';
+import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { injectI18n } from '@kbn/i18n/react';
+// @ts-ignore
import { isMetricEnabled } from '../../lib/check_ui_restrictions';
+import { MetricsItemsSchema } from '../../../../common/types';
+import { TimeseriesUIRestrictions } from '../../../../common/ui_restrictions';
-const metricAggs = [
+type AggSelectOption = EuiComboBoxOptionOption;
+
+const metricAggs: AggSelectOption[] = [
{
label: i18n.translate('visTypeTimeseries.aggSelect.metricsAggs.averageLabel', {
defaultMessage: 'Average',
@@ -123,7 +126,7 @@ const metricAggs = [
},
];
-const pipelineAggs = [
+const pipelineAggs: AggSelectOption[] = [
{
label: i18n.translate('visTypeTimeseries.aggSelect.pipelineAggs.bucketScriptLabel', {
defaultMessage: 'Bucket Script',
@@ -162,7 +165,7 @@ const pipelineAggs = [
},
];
-const siblingAggs = [
+const siblingAggs: AggSelectOption[] = [
{
label: i18n.translate('visTypeTimeseries.aggSelect.siblingAggs.overallAverageLabel', {
defaultMessage: 'Overall Average',
@@ -207,7 +210,7 @@ const siblingAggs = [
},
];
-const specialAggs = [
+const specialAggs: AggSelectOption[] = [
{
label: i18n.translate('visTypeTimeseries.aggSelect.specialAggs.seriesAggLabel', {
defaultMessage: 'Series Agg',
@@ -224,14 +227,23 @@ const specialAggs = [
const allAggOptions = [...metricAggs, ...pipelineAggs, ...siblingAggs, ...specialAggs];
-function filterByPanelType(panelType) {
- return (agg) => {
+function filterByPanelType(panelType: string) {
+ return (agg: AggSelectOption) => {
if (panelType === 'table') return agg.value !== 'series_agg';
return true;
};
}
-function AggSelectUi(props) {
+interface AggSelectUiProps {
+ id: string;
+ panelType: string;
+ siblings: MetricsItemsSchema[];
+ value: string;
+ uiRestrictions?: TimeseriesUIRestrictions;
+ onChange: (currentlySelectedOptions: AggSelectOption[]) => void;
+}
+
+export function AggSelect(props: AggSelectUiProps) {
const { siblings, panelType, value, onChange, uiRestrictions, ...rest } = props;
const selectedOptions = allAggOptions.filter((option) => {
@@ -242,11 +254,11 @@ function AggSelectUi(props) {
if (siblings.length <= 1) enablePipelines = false;
- let options;
+ let options: EuiComboBoxOptionOption[];
if (panelType === 'metrics') {
options = metricAggs;
} else {
- const disableSiblingAggs = (agg) => ({
+ const disableSiblingAggs = (agg: AggSelectOption) => ({
...agg,
disabled: !enablePipelines || !isMetricEnabled(agg.value, uiRestrictions),
});
@@ -282,9 +294,9 @@ function AggSelectUi(props) {
];
}
- const handleChange = (selectedOptions) => {
- if (!selectedOptions || selectedOptions.length <= 0) return;
- onChange(selectedOptions);
+ const handleChange = (currentlySelectedOptions: AggSelectOption[]) => {
+ if (!currentlySelectedOptions || currentlySelectedOptions.length <= 0) return;
+ onChange(currentlySelectedOptions);
};
return (
@@ -303,13 +315,3 @@ function AggSelectUi(props) {