[GET /api/status] Default to v8format and allow v7format=true (#110830)

This commit is contained in:
Alejandro Fernández Haro 2021-09-03 12:32:59 +01:00 committed by GitHub
parent 4e9e7a8671
commit dfea0fee21
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 412 additions and 226 deletions

View file

@ -31,10 +31,10 @@ async function getDeprecations({ esClient, savedObjectsClient }: GetDeprecations
// Example of a manual correctiveAction
deprecations.push({
title: i18n.translate('xpack.timelion.deprecations.worksheetsTitle', {
defaultMessage: 'Found Timelion worksheets.'
defaultMessage: 'Timelion worksheets are deprecated'
}),
message: i18n.translate('xpack.timelion.deprecations.worksheetsMessage', {
defaultMessage: 'You have {count} Timelion worksheets. The Timelion app will be removed in 8.0. To continue using your Timelion worksheets, migrate them to a dashboard.',
defaultMessage: 'You have {count} Timelion worksheets. Migrate your Timelion worksheets to a dashboard to continue using them.',
values: { count },
}),
documentationUrl:

View file

@ -8,26 +8,14 @@
import { KbnClientStatus } from './kbn_client_status';
const PLUGIN_STATUS_ID = /^plugin:(.+?)@/;
export class KbnClientPlugins {
constructor(private readonly status: KbnClientStatus) {}
/**
* Get a list of plugin ids that are enabled on the server
*/
public async getEnabledIds() {
const pluginIds: string[] = [];
const apiResp = await this.status.get();
for (const status of apiResp.status.statuses) {
if (status.id) {
const match = status.id.match(PLUGIN_STATUS_ID);
if (match) {
pluginIds.push(match[1]);
}
}
}
return pluginIds;
return Object.keys(apiResp.status.plugins);
}
}

View file

@ -9,13 +9,11 @@
import { KbnClientRequester } from './kbn_client_requester';
interface Status {
state: 'green' | 'red' | 'yellow';
title?: string;
id?: string;
icon: string;
message: string;
uiColor: string;
since: string;
level: 'available' | 'degraded' | 'unavailable' | 'critical';
summary: string;
detail?: string;
documentationUrl?: string;
meta?: Record<string, unknown>;
}
interface ApiResponseStatus {
@ -29,7 +27,8 @@ interface ApiResponseStatus {
};
status: {
overall: Status;
statuses: Status[];
core: Record<string, Status>;
plugins: Record<string, Status>;
};
metrics: unknown;
}
@ -55,6 +54,6 @@ export class KbnClientStatus {
*/
public async getOverallState() {
const status = await this.get();
return status.status.overall.state;
return status.status.overall.level;
}
}

View file

@ -145,7 +145,7 @@ export async function loadStatus({
let response: StatusResponse;
try {
response = await http.get('/api/status', { query: { v8format: true } });
response = await http.get('/api/status');
} catch (e) {
// API returns a 503 response if not all services are available.
// In this case, we want to treat this as a successful API call, so that we can

View file

@ -10,12 +10,14 @@ import { PublicMethodsOf } from '@kbn/utility-types';
import { BehaviorSubject } from 'rxjs';
import { CoreUsageDataService } from './core_usage_data_service';
import { coreUsageStatsClientMock } from './core_usage_stats_client.mock';
import { CoreUsageData, CoreUsageDataSetup, CoreUsageDataStart } from './types';
import { CoreUsageData, InternalCoreUsageDataSetup, CoreUsageDataStart } from './types';
const createSetupContractMock = (usageStatsClient = coreUsageStatsClientMock.create()) => {
const setupContract: jest.Mocked<CoreUsageDataSetup> = {
const setupContract: jest.Mocked<InternalCoreUsageDataSetup> = {
registerType: jest.fn(),
getClient: jest.fn().mockReturnValue(usageStatsClient),
registerUsageCounter: jest.fn(),
incrementUsageCounter: jest.fn(),
};
return setupContract;
};

View file

@ -150,6 +150,50 @@ describe('CoreUsageDataService', () => {
expect(usageStatsClient).toBeInstanceOf(CoreUsageStatsClient);
});
});
describe('Usage Counter', () => {
it('registers a usage counter and uses it to increment the counters', async () => {
const http = httpServiceMock.createInternalSetupContract();
const metrics = metricsServiceMock.createInternalSetupContract();
const savedObjectsStartPromise = Promise.resolve(
savedObjectsServiceMock.createStartContract()
);
const changedDeprecatedConfigPath$ = configServiceMock.create().getDeprecatedConfigPath$();
const coreUsageData = service.setup({
http,
metrics,
savedObjectsStartPromise,
changedDeprecatedConfigPath$,
});
const myUsageCounter = { incrementCounter: jest.fn() };
coreUsageData.registerUsageCounter(myUsageCounter);
coreUsageData.incrementUsageCounter({ counterName: 'test' });
expect(myUsageCounter.incrementCounter).toHaveBeenCalledWith({ counterName: 'test' });
});
it('swallows errors when provided increment counter fails', async () => {
const http = httpServiceMock.createInternalSetupContract();
const metrics = metricsServiceMock.createInternalSetupContract();
const savedObjectsStartPromise = Promise.resolve(
savedObjectsServiceMock.createStartContract()
);
const changedDeprecatedConfigPath$ = configServiceMock.create().getDeprecatedConfigPath$();
const coreUsageData = service.setup({
http,
metrics,
savedObjectsStartPromise,
changedDeprecatedConfigPath$,
});
const myUsageCounter = {
incrementCounter: jest.fn(() => {
throw new Error('Something is really wrong');
}),
};
coreUsageData.registerUsageCounter(myUsageCounter);
expect(() => coreUsageData.incrementUsageCounter({ counterName: 'test' })).not.toThrow();
expect(myUsageCounter.incrementCounter).toHaveBeenCalledWith({ counterName: 'test' });
});
});
});
describe('start', () => {

View file

@ -27,7 +27,7 @@ import type {
CoreServicesUsageData,
CoreUsageData,
CoreUsageDataStart,
CoreUsageDataSetup,
InternalCoreUsageDataSetup,
ConfigUsageData,
CoreConfigUsageData,
} from './types';
@ -39,6 +39,7 @@ import { LEGACY_URL_ALIAS_TYPE } from '../saved_objects/object_types';
import { CORE_USAGE_STATS_TYPE } from './constants';
import { CoreUsageStatsClient } from './core_usage_stats_client';
import { MetricsServiceSetup, OpsMetrics } from '..';
import { CoreIncrementUsageCounter } from './types';
export type ExposedConfigsToUsage = Map<string, Record<string, boolean>>;
@ -86,7 +87,8 @@ const isCustomIndex = (index: string) => {
return index !== '.kibana';
};
export class CoreUsageDataService implements CoreService<CoreUsageDataSetup, CoreUsageDataStart> {
export class CoreUsageDataService
implements CoreService<InternalCoreUsageDataSetup, CoreUsageDataStart> {
private logger: Logger;
private elasticsearchConfig?: ElasticsearchConfigType;
private configService: CoreContext['configService'];
@ -98,6 +100,7 @@ export class CoreUsageDataService implements CoreService<CoreUsageDataSetup, Cor
private kibanaConfig?: KibanaConfigType;
private coreUsageStatsClient?: CoreUsageStatsClient;
private deprecatedConfigPaths: ChangedDeprecatedPaths = { set: [], unset: [] };
private incrementUsageCounter: CoreIncrementUsageCounter = () => {}; // Initially set to noop
constructor(core: CoreContext) {
this.logger = core.logger.get('core-usage-stats-service');
@ -495,7 +498,24 @@ export class CoreUsageDataService implements CoreService<CoreUsageDataSetup, Cor
this.coreUsageStatsClient = getClient();
return { registerType, getClient } as CoreUsageDataSetup;
const contract: InternalCoreUsageDataSetup = {
registerType,
getClient,
registerUsageCounter: (usageCounter) => {
this.incrementUsageCounter = (params) => usageCounter.incrementCounter(params);
},
incrementUsageCounter: (params) => {
try {
this.incrementUsageCounter(params);
} catch (e) {
// Self-defense mechanism since the handler is externally registered
this.logger.debug('Failed to increase the usage counter');
this.logger.debug(e);
}
},
};
return contract;
}
start({ savedObjects, elasticsearch, exposedConfigsToUsage }: StartDeps) {

View file

@ -7,7 +7,15 @@
*/
export { CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID } from './constants';
export type { CoreUsageDataSetup, ConfigUsageData, CoreUsageDataStart } from './types';
export type {
InternalCoreUsageDataSetup,
ConfigUsageData,
CoreUsageDataStart,
CoreUsageDataSetup,
CoreUsageCounter,
CoreIncrementUsageCounter,
CoreIncrementCounterParams,
} from './types';
export { CoreUsageDataService } from './core_usage_data_service';
export { CoreUsageStatsClient, REPOSITORY_RESOLVE_OUTCOME_STATS } from './core_usage_stats_client';

View file

@ -280,12 +280,59 @@ export interface CoreConfigUsageData {
};
}
/**
* @internal Details about the counter to be incremented
*/
export interface CoreIncrementCounterParams {
/** The name of the counter **/
counterName: string;
/** The counter type ("count" by default) **/
counterType?: string;
/** Increment the counter by this number (1 if not specified) **/
incrementBy?: number;
}
/**
* @internal
* Method to call whenever an event occurs, so the counter can be increased.
*/
export type CoreIncrementUsageCounter = (params: CoreIncrementCounterParams) => void;
/**
* @internal
* API to track whenever an event occurs, so the core can report them.
*/
export interface CoreUsageCounter {
/** @internal {@link CoreIncrementUsageCounter} **/
incrementCounter: CoreIncrementUsageCounter;
}
/** @internal */
export interface CoreUsageDataSetup {
export interface InternalCoreUsageDataSetup extends CoreUsageDataSetup {
registerType(
typeRegistry: ISavedObjectTypeRegistry & Pick<SavedObjectTypeRegistry, 'registerType'>
): void;
getClient(): CoreUsageStatsClient;
/** @internal {@link CoreIncrementUsageCounter} **/
incrementUsageCounter: CoreIncrementUsageCounter;
}
/**
* Internal API for registering the Usage Tracker used for Core's usage data payload.
*
* @note This API should never be used to drive application logic and is only
* intended for telemetry purposes.
*
* @internal
*/
export interface CoreUsageDataSetup {
/**
* @internal
* API for a usage tracker plugin to inject the {@link CoreUsageCounter} to use
* when tracking events.
*/
registerUsageCounter: (usageCounter: CoreUsageCounter) => void;
}
/**

View file

@ -96,8 +96,8 @@ describe('fake elasticsearch', () => {
test('should return unknown product when it cannot perform the Product check (503 response)', async () => {
const resp = await supertest(kibanaHttpServer).get('/api/status').expect(503);
expect(resp.body.status.overall.state).toBe('red');
expect(resp.body.status.statuses[0].message).toBe(
expect(resp.body.status.overall.level).toBe('critical');
expect(resp.body.status.core.elasticsearch.summary).toBe(
'Unable to retrieve version information from Elasticsearch nodes. The client noticed that the server is not Elasticsearch and we do not support this unknown product.'
);
});

View file

@ -55,7 +55,7 @@ import { CapabilitiesSetup, CapabilitiesStart } from './capabilities';
import { MetricsServiceSetup, MetricsServiceStart } from './metrics';
import { StatusServiceSetup } from './status';
import { AppenderConfigType, appendersSchema, LoggingServiceSetup } from './logging';
import { CoreUsageDataStart } from './core_usage_data';
import { CoreUsageDataStart, CoreUsageDataSetup } from './core_usage_data';
import { I18nServiceSetup } from './i18n';
import { DeprecationsServiceSetup, DeprecationsClient } from './deprecations';
// Because of #79265 we need to explicitly import, then export these types for
@ -410,7 +410,13 @@ export type {
export { ServiceStatusLevels } from './status';
export type { CoreStatus, ServiceStatus, ServiceStatusLevel, StatusServiceSetup } from './status';
export type { CoreUsageDataStart } from './core_usage_data';
export type {
CoreUsageDataSetup,
CoreUsageDataStart,
CoreUsageCounter,
CoreIncrementUsageCounter,
CoreIncrementCounterParams,
} from './core_usage_data';
/**
* Plugin specific context passed to a route handler.
@ -500,6 +506,8 @@ export interface CoreSetup<TPluginsStart extends object = object, TStart = unkno
deprecations: DeprecationsServiceSetup;
/** {@link StartServicesAccessor} */
getStartServices: StartServicesAccessor<TPluginsStart, TStart>;
/** @internal {@link CoreUsageDataSetup} */
coreUsageData: CoreUsageDataSetup;
}
/**

View file

@ -36,7 +36,7 @@ import { InternalRenderingServiceSetup } from './rendering';
import { InternalHttpResourcesPreboot, InternalHttpResourcesSetup } from './http_resources';
import { InternalStatusServiceSetup } from './status';
import { InternalLoggingServicePreboot, InternalLoggingServiceSetup } from './logging';
import { CoreUsageDataStart } from './core_usage_data';
import { CoreUsageDataStart, InternalCoreUsageDataSetup } from './core_usage_data';
import { I18nServiceSetup } from './i18n';
import { InternalDeprecationsServiceSetup, InternalDeprecationsServiceStart } from './deprecations';
import type {
@ -73,6 +73,7 @@ export interface InternalCoreSetup {
logging: InternalLoggingServiceSetup;
metrics: InternalMetricsServiceSetup;
deprecations: InternalDeprecationsServiceSetup;
coreUsageData: InternalCoreUsageDataSetup;
}
/**

View file

@ -169,6 +169,9 @@ function createCoreSetupMock({
metrics: metricsServiceMock.createSetupContract(),
deprecations: deprecationsServiceMock.createSetupContract(),
executionContext: executionContextServiceMock.createInternalSetupContract(),
coreUsageData: {
registerUsageCounter: coreUsageDataServiceMock.createSetupContract().registerUsageCounter,
},
getStartServices: jest
.fn<Promise<[ReturnType<typeof createCoreStartMock>, object, any]>, []>()
.mockResolvedValue([createCoreStartMock(), pluginStartDeps, pluginStartContract]),
@ -222,6 +225,7 @@ function createInternalCoreSetupMock() {
metrics: metricsServiceMock.createInternalSetupContract(),
deprecations: deprecationsServiceMock.createInternalSetupContract(),
executionContext: executionContextServiceMock.createInternalSetupContract(),
coreUsageData: coreUsageDataServiceMock.createSetupContract(),
};
return setupDeps;
}

View file

@ -211,6 +211,9 @@ export function createPluginSetupContext<TPlugin, TPluginDependencies>(
},
getStartServices: () => plugin.startDependencies,
deprecations: deps.deprecations.getRegistry(plugin.name),
coreUsageData: {
registerUsageCounter: deps.coreUsageData.registerUsageCounter,
},
};
}

View file

@ -8,11 +8,11 @@
import { schema } from '@kbn/config-schema';
import { IRouter } from '../../http';
import { CoreUsageDataSetup } from '../../core_usage_data';
import { InternalCoreUsageDataSetup } from '../../core_usage_data';
import { catchAndReturnBoomErrors } from './utils';
interface RouteDependencies {
coreUsageData: CoreUsageDataSetup;
coreUsageData: InternalCoreUsageDataSetup;
}
export const registerBulkCreateRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => {

View file

@ -8,11 +8,11 @@
import { schema } from '@kbn/config-schema';
import { IRouter } from '../../http';
import { CoreUsageDataSetup } from '../../core_usage_data';
import { InternalCoreUsageDataSetup } from '../../core_usage_data';
import { catchAndReturnBoomErrors } from './utils';
interface RouteDependencies {
coreUsageData: CoreUsageDataSetup;
coreUsageData: InternalCoreUsageDataSetup;
}
export const registerBulkGetRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => {

View file

@ -8,11 +8,11 @@
import { schema } from '@kbn/config-schema';
import { IRouter } from '../../http';
import { CoreUsageDataSetup } from '../../core_usage_data';
import { InternalCoreUsageDataSetup } from '../../core_usage_data';
import { catchAndReturnBoomErrors } from './utils';
interface RouteDependencies {
coreUsageData: CoreUsageDataSetup;
coreUsageData: InternalCoreUsageDataSetup;
}
export const registerBulkUpdateRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => {

View file

@ -8,11 +8,11 @@
import { schema } from '@kbn/config-schema';
import { IRouter } from '../../http';
import { CoreUsageDataSetup } from '../../core_usage_data';
import { InternalCoreUsageDataSetup } from '../../core_usage_data';
import { catchAndReturnBoomErrors } from './utils';
interface RouteDependencies {
coreUsageData: CoreUsageDataSetup;
coreUsageData: InternalCoreUsageDataSetup;
}
export const registerCreateRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => {

View file

@ -8,11 +8,11 @@
import { schema } from '@kbn/config-schema';
import { IRouter } from '../../http';
import { CoreUsageDataSetup } from '../../core_usage_data';
import { InternalCoreUsageDataSetup } from '../../core_usage_data';
import { catchAndReturnBoomErrors } from './utils';
interface RouteDependencies {
coreUsageData: CoreUsageDataSetup;
coreUsageData: InternalCoreUsageDataSetup;
}
export const registerDeleteRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => {

View file

@ -11,7 +11,7 @@ import stringify from 'json-stable-stringify';
import { createPromiseFromStreams, createMapStream, createConcatStream } from '@kbn/utils';
import { IRouter, KibanaRequest } from '../../http';
import { CoreUsageDataSetup } from '../../core_usage_data';
import { InternalCoreUsageDataSetup } from '../../core_usage_data';
import { SavedObjectConfig } from '../saved_objects_config';
import {
SavedObjectsExportByTypeOptions,
@ -22,7 +22,7 @@ import { validateTypes, validateObjects, catchAndReturnBoomErrors } from './util
interface RouteDependencies {
config: SavedObjectConfig;
coreUsageData: CoreUsageDataSetup;
coreUsageData: InternalCoreUsageDataSetup;
}
type EitherExportOptions = SavedObjectsExportByTypeOptions | SavedObjectsExportByObjectOptions;

View file

@ -8,11 +8,11 @@
import { schema } from '@kbn/config-schema';
import { IRouter } from '../../http';
import { CoreUsageDataSetup } from '../../core_usage_data';
import { InternalCoreUsageDataSetup } from '../../core_usage_data';
import { catchAndReturnBoomErrors } from './utils';
interface RouteDependencies {
coreUsageData: CoreUsageDataSetup;
coreUsageData: InternalCoreUsageDataSetup;
}
export const registerFindRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => {

View file

@ -8,11 +8,11 @@
import { schema } from '@kbn/config-schema';
import { IRouter } from '../../http';
import { CoreUsageDataSetup } from '../../core_usage_data';
import { InternalCoreUsageDataSetup } from '../../core_usage_data';
import { catchAndReturnBoomErrors } from './utils';
interface RouteDependencies {
coreUsageData: CoreUsageDataSetup;
coreUsageData: InternalCoreUsageDataSetup;
}
export const registerGetRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => {

View file

@ -10,14 +10,14 @@ import { Readable } from 'stream';
import { extname } from 'path';
import { schema } from '@kbn/config-schema';
import { IRouter } from '../../http';
import { CoreUsageDataSetup } from '../../core_usage_data';
import { InternalCoreUsageDataSetup } from '../../core_usage_data';
import { SavedObjectConfig } from '../saved_objects_config';
import { SavedObjectsImportError } from '../import';
import { catchAndReturnBoomErrors, createSavedObjectsStreamFromNdJson } from './utils';
interface RouteDependencies {
config: SavedObjectConfig;
coreUsageData: CoreUsageDataSetup;
coreUsageData: InternalCoreUsageDataSetup;
}
interface FileStream extends Readable {

View file

@ -7,7 +7,7 @@
*/
import { InternalHttpServiceSetup } from '../../http';
import { CoreUsageDataSetup } from '../../core_usage_data';
import { InternalCoreUsageDataSetup } from '../../core_usage_data';
import { Logger } from '../../logging';
import { SavedObjectConfig } from '../saved_objects_config';
import { IKibanaMigrator } from '../migrations';
@ -34,7 +34,7 @@ export function registerRoutes({
migratorPromise,
}: {
http: InternalHttpServiceSetup;
coreUsageData: CoreUsageDataSetup;
coreUsageData: InternalCoreUsageDataSetup;
logger: Logger;
config: SavedObjectConfig;
migratorPromise: Promise<IKibanaMigrator>;

View file

@ -8,10 +8,10 @@
import { schema } from '@kbn/config-schema';
import { IRouter } from '../../http';
import { CoreUsageDataSetup } from '../../core_usage_data';
import { InternalCoreUsageDataSetup } from '../../core_usage_data';
interface RouteDependencies {
coreUsageData: CoreUsageDataSetup;
coreUsageData: InternalCoreUsageDataSetup;
}
export const registerResolveRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => {

View file

@ -11,13 +11,13 @@ import { Readable } from 'stream';
import { schema } from '@kbn/config-schema';
import { chain } from 'lodash';
import { IRouter } from '../../http';
import { CoreUsageDataSetup } from '../../core_usage_data';
import { InternalCoreUsageDataSetup } from '../../core_usage_data';
import { SavedObjectConfig } from '../saved_objects_config';
import { SavedObjectsImportError } from '../import';
import { catchAndReturnBoomErrors, createSavedObjectsStreamFromNdJson } from './utils';
interface RouteDependencies {
config: SavedObjectConfig;
coreUsageData: CoreUsageDataSetup;
coreUsageData: InternalCoreUsageDataSetup;
}
interface FileStream extends Readable {

View file

@ -8,12 +8,12 @@
import { schema } from '@kbn/config-schema';
import { IRouter } from '../../http';
import { CoreUsageDataSetup } from '../../core_usage_data';
import { InternalCoreUsageDataSetup } from '../../core_usage_data';
import type { SavedObjectsUpdateOptions } from '../service/saved_objects_client';
import { catchAndReturnBoomErrors } from './utils';
interface RouteDependencies {
coreUsageData: CoreUsageDataSetup;
coreUsageData: InternalCoreUsageDataSetup;
}
export const registerUpdateRoute = (router: IRouter, { coreUsageData }: RouteDependencies) => {

View file

@ -16,7 +16,7 @@ import {
} from './';
import { KibanaMigrator, IKibanaMigrator } from './migrations';
import { CoreContext } from '../core_context';
import { CoreUsageDataSetup } from '../core_usage_data';
import { InternalCoreUsageDataSetup } from '../core_usage_data';
import {
ElasticsearchClient,
InternalElasticsearchServiceSetup,
@ -250,7 +250,7 @@ export interface SavedObjectsRepositoryFactory {
export interface SavedObjectsSetupDeps {
http: InternalHttpServiceSetup;
elasticsearch: InternalElasticsearchServiceSetup;
coreUsageData: CoreUsageDataSetup;
coreUsageData: InternalCoreUsageDataSetup;
}
interface WrappedClientFactoryWrapper {

View file

@ -359,6 +359,16 @@ export interface CoreEnvironmentUsageData {
// @internal (undocumented)
export type CoreId = symbol;
// @internal
export interface CoreIncrementCounterParams {
counterName: string;
counterType?: string;
incrementBy?: number;
}
// @internal
export type CoreIncrementUsageCounter = (params: CoreIncrementCounterParams) => void;
// @public
export interface CorePreboot {
// (undocumented)
@ -395,6 +405,8 @@ export interface CoreSetup<TPluginsStart extends object = object, TStart = unkno
capabilities: CapabilitiesSetup;
// (undocumented)
context: ContextSetup;
// @internal (undocumented)
coreUsageData: CoreUsageDataSetup;
// (undocumented)
deprecations: DeprecationsServiceSetup;
// (undocumented)
@ -449,6 +461,12 @@ export interface CoreStatus {
savedObjects: ServiceStatus;
}
// @internal
export interface CoreUsageCounter {
// (undocumented)
incrementCounter: CoreIncrementUsageCounter;
}
// @internal
export interface CoreUsageData extends CoreUsageStats {
// (undocumented)
@ -459,6 +477,11 @@ export interface CoreUsageData extends CoreUsageStats {
services: CoreServicesUsageData;
}
// @internal
export interface CoreUsageDataSetup {
registerUsageCounter: (usageCounter: CoreUsageCounter) => void;
}
// @internal
export interface CoreUsageDataStart {
// (undocumented)

View file

@ -243,6 +243,7 @@ export class Server {
environment: environmentSetup,
http: httpSetup,
metrics: metricsSetup,
coreUsageData: coreUsageDataSetup,
});
const renderingSetup = await this.rendering.setup({
@ -278,6 +279,7 @@ export class Server {
logging: loggingSetup,
metrics: metricsSetup,
deprecations: deprecationsSetup,
coreUsageData: coreUsageDataSetup,
};
const pluginsSetup = await this.plugins.setup(coreSetup);

View file

@ -29,6 +29,7 @@ describe('GET /api/status', () => {
let server: HttpService;
let httpSetup: InternalHttpServiceSetup;
let metrics: jest.Mocked<MetricsServiceSetup>;
let incrementUsageCounter: jest.Mock;
const setupServer = async ({ allowAnonymous = true }: { allowAnonymous?: boolean } = {}) => {
const coreContext = createCoreContext({ coreId });
@ -50,6 +51,8 @@ describe('GET /api/status', () => {
d: { level: ServiceStatusLevels.critical, summary: 'd is critical' },
});
incrementUsageCounter = jest.fn();
const router = httpSetup.createRouter('');
registerStatusRoute({
router,
@ -71,6 +74,7 @@ describe('GET /api/status', () => {
core$: status.core$,
plugins$: pluginsStatus$,
},
incrementUsageCounter,
});
// Register dummy auth provider for testing auth
@ -137,69 +141,75 @@ describe('GET /api/status', () => {
});
describe('legacy status format', () => {
it('returns legacy status format when no query params provided', async () => {
await setupServer();
const result = await supertest(httpSetup.server.listener).get('/api/status').expect(200);
expect(result.body.status).toEqual({
overall: {
const legacyFormat = {
overall: {
icon: 'success',
nickname: 'Looking good',
since: expect.any(String),
state: 'green',
title: 'Green',
uiColor: 'secondary',
},
statuses: [
{
icon: 'success',
nickname: 'Looking good',
id: 'core:elasticsearch@9.9.9',
message: 'Service is working',
since: expect.any(String),
state: 'green',
title: 'Green',
uiColor: 'secondary',
},
statuses: [
{
icon: 'success',
id: 'core:elasticsearch@9.9.9',
message: 'Service is working',
since: expect.any(String),
state: 'green',
uiColor: 'secondary',
},
{
icon: 'success',
id: 'core:savedObjects@9.9.9',
message: 'Service is working',
since: expect.any(String),
state: 'green',
uiColor: 'secondary',
},
{
icon: 'success',
id: 'plugin:a@9.9.9',
message: 'a is available',
since: expect.any(String),
state: 'green',
uiColor: 'secondary',
},
{
icon: 'warning',
id: 'plugin:b@9.9.9',
message: 'b is degraded',
since: expect.any(String),
state: 'yellow',
uiColor: 'warning',
},
{
icon: 'danger',
id: 'plugin:c@9.9.9',
message: 'c is unavailable',
since: expect.any(String),
state: 'red',
uiColor: 'danger',
},
{
icon: 'danger',
id: 'plugin:d@9.9.9',
message: 'd is critical',
since: expect.any(String),
state: 'red',
uiColor: 'danger',
},
],
});
{
icon: 'success',
id: 'core:savedObjects@9.9.9',
message: 'Service is working',
since: expect.any(String),
state: 'green',
uiColor: 'secondary',
},
{
icon: 'success',
id: 'plugin:a@9.9.9',
message: 'a is available',
since: expect.any(String),
state: 'green',
uiColor: 'secondary',
},
{
icon: 'warning',
id: 'plugin:b@9.9.9',
message: 'b is degraded',
since: expect.any(String),
state: 'yellow',
uiColor: 'warning',
},
{
icon: 'danger',
id: 'plugin:c@9.9.9',
message: 'c is unavailable',
since: expect.any(String),
state: 'red',
uiColor: 'danger',
},
{
icon: 'danger',
id: 'plugin:d@9.9.9',
message: 'd is critical',
since: expect.any(String),
state: 'red',
uiColor: 'danger',
},
],
};
it('returns legacy status format when v7format=true is provided', async () => {
await setupServer();
const result = await supertest(httpSetup.server.listener)
.get('/api/status?v7format=true')
.expect(200);
expect(result.body.status).toEqual(legacyFormat);
expect(incrementUsageCounter).toHaveBeenCalledTimes(1);
expect(incrementUsageCounter).toHaveBeenCalledWith({ counterName: 'status_v7format' });
});
it('returns legacy status format when v8format=false is provided', async () => {
@ -207,109 +217,105 @@ describe('GET /api/status', () => {
const result = await supertest(httpSetup.server.listener)
.get('/api/status?v8format=false')
.expect(200);
expect(result.body.status).toEqual({
overall: {
icon: 'success',
nickname: 'Looking good',
since: expect.any(String),
state: 'green',
title: 'Green',
uiColor: 'secondary',
},
statuses: [
{
icon: 'success',
id: 'core:elasticsearch@9.9.9',
message: 'Service is working',
since: expect.any(String),
state: 'green',
uiColor: 'secondary',
},
{
icon: 'success',
id: 'core:savedObjects@9.9.9',
message: 'Service is working',
since: expect.any(String),
state: 'green',
uiColor: 'secondary',
},
{
icon: 'success',
id: 'plugin:a@9.9.9',
message: 'a is available',
since: expect.any(String),
state: 'green',
uiColor: 'secondary',
},
{
icon: 'warning',
id: 'plugin:b@9.9.9',
message: 'b is degraded',
since: expect.any(String),
state: 'yellow',
uiColor: 'warning',
},
{
icon: 'danger',
id: 'plugin:c@9.9.9',
message: 'c is unavailable',
since: expect.any(String),
state: 'red',
uiColor: 'danger',
},
{
icon: 'danger',
id: 'plugin:d@9.9.9',
message: 'd is critical',
since: expect.any(String),
state: 'red',
uiColor: 'danger',
},
],
});
expect(result.body.status).toEqual(legacyFormat);
expect(incrementUsageCounter).toHaveBeenCalledTimes(1);
expect(incrementUsageCounter).toHaveBeenCalledWith({ counterName: 'status_v7format' });
});
});
describe('v8format', () => {
const newFormat = {
core: {
elasticsearch: {
level: 'available',
summary: 'Service is working',
},
savedObjects: {
level: 'available',
summary: 'Service is working',
},
},
overall: {
level: 'available',
summary: 'Service is working',
},
plugins: {
a: {
level: 'available',
summary: 'a is available',
},
b: {
level: 'degraded',
summary: 'b is degraded',
},
c: {
level: 'unavailable',
summary: 'c is unavailable',
},
d: {
level: 'critical',
summary: 'd is critical',
},
},
};
it('returns new status format when no query params are provided', async () => {
await setupServer();
const result = await supertest(httpSetup.server.listener).get('/api/status').expect(200);
expect(result.body.status).toEqual(newFormat);
expect(incrementUsageCounter).not.toHaveBeenCalled();
});
it('returns new status format when v8format=true is provided', async () => {
await setupServer();
const result = await supertest(httpSetup.server.listener)
.get('/api/status?v8format=true')
.expect(200);
expect(result.body.status).toEqual({
core: {
elasticsearch: {
level: 'available',
summary: 'Service is working',
},
savedObjects: {
level: 'available',
summary: 'Service is working',
},
},
overall: {
level: 'available',
summary: 'Service is working',
},
plugins: {
a: {
level: 'available',
summary: 'a is available',
},
b: {
level: 'degraded',
summary: 'b is degraded',
},
c: {
level: 'unavailable',
summary: 'c is unavailable',
},
d: {
level: 'critical',
summary: 'd is critical',
},
},
});
expect(result.body.status).toEqual(newFormat);
expect(incrementUsageCounter).not.toHaveBeenCalled();
});
it('returns new status format when v7format=false is provided', async () => {
await setupServer();
const result = await supertest(httpSetup.server.listener)
.get('/api/status?v7format=false')
.expect(200);
expect(result.body.status).toEqual(newFormat);
expect(incrementUsageCounter).not.toHaveBeenCalled();
});
});
describe('invalid query parameters', () => {
it('v8format=true and v7format=true', async () => {
await setupServer();
await supertest(httpSetup.server.listener)
.get('/api/status?v8format=true&v7format=true')
.expect(400);
expect(incrementUsageCounter).not.toHaveBeenCalled();
});
it('v8format=true and v7format=false', async () => {
await setupServer();
await supertest(httpSetup.server.listener)
.get('/api/status?v8format=true&v7format=false')
.expect(400);
expect(incrementUsageCounter).not.toHaveBeenCalled();
});
it('v8format=false and v7format=false', async () => {
await setupServer();
await supertest(httpSetup.server.listener)
.get('/api/status?v8format=false&v7format=false')
.expect(400);
expect(incrementUsageCounter).not.toHaveBeenCalled();
});
it('v8format=false and v7format=true', async () => {
await setupServer();
await supertest(httpSetup.server.listener)
.get('/api/status?v8format=false&v7format=true')
.expect(400);
expect(incrementUsageCounter).not.toHaveBeenCalled();
});
});
});

View file

@ -12,6 +12,7 @@ import { schema } from '@kbn/config-schema';
import { IRouter } from '../../http';
import { MetricsServiceSetup } from '../../metrics';
import type { CoreIncrementUsageCounter } from '../../core_usage_data/types';
import { ServiceStatus, CoreStatus, ServiceStatusLevels } from '../types';
import { PluginName } from '../../plugins';
import { calculateLegacyStatus, LegacyStatusInfo } from '../legacy_status';
@ -34,6 +35,7 @@ interface Deps {
core$: Observable<CoreStatus>;
plugins$: Observable<Record<PluginName, ServiceStatus>>;
};
incrementUsageCounter: CoreIncrementUsageCounter;
}
interface StatusInfo {
@ -47,7 +49,13 @@ interface StatusHttpBody extends Omit<StatusResponse, 'status'> {
status: StatusInfo | LegacyStatusInfo;
}
export const registerStatusRoute = ({ router, config, metrics, status }: Deps) => {
export const registerStatusRoute = ({
router,
config,
metrics,
status,
incrementUsageCounter,
}: Deps) => {
// Since the status.plugins$ observable is not subscribed to elsewhere, we need to subscribe it here to eagerly load
// the plugins status when Kibana starts up so this endpoint responds quickly on first boot.
const combinedStatus$ = new ReplaySubject<
@ -63,9 +71,19 @@ export const registerStatusRoute = ({ router, config, metrics, status }: Deps) =
tags: ['api'], // ensures that unauthenticated calls receive a 401 rather than a 302 redirect to login page
},
validate: {
query: schema.object({
v8format: schema.boolean({ defaultValue: false }),
}),
query: schema.object(
{
v7format: schema.maybe(schema.boolean()),
v8format: schema.maybe(schema.boolean()),
},
{
validate: ({ v7format, v8format }) => {
if (typeof v7format === 'boolean' && typeof v8format === 'boolean') {
return `provide only one format option: v7format or v8format`;
}
},
}
),
},
},
async (context, req, res) => {
@ -73,14 +91,17 @@ export const registerStatusRoute = ({ router, config, metrics, status }: Deps) =
const versionWithoutSnapshot = version.replace(SNAPSHOT_POSTFIX, '');
const [overall, core, plugins] = await combinedStatus$.pipe(first()).toPromise();
const { v8format = true, v7format = false } = req.query ?? {};
let statusInfo: StatusInfo | LegacyStatusInfo;
if (req.query?.v8format) {
if (!v7format && v8format) {
statusInfo = {
overall,
core,
plugins,
};
} else {
incrementUsageCounter({ counterName: 'status_v7format' });
statusInfo = calculateLegacyStatus({
overall,
core,

View file

@ -18,6 +18,7 @@ import { httpServiceMock } from '../http/http_service.mock';
import { mockRouter, RouterMock } from '../http/router/router.mock';
import { metricsServiceMock } from '../metrics/metrics_service.mock';
import { configServiceMock } from '../config/mocks';
import { coreUsageDataServiceMock } from '../core_usage_data/core_usage_data_service.mock';
expect.addSnapshotSerializer(ServiceStatusLevelSnapshotSerializer);
@ -51,6 +52,7 @@ describe('StatusService', () => {
environment: environmentServiceMock.createSetupContract(),
http: httpServiceMock.createInternalSetupContract(),
metrics: metricsServiceMock.createInternalSetupContract(),
coreUsageData: coreUsageDataServiceMock.createSetupContract(),
...overrides,
};
};

View file

@ -20,6 +20,7 @@ import { PluginName } from '../plugins';
import { InternalMetricsServiceSetup } from '../metrics';
import { registerStatusRoute } from './routes';
import { InternalEnvironmentServiceSetup } from '../environment';
import type { InternalCoreUsageDataSetup } from '../core_usage_data';
import { config, StatusConfigType } from './status_config';
import { ServiceStatus, CoreStatus, InternalStatusServiceSetup } from './types';
@ -38,6 +39,7 @@ interface SetupDeps {
http: InternalHttpServiceSetup;
metrics: InternalMetricsServiceSetup;
savedObjects: Pick<InternalSavedObjectsServiceSetup, 'status$'>;
coreUsageData: Pick<InternalCoreUsageDataSetup, 'incrementUsageCounter'>;
}
export class StatusService implements CoreService<InternalStatusServiceSetup> {
@ -61,6 +63,7 @@ export class StatusService implements CoreService<InternalStatusServiceSetup> {
metrics,
savedObjects,
environment,
coreUsageData,
}: SetupDeps) {
const statusConfig = await this.config$.pipe(take(1)).toPromise();
const core$ = this.setupCoreStatus({ elasticsearch, savedObjects });
@ -101,6 +104,7 @@ export class StatusService implements CoreService<InternalStatusServiceSetup> {
plugins$: this.pluginsStatus.getAll$(),
core$,
},
incrementUsageCounter: coreUsageData.incrementUsageCounter,
};
const router = http.createRouter('');

View file

@ -42,6 +42,8 @@ describe('kibana_usage_collection', () => {
expect(pluginInstance.setup(coreSetup, { usageCollection })).toBe(undefined);
expect(coreSetup.coreUsageData.registerUsageCounter).toHaveBeenCalled();
await expect(
Promise.all(
usageCollectors.map(async (usageCollector) => {

View file

@ -73,6 +73,7 @@ export class KibanaUsageCollectionPlugin implements Plugin {
public setup(coreSetup: CoreSetup, { usageCollection }: KibanaUsageCollectionPluginsDepsSetup) {
usageCollection.createUsageCounter('uiCounters');
this.eventLoopUsageCounter = usageCollection.createUsageCounter('eventLoop');
coreSetup.coreUsageData.registerUsageCounter(usageCollection.createUsageCounter('core'));
this.registerUsageCollectors(
usageCollection,
coreSetup,

View file

@ -25,9 +25,10 @@ export default function ({ getService }) {
expect(body.version.build_number).to.be.a('number');
expect(body.status.overall).to.be.an('object');
expect(body.status.overall.state).to.be('green');
expect(body.status.overall.level).to.be('available');
expect(body.status.statuses).to.be.an('array');
expect(body.status.core).to.be.an('object');
expect(body.status.plugins).to.be.an('object');
expect(body.metrics.collection_interval_in_millis).to.be.a('number');

View file

@ -16,7 +16,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide
const delay = (ms: number) => new Promise((r) => setTimeout(r, ms));
const getStatus = async (pluginName?: string) => {
const resp = await supertest.get('/api/status?v8format=true');
const resp = await supertest.get('/api/status');
if (pluginName) {
return resp.body.status.plugins[pluginName];

View file

@ -18,7 +18,7 @@ export default function ({ getService }: FtrProviderContext) {
const retry = getService('retry');
const getStatus = async (pluginName: string): Promise<ServiceStatusSerialized> => {
const resp = await supertest.get('/api/status?v8format=true');
const resp = await supertest.get('/api/status');
return resp.body.status.plugins[pluginName];
};