mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Change the health gateway to use the status API (#160125)
## Summary Follow-up of https://github.com/elastic/kibana/pull/159768 Related to https://github.com/elastic/kibana/issues/158910 Change the health-gateway behavior to hit the `/api/status` endpoint instead of just the root `/` path. This was made possible by https://github.com/elastic/kibana/pull/159768, as we now always return the correct http code from the status endpoint even for unauthenticated requests.
This commit is contained in:
parent
968c09a695
commit
c10ab82521
15 changed files with 195 additions and 95 deletions
|
@ -6,4 +6,4 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { RootRoute } from './root';
|
||||
export { StatusHandler } from './status';
|
|
@ -8,12 +8,13 @@
|
|||
|
||||
import { Server } from '@hapi/hapi';
|
||||
import { duration } from 'moment';
|
||||
import { URL } from 'url';
|
||||
import fetch, { Response } from 'node-fetch';
|
||||
import { loggerMock, MockedLogger } from '@kbn/logging-mocks';
|
||||
import type { KibanaConfig } from '../kibana_config';
|
||||
import { RootRoute } from './root';
|
||||
import { StatusHandler } from './status';
|
||||
|
||||
describe('RootRoute', () => {
|
||||
describe('StatusHandler', () => {
|
||||
let kibanaConfig: KibanaConfig;
|
||||
let logger: MockedLogger;
|
||||
let server: Server;
|
||||
|
@ -30,7 +31,11 @@ describe('RootRoute', () => {
|
|||
logger = loggerMock.create();
|
||||
|
||||
server = new Server();
|
||||
server.route(new RootRoute(kibanaConfig, logger));
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: '/',
|
||||
handler: new StatusHandler(kibanaConfig, logger).handler,
|
||||
});
|
||||
await server.initialize();
|
||||
});
|
||||
|
||||
|
@ -248,5 +253,49 @@ describe('RootRoute', () => {
|
|||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should call the host with the correct path', async () => {
|
||||
kibanaConfig.hosts.splice(0, kibanaConfig.hosts.length);
|
||||
kibanaConfig.hosts.push('http://localhost:5601', 'http://localhost:5602');
|
||||
|
||||
(fetch as jest.MockedFunction<typeof fetch>).mockResolvedValue(new Response('', ok));
|
||||
|
||||
await server.inject({
|
||||
method: 'get',
|
||||
url: '/',
|
||||
});
|
||||
|
||||
expect(fetch).toHaveBeenCalledTimes(2);
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
new URL('http://localhost:5601/api/status'),
|
||||
expect.any(Object)
|
||||
);
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
new URL('http://localhost:5602/api/status'),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('should append the status path when path already present on the host', async () => {
|
||||
kibanaConfig.hosts.splice(0, kibanaConfig.hosts.length);
|
||||
kibanaConfig.hosts.push('http://localhost:5601/prefix', 'http://localhost:5602/other/path');
|
||||
|
||||
(fetch as jest.MockedFunction<typeof fetch>).mockResolvedValue(new Response('', ok));
|
||||
|
||||
await server.inject({
|
||||
method: 'get',
|
||||
url: '/',
|
||||
});
|
||||
|
||||
expect(fetch).toHaveBeenCalledTimes(2);
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
new URL('http://localhost:5601/prefix/api/status'),
|
||||
expect.any(Object)
|
||||
);
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
new URL('http://localhost:5602/other/path/api/status'),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -6,17 +6,19 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { capitalize, chain, memoize, pick } from 'lodash';
|
||||
import { capitalize, chain, memoize } from 'lodash';
|
||||
import { Agent, AgentOptions } from 'https';
|
||||
import { URL } from 'url';
|
||||
import type { Request, ResponseObject, ResponseToolkit, ServerRoute } from '@hapi/hapi';
|
||||
import type { Request, ResponseObject, ResponseToolkit } from '@hapi/hapi';
|
||||
import nodeFetch, { Response } from 'node-fetch';
|
||||
import type { Logger } from '@kbn/logging';
|
||||
import type { KibanaConfig } from '../kibana_config';
|
||||
|
||||
type Status = 'healthy' | 'unhealthy' | 'failure' | 'timeout';
|
||||
|
||||
interface RootRouteResponse {
|
||||
const statusApiPath = '/api/status';
|
||||
|
||||
interface StatusRouteResponse {
|
||||
status: Status;
|
||||
hosts?: HostStatus[];
|
||||
}
|
||||
|
@ -27,9 +29,9 @@ interface HostStatus {
|
|||
code?: number;
|
||||
}
|
||||
|
||||
export class RootRoute implements ServerRoute {
|
||||
export class StatusHandler {
|
||||
private static isHealthy(response: Response) {
|
||||
return RootRoute.isSuccess(response) || RootRoute.isUnauthorized(response);
|
||||
return StatusHandler.isSuccess(response) || StatusHandler.isUnauthorized(response);
|
||||
}
|
||||
|
||||
private static isUnauthorized({ status, headers }: Response): boolean {
|
||||
|
@ -47,25 +49,20 @@ export class RootRoute implements ServerRoute {
|
|||
timeout: 504,
|
||||
};
|
||||
|
||||
readonly method = 'GET';
|
||||
readonly path = '/';
|
||||
|
||||
constructor(private kibanaConfig: KibanaConfig, private logger: Logger) {
|
||||
this.handler = this.handler.bind(this);
|
||||
|
||||
return pick(this, ['method', 'path', 'handler']) as RootRoute;
|
||||
}
|
||||
|
||||
async handler(request: Request, toolkit: ResponseToolkit): Promise<ResponseObject> {
|
||||
const body = await this.poll();
|
||||
const code = RootRoute.STATUS_CODE[body.status];
|
||||
const code = StatusHandler.STATUS_CODE[body.status];
|
||||
|
||||
this.logger.debug(`Returning ${code} response with body: ${JSON.stringify(body)}`);
|
||||
|
||||
return toolkit.response(body).type('application/json').code(code);
|
||||
}
|
||||
|
||||
private async poll(): Promise<RootRouteResponse> {
|
||||
private async poll(): Promise<StatusRouteResponse> {
|
||||
const hosts = await Promise.all(this.kibanaConfig.hosts.map(this.pollHost.bind(this)));
|
||||
const statuses = chain(hosts).map('status').uniq().value();
|
||||
const status = statuses.length <= 1 ? statuses[0] ?? 'healthy' : 'unhealthy';
|
||||
|
@ -81,7 +78,7 @@ export class RootRoute implements ServerRoute {
|
|||
|
||||
try {
|
||||
const response = await this.fetch(host);
|
||||
const status = RootRoute.isHealthy(response) ? 'healthy' : 'unhealthy';
|
||||
const status = StatusHandler.isHealthy(response) ? 'healthy' : 'unhealthy';
|
||||
this.logger.debug(
|
||||
`${capitalize(status)} response from '${host}' with code ${response.status}`
|
||||
);
|
||||
|
@ -112,8 +109,9 @@ export class RootRoute implements ServerRoute {
|
|||
}
|
||||
}
|
||||
|
||||
private async fetch(url: string) {
|
||||
const { protocol } = new URL(url);
|
||||
private async fetch(host: string) {
|
||||
const url = new URL(host);
|
||||
appendStatusApiPath(url);
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(
|
||||
|
@ -123,7 +121,7 @@ export class RootRoute implements ServerRoute {
|
|||
|
||||
try {
|
||||
return await nodeFetch(url, {
|
||||
agent: protocol === 'https:' ? this.getAgent() : undefined,
|
||||
agent: url.protocol === 'https:' ? this.getAgent() : undefined,
|
||||
signal: controller.signal,
|
||||
redirect: 'manual',
|
||||
});
|
||||
|
@ -161,3 +159,7 @@ export class RootRoute implements ServerRoute {
|
|||
return options;
|
||||
}
|
||||
}
|
||||
|
||||
const appendStatusApiPath = (url: URL) => {
|
||||
url.pathname = `${url.pathname}/${statusApiPath}`.replace(/\/{2,}/g, '/');
|
||||
};
|
|
@ -50,6 +50,19 @@ describe('KibanaService', () => {
|
|||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
path: '/',
|
||||
handler: expect.any(Function),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('registers /api/status route with the server', async () => {
|
||||
const kibanaService = new KibanaService({ config, logger });
|
||||
await kibanaService.start({ server });
|
||||
expect(server.addRoute).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
path: '/api/status',
|
||||
handler: expect.any(Function),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
|
|
@ -10,7 +10,7 @@ import type { IConfigService } from '@kbn/config';
|
|||
import type { Logger, LoggerFactory } from '@kbn/logging';
|
||||
import { ServerStart } from '../server';
|
||||
import { KibanaConfig } from './kibana_config';
|
||||
import { RootRoute } from './routes';
|
||||
import { StatusHandler } from './handlers';
|
||||
|
||||
interface KibanaServiceStartDependencies {
|
||||
server: ServerStart;
|
||||
|
@ -34,7 +34,17 @@ export class KibanaService {
|
|||
}
|
||||
|
||||
async start({ server }: KibanaServiceStartDependencies) {
|
||||
server.addRoute(new RootRoute(this.kibanaConfig, this.logger));
|
||||
const statusHandler = new StatusHandler(this.kibanaConfig, this.logger);
|
||||
server.addRoute({
|
||||
method: 'GET',
|
||||
path: '/',
|
||||
handler: statusHandler.handler,
|
||||
});
|
||||
server.addRoute({
|
||||
method: 'GET',
|
||||
path: '/api/status',
|
||||
handler: statusHandler.handler,
|
||||
});
|
||||
this.logger.info('Server is ready');
|
||||
}
|
||||
|
||||
|
|
|
@ -4,8 +4,8 @@ server:
|
|||
|
||||
kibana:
|
||||
hosts:
|
||||
- ${KIBANA_URL}/api/status/ok
|
||||
- ${KIBANA_URL}/api/status/flaky?session=${SESSION}
|
||||
- ${KIBANA_URL}/health/ok
|
||||
- ${KIBANA_URL}/health/flaky?session=${SESSION}
|
||||
|
||||
logging:
|
||||
root:
|
||||
|
|
|
@ -4,9 +4,9 @@ server:
|
|||
|
||||
kibana:
|
||||
hosts:
|
||||
- ${KIBANA_URL}/api/status/ok
|
||||
- ${KIBANA_URL}/api/status/redirect
|
||||
- ${KIBANA_URL}/api/status/unauthorized
|
||||
- ${KIBANA_URL}/health/ok
|
||||
- ${KIBANA_URL}/health/redirect
|
||||
- ${KIBANA_URL}/health/unauthorized
|
||||
|
||||
logging:
|
||||
root:
|
||||
|
|
|
@ -4,7 +4,7 @@ server:
|
|||
|
||||
kibana:
|
||||
hosts:
|
||||
- http://localhost:65537/api/status/ok
|
||||
- http://localhost:65537/health/ok
|
||||
requestTimeout: 2s
|
||||
|
||||
logging:
|
||||
|
|
|
@ -4,8 +4,8 @@ server:
|
|||
|
||||
kibana:
|
||||
hosts:
|
||||
- ${KIBANA_URL}/api/status/ok
|
||||
- ${KIBANA_URL}/api/status/not-found
|
||||
- ${KIBANA_URL}/health/ok
|
||||
- ${KIBANA_URL}/health/not-found
|
||||
|
||||
logging:
|
||||
root:
|
||||
|
|
|
@ -4,7 +4,7 @@ server:
|
|||
|
||||
kibana:
|
||||
hosts:
|
||||
- ${KIBANA_URL}/api/status/slow
|
||||
- ${KIBANA_URL}/health/slow
|
||||
requestTimeout: 2s
|
||||
|
||||
logging:
|
||||
|
|
|
@ -4,7 +4,7 @@ server:
|
|||
|
||||
kibana:
|
||||
hosts:
|
||||
- ${KIBANA_URL}/api/status/not-found
|
||||
- ${KIBANA_URL}/health/not-found
|
||||
|
||||
logging:
|
||||
root:
|
||||
|
|
|
@ -13,23 +13,27 @@ export class HealthGatewayStatusPlugin implements Plugin<void, void> {
|
|||
public setup(core: CoreSetup) {
|
||||
const router = core.http.createRouter();
|
||||
|
||||
router.get({ path: '/api/status/ok', validate: {} }, async (context, req, res) => res.ok());
|
||||
|
||||
router.get({ path: '/api/status/redirect', validate: {} }, async (context, req, res) =>
|
||||
res.redirected({ headers: { location: '/api/status/ok' } })
|
||||
router.get({ path: '/health/ok/api/status', validate: {} }, async (context, req, res) =>
|
||||
res.ok()
|
||||
);
|
||||
|
||||
router.get({ path: '/api/status/unauthorized', validate: {} }, async (context, req, res) =>
|
||||
res.unauthorized({
|
||||
headers: { 'www-authenticate': 'Basic' },
|
||||
})
|
||||
router.get({ path: '/health/redirect/api/status', validate: {} }, async (context, req, res) =>
|
||||
res.redirected({ headers: { location: '/health/ok/api/status' } })
|
||||
);
|
||||
|
||||
router.get({ path: '/api/status/not-found', validate: {} }, async (context, req, res) =>
|
||||
router.get(
|
||||
{ path: '/health/unauthorized/api/status', validate: {} },
|
||||
async (context, req, res) =>
|
||||
res.unauthorized({
|
||||
headers: { 'www-authenticate': 'Basic' },
|
||||
})
|
||||
);
|
||||
|
||||
router.get({ path: '/health/not-found/api/status', validate: {} }, async (context, req, res) =>
|
||||
res.notFound()
|
||||
);
|
||||
|
||||
router.get({ path: '/api/status/slow', validate: {} }, async (context, req, res) => {
|
||||
router.get({ path: '/health/slow/api/status', validate: {} }, async (context, req, res) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
|
||||
return res.ok();
|
||||
|
@ -38,7 +42,7 @@ export class HealthGatewayStatusPlugin implements Plugin<void, void> {
|
|||
const sessions = new Set<string>();
|
||||
router.get(
|
||||
{
|
||||
path: '/api/status/flaky',
|
||||
path: '/health/flaky/api/status',
|
||||
validate: {
|
||||
query: schema.object({ session: schema.string() }),
|
||||
},
|
||||
|
@ -58,5 +62,6 @@ export class HealthGatewayStatusPlugin implements Plugin<void, void> {
|
|||
}
|
||||
|
||||
public start() {}
|
||||
|
||||
public stop() {}
|
||||
}
|
||||
|
|
|
@ -66,9 +66,9 @@ export class HealthGatewayService extends FtrService {
|
|||
this.port = undefined;
|
||||
}
|
||||
|
||||
poll() {
|
||||
poll(path: string) {
|
||||
this.assertRunning();
|
||||
|
||||
return supertest(`http://${this.host}:${this.port}`).get('/').send();
|
||||
return supertest(`http://${this.host}:${this.port}`).get(path).send();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,58 +6,10 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from './ftr_provider_context';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const healthGateway = getService('healthGateway');
|
||||
|
||||
export default function ({ loadTestFile }: FtrProviderContext) {
|
||||
describe('health gateway', () => {
|
||||
it('returns 200 on healthy hosts', async () => {
|
||||
await healthGateway.start('fixtures/healthy.yaml');
|
||||
|
||||
const { body } = await healthGateway.poll().expect(200);
|
||||
expect(body).to.have.property('status', 'healthy');
|
||||
});
|
||||
|
||||
it('returns 503 on unhealthy host', async () => {
|
||||
await healthGateway.start('fixtures/unhealthy.yaml');
|
||||
|
||||
const { body } = await healthGateway.poll().expect(503);
|
||||
expect(body).to.have.property('status', 'unhealthy');
|
||||
});
|
||||
|
||||
it('returns 503 on mixed responses', async () => {
|
||||
await healthGateway.start('fixtures/mixed.yaml');
|
||||
|
||||
const { body } = await healthGateway.poll().expect(503);
|
||||
expect(body).to.have.property('status', 'unhealthy');
|
||||
});
|
||||
|
||||
it('returns 504 on timeout', async () => {
|
||||
await healthGateway.start('fixtures/timeout.yaml');
|
||||
|
||||
const { body } = await healthGateway.poll().expect(504);
|
||||
expect(body).to.have.property('status', 'timeout');
|
||||
});
|
||||
|
||||
it('returns 502 on exception', async () => {
|
||||
await healthGateway.start('fixtures/invalid.yaml');
|
||||
|
||||
const { body } = await healthGateway.poll().expect(502);
|
||||
expect(body).to.have.property('status', 'failure');
|
||||
});
|
||||
|
||||
it('returns different status codes on state changes', async () => {
|
||||
await healthGateway.start('fixtures/flaky.yaml', { env: { SESSION: `${Math.random()}` } });
|
||||
|
||||
await healthGateway.poll().expect(200);
|
||||
await healthGateway.poll().expect(503);
|
||||
await healthGateway.poll().expect(200);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await healthGateway.stop();
|
||||
});
|
||||
loadTestFile(require.resolve('./status'));
|
||||
});
|
||||
}
|
||||
|
|
69
test/health_gateway/tests/status.ts
Normal file
69
test/health_gateway/tests/status.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from './ftr_provider_context';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const healthGateway = getService('healthGateway');
|
||||
|
||||
describe('status API', () => {
|
||||
afterEach(async () => {
|
||||
await healthGateway.stop();
|
||||
});
|
||||
|
||||
['/', '/api/status'].forEach((path) => {
|
||||
describe(`${path}`, () => {
|
||||
it('returns 200 on healthy hosts', async () => {
|
||||
await healthGateway.start('fixtures/healthy.yaml');
|
||||
|
||||
const { body } = await healthGateway.poll(path).expect(200);
|
||||
expect(body).to.have.property('status', 'healthy');
|
||||
});
|
||||
|
||||
it('returns 503 on unhealthy host', async () => {
|
||||
await healthGateway.start('fixtures/unhealthy.yaml');
|
||||
|
||||
const { body } = await healthGateway.poll(path).expect(503);
|
||||
expect(body).to.have.property('status', 'unhealthy');
|
||||
});
|
||||
|
||||
it('returns 503 on mixed responses', async () => {
|
||||
await healthGateway.start('fixtures/mixed.yaml');
|
||||
|
||||
const { body } = await healthGateway.poll(path).expect(503);
|
||||
expect(body).to.have.property('status', 'unhealthy');
|
||||
});
|
||||
|
||||
it('returns 504 on timeout', async () => {
|
||||
await healthGateway.start('fixtures/timeout.yaml');
|
||||
|
||||
const { body } = await healthGateway.poll(path).expect(504);
|
||||
expect(body).to.have.property('status', 'timeout');
|
||||
});
|
||||
|
||||
it('returns 502 on exception', async () => {
|
||||
await healthGateway.start('fixtures/invalid.yaml');
|
||||
|
||||
const { body } = await healthGateway.poll(path).expect(502);
|
||||
expect(body).to.have.property('status', 'failure');
|
||||
});
|
||||
|
||||
it('returns different status codes on state changes', async () => {
|
||||
await healthGateway.start('fixtures/flaky.yaml', {
|
||||
env: { SESSION: `${Math.random()}` },
|
||||
});
|
||||
|
||||
await healthGateway.poll(path).expect(200);
|
||||
await healthGateway.poll(path).expect(503);
|
||||
await healthGateway.poll(path).expect(200);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue