Display Kibana overall status in the logs and have FTR wait for green status before running tests (#92568)

* add licensing plugin status

* simplify log message

* review comments
This commit is contained in:
Pierre Gayvallet 2021-03-05 09:55:14 +01:00 committed by GitHub
parent 1f936a1571
commit d53894aa5a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 313 additions and 18 deletions

View file

@ -38,7 +38,7 @@ export async function runKibanaServer({ procs, config, options }) {
...extendNodeOptions(installDir),
},
cwd: installDir || KIBANA_ROOT,
wait: /http server running/,
wait: /Kibana is now available/,
});
}

View file

@ -95,8 +95,6 @@ export async function runTests(options) {
try {
es = await runElasticsearch({ config, options: opts });
await runKibanaServer({ procs, config, options: opts });
// workaround until https://github.com/elastic/kibana/issues/89828 is addressed
await delay(5000);
await runFtr({ configPath, options: opts });
} finally {
try {
@ -162,7 +160,3 @@ async function silence(log, milliseconds) {
)
.toPromise();
}
async function delay(ms) {
await new Promise((resolve) => setTimeout(resolve, ms));
}

View file

@ -269,6 +269,7 @@ export class Server {
plugins: mapToObject(pluginsStart.contracts),
});
this.status.start();
await this.http.start();
startTransaction?.end();

View file

@ -0,0 +1,103 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { TestScheduler } from 'rxjs/testing';
import { ServiceStatus, ServiceStatusLevels } from './types';
import { getOverallStatusChanges } from './log_overall_status';
const getTestScheduler = () =>
new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
const createStatus = (parts: Partial<ServiceStatus> = {}): ServiceStatus => ({
level: ServiceStatusLevels.available,
summary: 'summary',
...parts,
});
describe('getOverallStatusChanges', () => {
it('emits an initial message after first overall$ emission', () => {
getTestScheduler().run(({ expectObservable, hot }) => {
const overall$ = hot<ServiceStatus>('--a', {
a: createStatus(),
});
const stop$ = hot<void>('');
const expected = '--a';
expectObservable(getOverallStatusChanges(overall$, stop$)).toBe(expected, {
a: 'Kibana is now available',
});
});
});
it('emits a new message every time the status level changes', () => {
getTestScheduler().run(({ expectObservable, hot }) => {
const overall$ = hot<ServiceStatus>('--a--b', {
a: createStatus({
level: ServiceStatusLevels.degraded,
}),
b: createStatus({
level: ServiceStatusLevels.available,
}),
});
const stop$ = hot<void>('');
const expected = '--a--b';
expectObservable(getOverallStatusChanges(overall$, stop$)).toBe(expected, {
a: 'Kibana is now degraded',
b: 'Kibana is now available (was degraded)',
});
});
});
it('does not emit when the status stays the same', () => {
getTestScheduler().run(({ expectObservable, hot }) => {
const overall$ = hot<ServiceStatus>('--a--b--c', {
a: createStatus({
level: ServiceStatusLevels.degraded,
summary: 'summary 1',
}),
b: createStatus({
level: ServiceStatusLevels.degraded,
summary: 'summary 2',
}),
c: createStatus({
level: ServiceStatusLevels.available,
summary: 'summary 2',
}),
});
const stop$ = hot<void>('');
const expected = '--a-----b';
expectObservable(getOverallStatusChanges(overall$, stop$)).toBe(expected, {
a: 'Kibana is now degraded',
b: 'Kibana is now available (was degraded)',
});
});
});
it('stops emitting once `stop$` emits', () => {
getTestScheduler().run(({ expectObservable, hot }) => {
const overall$ = hot<ServiceStatus>('--a--b', {
a: createStatus({
level: ServiceStatusLevels.degraded,
}),
b: createStatus({
level: ServiceStatusLevels.available,
}),
});
const stop$ = hot<void>('----(s|)');
const expected = '--a-|';
expectObservable(getOverallStatusChanges(overall$, stop$)).toBe(expected, {
a: 'Kibana is now degraded',
});
});
});
});

View file

@ -0,0 +1,31 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { Observable } from 'rxjs';
import { distinctUntilChanged, pairwise, startWith, takeUntil, map } from 'rxjs/operators';
import { ServiceStatus } from './types';
export const getOverallStatusChanges = (
overall$: Observable<ServiceStatus>,
stop$: Observable<void>
) => {
return overall$.pipe(
takeUntil(stop$),
distinctUntilChanged((previous, next) => {
return previous.level.toString() === next.level.toString();
}),
startWith(undefined),
pairwise(),
map(([oldStatus, newStatus]) => {
if (oldStatus) {
return `Kibana is now ${newStatus!.level.toString()} (was ${oldStatus!.level.toString()})`;
}
return `Kibana is now ${newStatus!.level.toString()}`;
})
);
};

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { Observable, combineLatest, Subscription } from 'rxjs';
import { Observable, combineLatest, Subscription, Subject } from 'rxjs';
import { map, distinctUntilChanged, shareReplay, take, debounceTime } from 'rxjs/operators';
import { isDeepStrictEqual } from 'util';
@ -25,6 +25,7 @@ import { config, StatusConfigType } from './status_config';
import { ServiceStatus, CoreStatus, InternalStatusServiceSetup } from './types';
import { getSummaryStatus } from './get_summary_status';
import { PluginsStatusService } from './plugins_status';
import { getOverallStatusChanges } from './log_overall_status';
interface SetupDeps {
elasticsearch: Pick<InternalElasticsearchServiceSetup, 'status$'>;
@ -38,7 +39,9 @@ interface SetupDeps {
export class StatusService implements CoreService<InternalStatusServiceSetup> {
private readonly logger: Logger;
private readonly config$: Observable<StatusConfigType>;
private readonly stop$ = new Subject<void>();
private overall$?: Observable<ServiceStatus>;
private pluginsStatus?: PluginsStatusService;
private overallSubscription?: Subscription;
@ -59,10 +62,7 @@ export class StatusService implements CoreService<InternalStatusServiceSetup> {
const core$ = this.setupCoreStatus({ elasticsearch, savedObjects });
this.pluginsStatus = new PluginsStatusService({ core$, pluginDependencies });
const overall$: Observable<ServiceStatus> = combineLatest([
core$,
this.pluginsStatus.getAll$(),
]).pipe(
this.overall$ = combineLatest([core$, this.pluginsStatus.getAll$()]).pipe(
// Prevent many emissions at once from dependency status resolution from making this too noisy
debounceTime(500),
map(([coreStatus, pluginsStatus]) => {
@ -78,7 +78,7 @@ export class StatusService implements CoreService<InternalStatusServiceSetup> {
);
// Create an unused subscription to ensure all underlying lazy observables are started.
this.overallSubscription = overall$.subscribe();
this.overallSubscription = this.overall$.subscribe();
const router = http.createRouter('');
registerStatusRoute({
@ -91,7 +91,7 @@ export class StatusService implements CoreService<InternalStatusServiceSetup> {
},
metrics,
status: {
overall$,
overall$: this.overall$,
plugins$: this.pluginsStatus.getAll$(),
core$,
},
@ -99,7 +99,7 @@ export class StatusService implements CoreService<InternalStatusServiceSetup> {
return {
core$,
overall$,
overall$: this.overall$,
plugins: {
set: this.pluginsStatus.set.bind(this.pluginsStatus),
getDependenciesStatus$: this.pluginsStatus.getDependenciesStatus$.bind(this.pluginsStatus),
@ -109,9 +109,19 @@ export class StatusService implements CoreService<InternalStatusServiceSetup> {
};
}
public start() {}
public start() {
if (!this.overall$) {
throw new Error('cannot call `start` before `setup`');
}
getOverallStatusChanges(this.overall$, this.stop$).subscribe((message) => {
this.logger.info(message);
});
}
public stop() {
this.stop$.next();
this.stop$.complete();
if (this.overallSubscription) {
this.overallSubscription.unsubscribe();
this.overallSubscription = undefined;

View file

@ -147,7 +147,7 @@ $WAIT_ON_BIN -i 500 -w 500 http-get://admin:changeme@localhost:$KIBANA_PORT/api/
## Workaround to wait for the http server running
## See: https://github.com/elastic/kibana/issues/66326
if [ -e kibana.log ] ; then
grep -m 1 "http server running" <(tail -f -n +1 kibana.log)
grep -m 1 "Kibana is now available" <(tail -f -n +1 kibana.log)
echo "✅ Kibana server running..."
grep -m 1 "bundles compiled successfully" <(tail -f -n +1 kibana.log)
echo "✅ Kibana bundles have been compiled..."

View file

@ -32,6 +32,7 @@ import { FeatureUsageService } from './services';
import { LicenseConfigType } from './licensing_config';
import { createRouteHandlerContext } from './licensing_route_handler_context';
import { createOnPreResponseHandler } from './on_pre_response_handler';
import { getPluginStatus$ } from './plugin_status';
function normalizeServerLicense(license: RawLicense): PublicLicense {
return {
@ -80,7 +81,7 @@ function sign({
* current Kibana instance.
*/
export class LicensingPlugin implements Plugin<LicensingPluginSetup, LicensingPluginStart, {}, {}> {
private stop$ = new Subject();
private stop$ = new Subject<void>();
private readonly logger: Logger;
private readonly config: LicenseConfigType;
private loggingSubscription?: Subscription;
@ -127,6 +128,8 @@ export class LicensingPlugin implements Plugin<LicensingPluginSetup, LicensingPl
pollingFrequency.asMilliseconds()
);
core.status.set(getPluginStatus$(license$, this.stop$.asObservable()));
core.http.registerRouteHandlerContext(
'licensing',
createRouteHandlerContext(license$, core.getStartServices)

View file

@ -0,0 +1,114 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { TestScheduler } from 'rxjs/testing';
import { ServiceStatusLevels } from '../../../../src/core/server';
import { licenseMock } from '../common/licensing.mock';
import { getPluginStatus$ } from './plugin_status';
import { ILicense } from '../common/types';
const getTestScheduler = () =>
new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
const degradedStatus = {
level: ServiceStatusLevels.degraded,
summary: expect.any(String),
};
const availableStatus = {
level: ServiceStatusLevels.available,
summary: expect.any(String),
};
const unavailableStatus = {
level: ServiceStatusLevels.unavailable,
summary: expect.any(String),
};
describe('getPluginStatus$', () => {
it('emits an initial `degraded` status', () => {
getTestScheduler().run(({ expectObservable, hot }) => {
const license$ = hot<ILicense>('|');
const stop$ = hot<void>('');
const expected = '(a|)';
expectObservable(getPluginStatus$(license$, stop$)).toBe(expected, {
a: degradedStatus,
});
});
});
it('emits an `available` status once the license emits', () => {
getTestScheduler().run(({ expectObservable, hot }) => {
const license$ = hot<ILicense>('--a', {
a: licenseMock.createLicenseMock(),
});
const stop$ = hot<void>('');
const expected = 'a-b';
expectObservable(getPluginStatus$(license$, stop$)).toBe(expected, {
a: degradedStatus,
b: availableStatus,
});
});
});
it('emits an `unavailable` status if the license emits an error', () => {
getTestScheduler().run(({ expectObservable, hot }) => {
const errorLicense = licenseMock.createLicenseMock();
errorLicense.error = 'some-error';
const license$ = hot<ILicense>('--a', {
a: errorLicense,
});
const stop$ = hot<void>('');
const expected = 'a-b';
expectObservable(getPluginStatus$(license$, stop$)).toBe(expected, {
a: degradedStatus,
b: unavailableStatus,
});
});
});
it('can emit `available` after `unavailable`', () => {
getTestScheduler().run(({ expectObservable, hot }) => {
const errorLicense = licenseMock.createLicenseMock();
errorLicense.error = 'some-error';
const validLicense = licenseMock.createLicenseMock();
const license$ = hot<ILicense>('--a--b', {
a: errorLicense,
b: validLicense,
});
const stop$ = hot<void>('');
const expected = 'a-b--c';
expectObservable(getPluginStatus$(license$, stop$)).toBe(expected, {
a: degradedStatus,
b: unavailableStatus,
c: availableStatus,
});
});
});
it('closes when `stop$` emits', () => {
getTestScheduler().run(({ expectObservable, hot }) => {
const license$ = hot<ILicense>('--a--b', {
a: licenseMock.createLicenseMock(),
b: licenseMock.createLicenseMock(),
});
const stop$ = hot<void>('----a', { a: undefined });
const expected = 'a-b-|';
expectObservable(getPluginStatus$(license$, stop$)).toBe(expected, {
a: degradedStatus,
b: availableStatus,
});
});
});
});

View file

@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { Observable } from 'rxjs';
import { takeUntil, startWith, map } from 'rxjs/operators';
import { ServiceStatus, ServiceStatusLevels } from '../../../../src/core/server';
import { ILicense } from '../common/types';
export const getPluginStatus$ = (
license$: Observable<ILicense>,
stop$: Observable<void>
): Observable<ServiceStatus> => {
return license$.pipe(
startWith(undefined),
takeUntil(stop$),
map((license) => {
if (license) {
if (license.error) {
return {
level: ServiceStatusLevels.unavailable,
summary: 'Error fetching license',
};
}
return {
level: ServiceStatusLevels.available,
summary: 'License fetched',
};
}
return {
level: ServiceStatusLevels.degraded,
summary: 'License not fetched yet',
};
})
);
};