Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[7.x] Display Kibana overall status in the logs and have FTR wait for green status before running tests (#102108) #102511

Merged
merged 1 commit into from
Jun 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/kbn-test/src/functional_test_runner/lib/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,19 @@ export const schema = Joi.object()
sourceArgs: Joi.array(),
serverArgs: Joi.array(),
installDir: Joi.string(),
/** Options for how FTR should execute and interact with Kibana */
runOptions: Joi.object()
.keys({
/**
* Log message to wait for before initiating tests, defaults to waiting for Kibana status to be `available`.
* Note that this log message must not be filtered out by the current logging config, for example by the
* log level. If needed, you can adjust the logging level via `kbnTestServer.serverArgs`.
*/
wait: Joi.object()
.regex()
.default(/Kibana is now available/),
})
.default(),
})
.default(),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ function extendNodeOptions(installDir) {

export async function runKibanaServer({ procs, config, options }) {
const { installDir } = options;
const runOptions = config.get('kbnTestServer.runOptions');

await procs.run('kibana', {
cmd: getKibanaCmd(installDir),
Expand All @@ -38,7 +39,7 @@ export async function runKibanaServer({ procs, config, options }) {
...extendNodeOptions(installDir),
},
cwd: installDir || KIBANA_ROOT,
wait: /\[Kibana\]\[http\] http server running/,
wait: runOptions.wait,
});
}

Expand Down
6 changes: 0 additions & 6 deletions packages/kbn-test/src/functional_tests/tasks.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,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 {
Expand Down Expand Up @@ -164,7 +162,3 @@ async function silence(log, milliseconds) {
)
.toPromise();
}

async function delay(ms) {
await new Promise((resolve) => setTimeout(resolve, ms));
}
1 change: 1 addition & 0 deletions src/core/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ export class Server {

await this.plugins.start(this.coreStart);

this.status.start();
await this.http.start();

startTransaction?.end();
Expand Down
103 changes: 103 additions & 0 deletions src/core/server/status/log_overall_status.test.ts
Original file line number Diff line number Diff line change
@@ -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',
});
});
});
});
31 changes: 31 additions & 0 deletions src/core/server/status/log_overall_status.ts
Original file line number Diff line number Diff line change
@@ -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()}`;
})
);
};
4 changes: 2 additions & 2 deletions src/core/server/status/plugins_status.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,9 +301,9 @@ describe('PluginStatusService', () => {
pluginA$.next(available);
pluginA$.next(degraded);
// Waiting for the debounce timeout should cut a new update
await delay(500);
await delay(25);
pluginA$.next(available);
await delay(500);
await delay(25);
subscription.unsubscribe();

expect(statusUpdates).toMatchInlineSnapshot(`
Expand Down
2 changes: 1 addition & 1 deletion src/core/server/status/plugins_status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export class PluginsStatusService {

return this.getPluginStatuses$(dependencies).pipe(
// Prevent many emissions at once from dependency status resolution from making this too noisy
debounceTime(500)
debounceTime(25)
);
}

Expand Down
28 changes: 19 additions & 9 deletions src/core/server/status/status_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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 StatusLogMeta extends LogMeta {
kibana: { status: ServiceStatus };
Expand All @@ -42,7 +43,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;

Expand All @@ -63,10 +66,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]) => {
Expand All @@ -86,7 +86,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 commonRouteDeps = {
config: {
Expand All @@ -97,7 +97,7 @@ export class StatusService implements CoreService<InternalStatusServiceSetup> {
},
metrics,
status: {
overall$,
overall$: this.overall$,
plugins$: this.pluginsStatus.getAll$(),
core$,
},
Expand All @@ -124,7 +124,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),
Expand All @@ -134,9 +134,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;
Expand Down
9 changes: 0 additions & 9 deletions test/functional/apps/bundles/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,6 @@ export default function ({ getService }) {

let buildNum;
before(async () => {
// Wait for status to become green
let status;
const start = Date.now();
do {
const resp = await supertest.get('/api/status');
status = resp.status;
// Stop polling once status stabilizes OR once 40s has passed
} while (status !== 200 && Date.now() - start < 40_000);

const resp = await supertest.get('/api/status').expect(200);
buildNum = resp.body.version.build_number;
});
Expand Down
11 changes: 0 additions & 11 deletions test/server_integration/http/platform/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,6 @@ export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');

describe('kibana server cache-control', () => {
before(async () => {
// Wait for status to become green
let status;
const start = Date.now();
do {
const resp = await supertest.get('/api/status');
status = resp.status;
// Stop polling once status stabilizes OR once 40s has passed
} while (status !== 200 && Date.now() - start < 40_000);
});

it('properly marks responses as private, with directives to disable caching', async () => {
await supertest
.get('/api/status')
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/apm/e2e/run-e2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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..."
Expand Down
5 changes: 4 additions & 1 deletion x-pack/plugins/licensing/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
Loading