Core usage data (#79101)

* Core Telemetry service

* CoreTelemetryService mock

* Add missing config values back, cleanup

* Core usage collector

* HttpConfig path is 'server'

* Fix tests

* CoreTelemetry -> CoreUsageData

* Improve tests / docs

* Fix telemetry_check

* Don't catch fetch function exceptions, let usage collector handle it

* Code review

* Collect saved object index usage data

* Fix tests and telemetry_check

* explicitly import/export usage data types for telemetry_check

* Remove OS data for now, test for SO usage data

* Fix tests

* Polish core docs

* This shouldn't be here
This commit is contained in:
Rudolf Meijering 2020-10-05 21:50:07 +02:00 committed by GitHub
parent 7eeb4eed83
commit cd383800e3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 1462 additions and 4 deletions

View file

@ -0,0 +1,153 @@
/*
* 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 { PublicMethodsOf } from '@kbn/utility-types';
import { BehaviorSubject } from 'rxjs';
import { CoreUsageDataService } from './core_usage_data_service';
import { CoreUsageData, CoreUsageDataStart } from './types';
const createStartContractMock = () => {
const startContract: jest.Mocked<CoreUsageDataStart> = {
getCoreUsageData: jest.fn().mockResolvedValue(
new BehaviorSubject<CoreUsageData>({
config: {
elasticsearch: {
apiVersion: 'master',
customHeadersConfigured: false,
healthCheckDelayMs: 2500,
logQueries: false,
numberOfHostsConfigured: 1,
pingTimeoutMs: 30000,
requestHeadersWhitelistConfigured: false,
requestTimeoutMs: 30000,
shardTimeoutMs: 30000,
sniffIntervalMs: -1,
sniffOnConnectionFault: false,
sniffOnStart: false,
ssl: {
alwaysPresentCertificate: false,
certificateAuthoritiesConfigured: false,
certificateConfigured: false,
keyConfigured: false,
verificationMode: 'full',
keystoreConfigured: false,
truststoreConfigured: false,
},
},
http: {
basePathConfigured: false,
compression: {
enabled: true,
referrerWhitelistConfigured: false,
},
keepaliveTimeout: 120000,
maxPayloadInBytes: 1048576,
requestId: {
allowFromAnyIp: false,
ipAllowlistConfigured: false,
},
rewriteBasePath: false,
socketTimeout: 120000,
ssl: {
certificateAuthoritiesConfigured: false,
certificateConfigured: false,
cipherSuites: [
'ECDHE-RSA-AES128-GCM-SHA256',
'ECDHE-ECDSA-AES128-GCM-SHA256',
'ECDHE-RSA-AES256-GCM-SHA384',
'ECDHE-ECDSA-AES256-GCM-SHA384',
'DHE-RSA-AES128-GCM-SHA256',
'ECDHE-RSA-AES128-SHA256',
'DHE-RSA-AES128-SHA256',
'ECDHE-RSA-AES256-SHA384',
'DHE-RSA-AES256-SHA384',
'ECDHE-RSA-AES256-SHA256',
'DHE-RSA-AES256-SHA256',
'HIGH',
'!aNULL',
'!eNULL',
'!EXPORT',
'!DES',
'!RC4',
'!MD5',
'!PSK',
'!SRP',
'!CAMELLIA',
],
clientAuthentication: 'none',
keyConfigured: false,
keystoreConfigured: false,
redirectHttpFromPortConfigured: false,
supportedProtocols: ['TLSv1.1', 'TLSv1.2'],
truststoreConfigured: false,
},
xsrf: {
disableProtection: false,
whitelistConfigured: false,
},
},
logging: {
appendersTypesUsed: [],
loggersConfiguredCount: 0,
},
savedObjects: {
maxImportExportSizeBytes: 10000,
maxImportPayloadBytes: 10485760,
},
},
environment: {
memory: {
heapSizeLimit: 1,
heapTotalBytes: 1,
heapUsedBytes: 1,
},
},
services: {
savedObjects: {
indices: [
{
docsCount: 1,
docsDeleted: 1,
alias: 'test_index',
primaryStoreSizeBytes: 1,
storeSizeBytes: 1,
},
],
},
},
})
),
};
return startContract;
};
const createMock = () => {
const mocked: jest.Mocked<PublicMethodsOf<CoreUsageDataService>> = {
setup: jest.fn(),
start: jest.fn().mockReturnValue(createStartContractMock()),
stop: jest.fn(),
};
return mocked;
};
export const coreUsageDataServiceMock = {
create: createMock,
createStartContract: createStartContractMock,
};

View file

@ -0,0 +1,259 @@
/*
* 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 { BehaviorSubject, Observable } from 'rxjs';
import { HotObservable } from 'rxjs/internal/testing/HotObservable';
import { TestScheduler } from 'rxjs/testing';
import { configServiceMock } from '../config/mocks';
import { mockCoreContext } from '../core_context.mock';
import { config as RawElasticsearchConfig } from '../elasticsearch/elasticsearch_config';
import { config as RawHttpConfig } from '../http/http_config';
import { config as RawLoggingConfig } from '../logging/logging_config';
import { config as RawKibanaConfig } from '../kibana_config';
import { savedObjectsConfig as RawSavedObjectsConfig } from '../saved_objects/saved_objects_config';
import { metricsServiceMock } from '../metrics/metrics_service.mock';
import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service.mock';
import { CoreUsageDataService } from './core_usage_data_service';
import { elasticsearchServiceMock } from '../elasticsearch/elasticsearch_service.mock';
describe('CoreUsageDataService', () => {
const getTestScheduler = () =>
new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
let service: CoreUsageDataService;
const configService = configServiceMock.create();
configService.atPath.mockImplementation((path) => {
if (path === 'elasticsearch') {
return new BehaviorSubject(RawElasticsearchConfig.schema.validate({}));
} else if (path === 'server') {
return new BehaviorSubject(RawHttpConfig.schema.validate({}));
} else if (path === 'logging') {
return new BehaviorSubject(RawLoggingConfig.schema.validate({}));
} else if (path === 'savedObjects') {
return new BehaviorSubject(RawSavedObjectsConfig.schema.validate({}));
} else if (path === 'kibana') {
return new BehaviorSubject(RawKibanaConfig.schema.validate({}));
}
return new BehaviorSubject({});
});
const coreContext = mockCoreContext.create({ configService });
beforeEach(() => {
service = new CoreUsageDataService(coreContext);
});
describe('start', () => {
describe('getCoreUsageData', () => {
it('returns core metrics for default config', () => {
const metrics = metricsServiceMock.createInternalSetupContract();
service.setup({ metrics });
const elasticsearch = elasticsearchServiceMock.createStart();
elasticsearch.client.asInternalUser.cat.indices.mockResolvedValueOnce({
body: [
{
name: '.kibana_task_manager_1',
'docs.count': 10,
'docs.deleted': 10,
'store.size': 1000,
'pri.store.size': 2000,
},
],
} as any);
elasticsearch.client.asInternalUser.cat.indices.mockResolvedValueOnce({
body: [
{
name: '.kibana_1',
'docs.count': 20,
'docs.deleted': 20,
'store.size': 2000,
'pri.store.size': 4000,
},
],
} as any);
const typeRegistry = savedObjectsServiceMock.createTypeRegistryMock();
typeRegistry.getAllTypes.mockReturnValue([
{ name: 'type 1', indexPattern: '.kibana' },
{ name: 'type 2', indexPattern: '.kibana_task_manager' },
] as any);
const { getCoreUsageData } = service.start({
savedObjects: savedObjectsServiceMock.createInternalStartContract(typeRegistry),
elasticsearch,
});
expect(getCoreUsageData()).resolves.toMatchInlineSnapshot(`
Object {
"config": Object {
"elasticsearch": Object {
"apiVersion": "master",
"customHeadersConfigured": false,
"healthCheckDelayMs": 2500,
"logQueries": false,
"numberOfHostsConfigured": 1,
"pingTimeoutMs": 30000,
"requestHeadersWhitelistConfigured": false,
"requestTimeoutMs": 30000,
"shardTimeoutMs": 30000,
"sniffIntervalMs": -1,
"sniffOnConnectionFault": false,
"sniffOnStart": false,
"ssl": Object {
"alwaysPresentCertificate": false,
"certificateAuthoritiesConfigured": false,
"certificateConfigured": false,
"keyConfigured": false,
"keystoreConfigured": false,
"truststoreConfigured": false,
"verificationMode": "full",
},
},
"http": Object {
"basePathConfigured": false,
"compression": Object {
"enabled": true,
"referrerWhitelistConfigured": false,
},
"keepaliveTimeout": 120000,
"maxPayloadInBytes": 1048576,
"requestId": Object {
"allowFromAnyIp": false,
"ipAllowlistConfigured": false,
},
"rewriteBasePath": false,
"socketTimeout": 120000,
"ssl": Object {
"certificateAuthoritiesConfigured": false,
"certificateConfigured": false,
"cipherSuites": Array [
"ECDHE-RSA-AES128-GCM-SHA256",
"ECDHE-ECDSA-AES128-GCM-SHA256",
"ECDHE-RSA-AES256-GCM-SHA384",
"ECDHE-ECDSA-AES256-GCM-SHA384",
"DHE-RSA-AES128-GCM-SHA256",
"ECDHE-RSA-AES128-SHA256",
"DHE-RSA-AES128-SHA256",
"ECDHE-RSA-AES256-SHA384",
"DHE-RSA-AES256-SHA384",
"ECDHE-RSA-AES256-SHA256",
"DHE-RSA-AES256-SHA256",
"HIGH",
"!aNULL",
"!eNULL",
"!EXPORT",
"!DES",
"!RC4",
"!MD5",
"!PSK",
"!SRP",
"!CAMELLIA",
],
"clientAuthentication": "none",
"keyConfigured": false,
"keystoreConfigured": false,
"redirectHttpFromPortConfigured": false,
"supportedProtocols": Array [
"TLSv1.1",
"TLSv1.2",
],
"truststoreConfigured": false,
},
"xsrf": Object {
"disableProtection": false,
"whitelistConfigured": false,
},
},
"logging": Object {
"appendersTypesUsed": Array [],
"loggersConfiguredCount": 0,
},
"savedObjects": Object {
"maxImportExportSizeBytes": 10000,
"maxImportPayloadBytes": 10485760,
},
},
"environment": Object {
"memory": Object {
"heapSizeLimit": 1,
"heapTotalBytes": 1,
"heapUsedBytes": 1,
},
},
"services": Object {
"savedObjects": Object {
"indices": Array [
Object {
"alias": ".kibana",
"docsCount": 10,
"docsDeleted": 10,
"primaryStoreSizeBytes": 2000,
"storeSizeBytes": 1000,
},
Object {
"alias": ".kibana_task_manager",
"docsCount": 20,
"docsDeleted": 20,
"primaryStoreSizeBytes": 4000,
"storeSizeBytes": 2000,
},
],
},
},
}
`);
});
});
});
describe('setup and stop', () => {
it('subscribes and unsubscribes from all config paths and metrics', () => {
getTestScheduler().run(({ cold, hot, expectSubscriptions }) => {
const observables: Array<HotObservable<string>> = [];
configService.atPath.mockImplementation(() => {
const newObservable = hot('-a-------');
observables.push(newObservable);
return newObservable;
});
const metrics = metricsServiceMock.createInternalSetupContract();
metrics.getOpsMetrics$.mockImplementation(() => {
const newObservable = hot('-a-------');
observables.push(newObservable);
return newObservable as Observable<any>;
});
service.setup({ metrics });
// Use the stopTimer$ to delay calling stop() until the third frame
const stopTimer$ = cold('---a|');
stopTimer$.subscribe(() => {
service.stop();
});
const subs = '^--!';
observables.forEach((o) => {
expectSubscriptions(o.subscriptions).toBe(subs);
});
});
});
});
});

View file

@ -0,0 +1,285 @@
/*
* 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 { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { CoreService } from 'src/core/types';
import { SavedObjectsServiceStart } from 'src/core/server';
import { CoreContext } from '../core_context';
import { ElasticsearchConfigType } from '../elasticsearch/elasticsearch_config';
import { HttpConfigType } from '../http';
import { LoggingConfigType } from '../logging';
import { SavedObjectsConfigType } from '../saved_objects/saved_objects_config';
import { CoreServicesUsageData, CoreUsageData, CoreUsageDataStart } from './types';
import { isConfigured } from './is_configured';
import { ElasticsearchServiceStart } from '../elasticsearch';
import { KibanaConfigType } from '../kibana_config';
import { MetricsServiceSetup, OpsMetrics } from '..';
export interface SetupDeps {
metrics: MetricsServiceSetup;
}
export interface StartDeps {
savedObjects: SavedObjectsServiceStart;
elasticsearch: ElasticsearchServiceStart;
}
/**
* Because users can configure their Saved Object to any arbitrary index name,
* we need to map customized index names back to a "standard" index name.
*
* e.g. If a user configures `kibana.index: .my_saved_objects` we want to the
* collected data to be grouped under `.kibana` not ".my_saved_objects".
*
* This is rather brittle, but the option to configure index names might go
* away completely anyway (see #60053).
*
* @param index The index name configured for this SO type
* @param kibanaConfigIndex The default kibana index as configured by the user
* with `kibana.index`
*/
const kibanaOrTaskManagerIndex = (index: string, kibanaConfigIndex: string) => {
return index === kibanaConfigIndex ? '.kibana' : '.kibana_task_manager';
};
export class CoreUsageDataService implements CoreService<void, CoreUsageDataStart> {
private elasticsearchConfig?: ElasticsearchConfigType;
private configService: CoreContext['configService'];
private httpConfig?: HttpConfigType;
private loggingConfig?: LoggingConfigType;
private soConfig?: SavedObjectsConfigType;
private stop$: Subject<void>;
private opsMetrics?: OpsMetrics;
private kibanaConfig?: KibanaConfigType;
constructor(core: CoreContext) {
this.configService = core.configService;
this.stop$ = new Subject();
}
private async getSavedObjectIndicesUsageData(
savedObjects: SavedObjectsServiceStart,
elasticsearch: ElasticsearchServiceStart
): Promise<CoreServicesUsageData['savedObjects']> {
const indices = await Promise.all(
Array.from(
savedObjects
.getTypeRegistry()
.getAllTypes()
.reduce((acc, type) => {
const index = type.indexPattern ?? this.kibanaConfig!.index;
return index != null ? acc.add(index) : acc;
}, new Set<string>())
.values()
).map((index) => {
// The _cat/indices API returns the _index_ and doesn't return a way
// to map back from the index to the alias. So we have to make an API
// call for every alias
return elasticsearch.client.asInternalUser.cat
.indices<any[]>({
index,
format: 'JSON',
bytes: 'b',
})
.then(({ body }) => {
const stats = body[0];
return {
alias: kibanaOrTaskManagerIndex(index, this.kibanaConfig!.index),
docsCount: stats['docs.count'],
docsDeleted: stats['docs.deleted'],
storeSizeBytes: stats['store.size'],
primaryStoreSizeBytes: stats['pri.store.size'],
};
});
})
);
return {
indices,
};
}
private async getCoreUsageData(
savedObjects: SavedObjectsServiceStart,
elasticsearch: ElasticsearchServiceStart
): Promise<CoreUsageData> {
if (
this.elasticsearchConfig == null ||
this.httpConfig == null ||
this.soConfig == null ||
this.opsMetrics == null
) {
throw new Error('Unable to read config values. Ensure that setup() has completed.');
}
const es = this.elasticsearchConfig;
const soUsageData = await this.getSavedObjectIndicesUsageData(savedObjects, elasticsearch);
const http = this.httpConfig;
return {
config: {
elasticsearch: {
apiVersion: es.apiVersion,
sniffOnStart: es.sniffOnStart,
sniffIntervalMs: es.sniffInterval !== false ? es.sniffInterval.asMilliseconds() : -1,
sniffOnConnectionFault: es.sniffOnConnectionFault,
numberOfHostsConfigured: Array.isArray(es.hosts)
? es.hosts.length
: isConfigured.string(es.hosts)
? 1
: 0,
customHeadersConfigured: isConfigured.record(es.customHeaders),
healthCheckDelayMs: es.healthCheck.delay.asMilliseconds(),
logQueries: es.logQueries,
pingTimeoutMs: es.pingTimeout.asMilliseconds(),
requestHeadersWhitelistConfigured: isConfigured.stringOrArray(
es.requestHeadersWhitelist,
['authorization']
),
requestTimeoutMs: es.requestTimeout.asMilliseconds(),
shardTimeoutMs: es.shardTimeout.asMilliseconds(),
ssl: {
alwaysPresentCertificate: es.ssl.alwaysPresentCertificate,
certificateAuthoritiesConfigured: isConfigured.stringOrArray(
es.ssl.certificateAuthorities
),
certificateConfigured: isConfigured.string(es.ssl.certificate),
keyConfigured: isConfigured.string(es.ssl.key),
verificationMode: es.ssl.verificationMode,
truststoreConfigured: isConfigured.record(es.ssl.truststore),
keystoreConfigured: isConfigured.record(es.ssl.keystore),
},
},
http: {
basePathConfigured: isConfigured.string(http.basePath),
maxPayloadInBytes: http.maxPayload.getValueInBytes(),
rewriteBasePath: http.rewriteBasePath,
keepaliveTimeout: http.keepaliveTimeout,
socketTimeout: http.socketTimeout,
compression: {
enabled: http.compression.enabled,
referrerWhitelistConfigured: isConfigured.array(http.compression.referrerWhitelist),
},
xsrf: {
disableProtection: http.xsrf.disableProtection,
whitelistConfigured: isConfigured.array(http.xsrf.whitelist),
},
requestId: {
allowFromAnyIp: http.requestId.allowFromAnyIp,
ipAllowlistConfigured: isConfigured.array(http.requestId.ipAllowlist),
},
ssl: {
certificateAuthoritiesConfigured: isConfigured.stringOrArray(
http.ssl.certificateAuthorities
),
certificateConfigured: isConfigured.string(http.ssl.certificate),
cipherSuites: http.ssl.cipherSuites,
keyConfigured: isConfigured.string(http.ssl.key),
redirectHttpFromPortConfigured: isConfigured.number(http.ssl.redirectHttpFromPort),
supportedProtocols: http.ssl.supportedProtocols,
clientAuthentication: http.ssl.clientAuthentication,
keystoreConfigured: isConfigured.record(http.ssl.keystore),
truststoreConfigured: isConfigured.record(http.ssl.truststore),
},
},
logging: {
appendersTypesUsed: Array.from(
Array.from(this.loggingConfig?.appenders.values() ?? [])
.reduce((acc, a) => acc.add(a.kind), new Set<string>())
.values()
),
loggersConfiguredCount: this.loggingConfig?.loggers.length ?? 0,
},
savedObjects: {
maxImportPayloadBytes: this.soConfig.maxImportPayloadBytes.getValueInBytes(),
maxImportExportSizeBytes: this.soConfig.maxImportExportSize.getValueInBytes(),
},
},
environment: {
memory: {
heapSizeLimit: this.opsMetrics.process.memory.heap.size_limit,
heapTotalBytes: this.opsMetrics.process.memory.heap.total_in_bytes,
heapUsedBytes: this.opsMetrics.process.memory.heap.used_in_bytes,
},
},
services: {
savedObjects: soUsageData,
},
};
}
setup({ metrics }: SetupDeps) {
metrics
.getOpsMetrics$()
.pipe(takeUntil(this.stop$))
.subscribe((opsMetrics) => (this.opsMetrics = opsMetrics));
this.configService
.atPath<ElasticsearchConfigType>('elasticsearch')
.pipe(takeUntil(this.stop$))
.subscribe((config) => {
this.elasticsearchConfig = config;
});
this.configService
.atPath<HttpConfigType>('server')
.pipe(takeUntil(this.stop$))
.subscribe((config) => {
this.httpConfig = config;
});
this.configService
.atPath<LoggingConfigType>('logging')
.pipe(takeUntil(this.stop$))
.subscribe((config) => {
this.loggingConfig = config;
});
this.configService
.atPath<SavedObjectsConfigType>('savedObjects')
.pipe(takeUntil(this.stop$))
.subscribe((config) => {
this.soConfig = config;
});
this.configService
.atPath<KibanaConfigType>('kibana')
.pipe(takeUntil(this.stop$))
.subscribe((config) => {
this.kibanaConfig = config;
});
}
start({ savedObjects, elasticsearch }: StartDeps) {
return {
getCoreUsageData: () => {
return this.getCoreUsageData(savedObjects, elasticsearch);
},
};
}
stop() {
this.stop$.next();
this.stop$.complete();
}
}

View file

@ -0,0 +1,31 @@
/*
* 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.
*/
export { CoreUsageDataStart } from './types';
export { CoreUsageDataService } from './core_usage_data_service';
// Because of #79265 we need to explicity import, then export these types for
// scripts/telemetry_check.js to work as expected
import {
CoreUsageData,
CoreConfigUsageData,
CoreEnvironmentUsageData,
CoreServicesUsageData,
} from './types';
export { CoreUsageData, CoreConfigUsageData, CoreEnvironmentUsageData, CoreServicesUsageData };

View file

@ -0,0 +1,137 @@
/*
* 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 { isConfigured } from './is_configured';
describe('isConfigured', () => {
describe('#string', () => {
it('returns true for a non-empty string', () => {
expect(isConfigured.string('I am configured')).toEqual(true);
});
it('returns false for an empty string', () => {
expect(isConfigured.string(' ')).toEqual(false);
expect(isConfigured.string(' ')).toEqual(false);
});
it('returns false for undefined', () => {
expect(isConfigured.string(undefined)).toEqual(false);
});
it('returns false for null', () => {
expect(isConfigured.string(null as any)).toEqual(false);
});
it('returns false for a record', () => {
expect(isConfigured.string({} as any)).toEqual(false);
expect(isConfigured.string({ key: 'hello' } as any)).toEqual(false);
});
it('returns false for an array', () => {
expect(isConfigured.string([] as any)).toEqual(false);
expect(isConfigured.string(['hello'] as any)).toEqual(false);
});
});
describe('array', () => {
it('returns true for a non-empty array', () => {
expect(isConfigured.array(['a'])).toEqual(true);
expect(isConfigured.array([{}])).toEqual(true);
expect(isConfigured.array([{ key: 'hello' }])).toEqual(true);
});
it('returns false for an empty array', () => {
expect(isConfigured.array([])).toEqual(false);
});
it('returns false for undefined', () => {
expect(isConfigured.array(undefined)).toEqual(false);
});
it('returns false for null', () => {
expect(isConfigured.array(null as any)).toEqual(false);
});
it('returns false for a string', () => {
expect(isConfigured.array('string')).toEqual(false);
});
it('returns false for a record', () => {
expect(isConfigured.array({} as any)).toEqual(false);
});
});
describe('stringOrArray', () => {
const arraySpy = jest.spyOn(isConfigured, 'array');
const stringSpy = jest.spyOn(isConfigured, 'string');
it('calls #array for an array', () => {
isConfigured.stringOrArray([]);
expect(arraySpy).toHaveBeenCalledWith([]);
});
it('calls #string for non-array values', () => {
isConfigured.stringOrArray('string');
expect(stringSpy).toHaveBeenCalledWith('string');
});
});
describe('record', () => {
it('returns true for a non-empty record', () => {
expect(isConfigured.record({ key: 'hello' })).toEqual(true);
expect(isConfigured.record({ key: undefined })).toEqual(true);
});
it('returns false for an empty record', () => {
expect(isConfigured.record({})).toEqual(false);
});
it('returns false for undefined', () => {
expect(isConfigured.record(undefined)).toEqual(false);
});
it('returns false for null', () => {
expect(isConfigured.record(null as any)).toEqual(false);
});
});
describe('number', () => {
it('returns true for a valid number', () => {
expect(isConfigured.number(0)).toEqual(true);
expect(isConfigured.number(-0)).toEqual(true);
expect(isConfigured.number(1)).toEqual(true);
expect(isConfigured.number(-0)).toEqual(true);
});
it('returns false for NaN', () => {
expect(isConfigured.number(Number.NaN)).toEqual(false);
});
it('returns false for a string', () => {
expect(isConfigured.number('1' as any)).toEqual(false);
expect(isConfigured.number('' as any)).toEqual(false);
});
it('returns false for undefined', () => {
expect(isConfigured.number(undefined)).toEqual(false);
});
it('returns false for null', () => {
expect(isConfigured.number(null as any)).toEqual(false);
});
});
});

View file

@ -0,0 +1,65 @@
/*
* 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 { isEqual } from 'lodash';
/**
* Test whether a given config value is configured based on it's schema type.
* Our configuration schema and code often accept and ignore empty values like
* `elasticsearch.customHeaders: {}`. However, for telemetry purposes, we're
* only interested when these values have been set to something that will
* change the behaviour of Kibana.
*/
export const isConfigured = {
/**
* config is a string with non-zero length
*/
string: (config?: string): boolean => {
return (config?.trim?.()?.length ?? 0) > 0;
},
/**
* config is an array with non-zero length
*/
array: (config?: unknown[] | string, defaultValue?: any): boolean => {
return Array.isArray(config)
? (config?.length ?? 0) > 0 && !isEqual(config, defaultValue)
: false;
},
/**
* config is a string or array of strings where each element has non-zero length
*/
stringOrArray: (config?: string[] | string, defaultValue?: any): boolean => {
return Array.isArray(config)
? isConfigured.array(config, defaultValue)
: isConfigured.string(config);
},
/**
* config is a record with at least one key
*/
record: (config?: Record<string, unknown>): boolean => {
return config != null && typeof config === 'object' && Object.keys(config).length > 0;
},
/**
* config is a number
*/
number: (config?: number): boolean => {
// kbn-config-schema already does NaN validation, but doesn't hurt to be sure
return typeof config === 'number' && !isNaN(config);
},
};

View file

@ -0,0 +1,162 @@
/*
* 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.
*/
/**
* Type describing Core's usage data payload
* @internal
*/
export interface CoreUsageData {
config: CoreConfigUsageData;
services: CoreServicesUsageData;
environment: CoreEnvironmentUsageData;
}
/**
* Usage data from Core services
* @internal
*/
export interface CoreServicesUsageData {
savedObjects: {
// scripts/telemetry_check.js does not support parsing Array<{...}> types
// so we have to disable eslint here and use {...}[]
// eslint-disable-next-line @typescript-eslint/array-type
indices: {
alias: string;
docsCount: number;
docsDeleted: number;
storeSizeBytes: number;
primaryStoreSizeBytes: number;
}[];
};
}
/**
* Usage data on this Kibana node's runtime environment.
* @internal
*/
export interface CoreEnvironmentUsageData {
memory: {
heapTotalBytes: number;
heapUsedBytes: number;
/** V8 heap size limit */
heapSizeLimit: number;
};
}
/**
* Usage data on this cluster's configuration of Core features
* @internal
*/
export interface CoreConfigUsageData {
elasticsearch: {
sniffOnStart: boolean;
sniffIntervalMs?: number;
sniffOnConnectionFault: boolean;
numberOfHostsConfigured: number;
requestHeadersWhitelistConfigured: boolean;
customHeadersConfigured: boolean;
shardTimeoutMs: number;
requestTimeoutMs: number;
pingTimeoutMs: number;
logQueries: boolean;
ssl: {
verificationMode: 'none' | 'certificate' | 'full';
certificateAuthoritiesConfigured: boolean;
certificateConfigured: boolean;
keyConfigured: boolean;
keystoreConfigured: boolean;
truststoreConfigured: boolean;
alwaysPresentCertificate: boolean;
};
apiVersion: string;
healthCheckDelayMs: number;
};
http: {
basePathConfigured: boolean;
maxPayloadInBytes: number;
rewriteBasePath: boolean;
keepaliveTimeout: number;
socketTimeout: number;
compression: {
enabled: boolean;
referrerWhitelistConfigured: boolean;
};
xsrf: {
disableProtection: boolean;
whitelistConfigured: boolean;
};
requestId: {
allowFromAnyIp: boolean;
ipAllowlistConfigured: boolean;
};
ssl: {
certificateAuthoritiesConfigured: boolean;
certificateConfigured: boolean;
cipherSuites: string[];
keyConfigured: boolean;
keystoreConfigured: boolean;
truststoreConfigured: boolean;
redirectHttpFromPortConfigured: boolean;
supportedProtocols: string[];
clientAuthentication: 'none' | 'optional' | 'required';
};
};
logging: {
appendersTypesUsed: string[];
loggersConfiguredCount: number;
};
// plugins: {
// /** list of built-in plugins that are disabled */
// firstPartyDisabled: string[];
// /** list of third-party plugins that are installed and enabled */
// thirdParty: string[];
// };
savedObjects: {
maxImportPayloadBytes: number;
maxImportExportSizeBytes: number;
};
// uiSettings: {
// overridesCount: number;
// };
}
/**
* Internal API for getting 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 CoreUsageDataStart {
/**
* Internal API for getting Core's usage data payload.
*
* @note This API should never be used to drive application logic and is only
* intended for telemetry purposes.
*
* @internal
* */
getCoreUsageData(): Promise<CoreUsageData>;
}

View file

@ -64,6 +64,18 @@ import { MetricsServiceSetup, MetricsServiceStart } from './metrics';
import { StatusServiceSetup } from './status';
import { Auditor, AuditTrailSetup, AuditTrailStart } from './audit_trail';
import { AppenderConfigType, appendersSchema, LoggingServiceSetup } from './logging';
import { CoreUsageDataStart } from './core_usage_data';
// Because of #79265 we need to explicity import, then export these types for
// scripts/telemetry_check.js to work as expected
import {
CoreUsageData,
CoreConfigUsageData,
CoreEnvironmentUsageData,
CoreServicesUsageData,
} from './core_usage_data';
export { CoreUsageData, CoreConfigUsageData, CoreEnvironmentUsageData, CoreServicesUsageData };
export { AuditableEvent, Auditor, AuditorFactory, AuditTrailSetup } from './audit_trail';
export { bootstrap } from './bootstrap';
@ -349,6 +361,8 @@ export {
StatusServiceSetup,
} from './status';
export { CoreUsageDataStart } from './core_usage_data';
/**
* Plugin specific context passed to a route handler.
*
@ -456,6 +470,8 @@ export interface CoreStart {
uiSettings: UiSettingsServiceStart;
/** {@link AuditTrailSetup} */
auditTrail: AuditTrailStart;
/** @internal {@link CoreUsageDataStart} */
coreUsageData: CoreUsageDataStart;
}
export {

View file

@ -39,6 +39,7 @@ import { InternalHttpResourcesSetup } from './http_resources';
import { InternalStatusServiceSetup } from './status';
import { AuditTrailSetup, AuditTrailStart } from './audit_trail';
import { InternalLoggingServiceSetup } from './logging';
import { CoreUsageDataStart } from './core_usage_data';
/** @internal */
export interface InternalCoreSetup {
@ -68,6 +69,7 @@ export interface InternalCoreStart {
savedObjects: InternalSavedObjectsServiceStart;
uiSettings: InternalUiSettingsServiceStart;
auditTrail: AuditTrailStart;
coreUsageData: CoreUsageDataStart;
}
/**

View file

@ -217,6 +217,11 @@ export class LegacyService implements CoreService {
},
uiSettings: { asScopedToClient: startDeps.core.uiSettings.asScopedToClient },
auditTrail: startDeps.core.auditTrail,
coreUsageData: {
getCoreUsageData: () => {
throw new Error('core.start.coreUsageData.getCoreUsageData is unsupported in legacy');
},
},
};
const router = setupDeps.core.http.createRouter('', this.legacyId);

View file

@ -37,6 +37,7 @@ import { metricsServiceMock } from './metrics/metrics_service.mock';
import { environmentServiceMock } from './environment/environment_service.mock';
import { statusServiceMock } from './status/status_service.mock';
import { auditTrailServiceMock } from './audit_trail/audit_trail_service.mock';
import { coreUsageDataServiceMock } from './core_usage_data/core_usage_data_service.mock';
export { configServiceMock } from './config/mocks';
export { httpServerMock } from './http/http_server.mocks';
@ -55,6 +56,7 @@ export { renderingMock } from './rendering/rendering_service.mock';
export { statusServiceMock } from './status/status_service.mock';
export { contextServiceMock } from './context/context_service.mock';
export { capabilitiesServiceMock } from './capabilities/capabilities_service.mock';
export { coreUsageDataServiceMock } from './core_usage_data/core_usage_data_service.mock';
export function pluginInitializerContextConfigMock<T>(config: T) {
const globalConfig: SharedGlobalConfig = {
@ -157,6 +159,7 @@ function createCoreStartMock() {
metrics: metricsServiceMock.createStartContract(),
savedObjects: savedObjectsServiceMock.createStartContract(),
uiSettings: uiSettingsServiceMock.createStartContract(),
coreUsageData: coreUsageDataServiceMock.createStartContract(),
};
return mock;
@ -190,6 +193,7 @@ function createInternalCoreStartMock() {
savedObjects: savedObjectsServiceMock.createInternalStartContract(),
uiSettings: uiSettingsServiceMock.createStartContract(),
auditTrail: auditTrailServiceMock.createStartContract(),
coreUsageData: coreUsageDataServiceMock.createStartContract(),
};
return startDeps;
}

View file

@ -251,5 +251,6 @@ export function createPluginStartContext<TPlugin, TPluginDependencies>(
asScopedToClient: deps.uiSettings.asScopedToClient,
},
auditTrail: deps.auditTrail,
coreUsageData: deps.coreUsageData,
};
}

View file

@ -33,10 +33,11 @@ import { savedObjectsClientMock } from './service/saved_objects_client.mock';
import { typeRegistryMock } from './saved_objects_type_registry.mock';
import { migrationMocks } from './migrations/mocks';
import { ServiceStatusLevels } from '../status';
import { ISavedObjectTypeRegistry } from './saved_objects_type_registry';
type SavedObjectsServiceContract = PublicMethodsOf<SavedObjectsService>;
const createStartContractMock = () => {
const createStartContractMock = (typeRegistry?: jest.Mocked<ISavedObjectTypeRegistry>) => {
const startContrat: jest.Mocked<SavedObjectsServiceStart> = {
getScopedClient: jest.fn(),
createInternalRepository: jest.fn(),
@ -48,13 +49,15 @@ const createStartContractMock = () => {
startContrat.getScopedClient.mockReturnValue(savedObjectsClientMock.create());
startContrat.createInternalRepository.mockReturnValue(savedObjectsRepositoryMock.create());
startContrat.createScopedRepository.mockReturnValue(savedObjectsRepositoryMock.create());
startContrat.getTypeRegistry.mockReturnValue(typeRegistryMock.create());
startContrat.getTypeRegistry.mockReturnValue(typeRegistry ?? typeRegistryMock.create());
return startContrat;
};
const createInternalStartContractMock = () => {
const internalStartContract: jest.Mocked<InternalSavedObjectsServiceStart> = createStartContractMock();
const createInternalStartContractMock = (typeRegistry?: jest.Mocked<ISavedObjectTypeRegistry>) => {
const internalStartContract: jest.Mocked<InternalSavedObjectsServiceStart> = createStartContractMock(
typeRegistry
);
return internalStartContract;
};

View file

@ -401,9 +401,102 @@ export interface ContextSetup {
createContextContainer<THandler extends HandlerFunction<any>>(): IContextContainer<THandler>;
}
// @internal
export interface CoreConfigUsageData {
// (undocumented)
elasticsearch: {
sniffOnStart: boolean;
sniffIntervalMs?: number;
sniffOnConnectionFault: boolean;
numberOfHostsConfigured: number;
requestHeadersWhitelistConfigured: boolean;
customHeadersConfigured: boolean;
shardTimeoutMs: number;
requestTimeoutMs: number;
pingTimeoutMs: number;
logQueries: boolean;
ssl: {
verificationMode: 'none' | 'certificate' | 'full';
certificateAuthoritiesConfigured: boolean;
certificateConfigured: boolean;
keyConfigured: boolean;
keystoreConfigured: boolean;
truststoreConfigured: boolean;
alwaysPresentCertificate: boolean;
};
apiVersion: string;
healthCheckDelayMs: number;
};
// (undocumented)
http: {
basePathConfigured: boolean;
maxPayloadInBytes: number;
rewriteBasePath: boolean;
keepaliveTimeout: number;
socketTimeout: number;
compression: {
enabled: boolean;
referrerWhitelistConfigured: boolean;
};
xsrf: {
disableProtection: boolean;
whitelistConfigured: boolean;
};
requestId: {
allowFromAnyIp: boolean;
ipAllowlistConfigured: boolean;
};
ssl: {
certificateAuthoritiesConfigured: boolean;
certificateConfigured: boolean;
cipherSuites: string[];
keyConfigured: boolean;
keystoreConfigured: boolean;
truststoreConfigured: boolean;
redirectHttpFromPortConfigured: boolean;
supportedProtocols: string[];
clientAuthentication: 'none' | 'optional' | 'required';
};
};
// (undocumented)
logging: {
appendersTypesUsed: string[];
loggersConfiguredCount: number;
};
// (undocumented)
savedObjects: {
maxImportPayloadBytes: number;
maxImportExportSizeBytes: number;
};
}
// @internal
export interface CoreEnvironmentUsageData {
// (undocumented)
memory: {
heapTotalBytes: number;
heapUsedBytes: number;
heapSizeLimit: number;
};
}
// @internal (undocumented)
export type CoreId = symbol;
// @internal
export interface CoreServicesUsageData {
// (undocumented)
savedObjects: {
indices: {
alias: string;
docsCount: number;
docsDeleted: number;
storeSizeBytes: number;
primaryStoreSizeBytes: number;
}[];
};
}
// @public
export interface CoreSetup<TPluginsStart extends object = object, TStart = unknown> {
// (undocumented)
@ -438,6 +531,8 @@ export interface CoreStart {
auditTrail: AuditTrailStart;
// (undocumented)
capabilities: CapabilitiesStart;
// @internal (undocumented)
coreUsageData: CoreUsageDataStart;
// (undocumented)
elasticsearch: ElasticsearchServiceStart;
// (undocumented)
@ -458,6 +553,21 @@ export interface CoreStatus {
savedObjects: ServiceStatus;
}
// @internal
export interface CoreUsageData {
// (undocumented)
config: CoreConfigUsageData;
// (undocumented)
environment: CoreEnvironmentUsageData;
// (undocumented)
services: CoreServicesUsageData;
}
// @internal
export interface CoreUsageDataStart {
getCoreUsageData(): Promise<CoreUsageData>;
}
// @public (undocumented)
export interface CountResponse {
// (undocumented)

View file

@ -48,6 +48,7 @@ import { config as statusConfig } from './status';
import { ContextService } from './context';
import { RequestHandlerContext } from '.';
import { InternalCoreSetup, InternalCoreStart, ServiceConfigDescriptor } from './internal_types';
import { CoreUsageDataService } from './core_usage_data';
import { CoreRouteHandlerContext } from './core_route_handler_context';
const coreId = Symbol('core');
@ -72,6 +73,7 @@ export class Server {
private readonly logging: LoggingService;
private readonly coreApp: CoreApp;
private readonly auditTrail: AuditTrailService;
private readonly coreUsageData: CoreUsageDataService;
#pluginsInitialized?: boolean;
private coreStart?: InternalCoreStart;
@ -103,6 +105,7 @@ export class Server {
this.httpResources = new HttpResourcesService(core);
this.auditTrail = new AuditTrailService(core);
this.logging = new LoggingService(core);
this.coreUsageData = new CoreUsageDataService(core);
}
public async setup() {
@ -184,6 +187,8 @@ export class Server {
loggingSystem: this.loggingSystem,
});
this.coreUsageData.setup({ metrics: metricsSetup });
const coreSetup: InternalCoreSetup = {
capabilities: capabilitiesSetup,
context: contextServiceSetup,
@ -235,6 +240,10 @@ export class Server {
const uiSettingsStart = await this.uiSettings.start();
const metricsStart = await this.metrics.start();
const httpStart = this.http.getStartContract();
const coreUsageDataStart = this.coreUsageData.start({
elasticsearch: elasticsearchStart,
savedObjects: savedObjectsStart,
});
this.coreStart = {
capabilities: capabilitiesStart,
@ -244,6 +253,7 @@ export class Server {
savedObjects: savedObjectsStart,
uiSettings: uiSettingsStart,
auditTrail: auditTrailStart,
coreUsageData: coreUsageDataStart,
};
const pluginsStart = await this.plugins.start(this.coreStart);

View file

@ -8,3 +8,4 @@ This plugin registers the basic usage collectors from Kibana:
- Number of Saved Objects per type
- Non-default UI Settings
- CSP configuration
- Core Metrics

View file

@ -11,3 +11,5 @@ exports[`kibana_usage_collection Runs the setup method without issues 4`] = `fal
exports[`kibana_usage_collection Runs the setup method without issues 5`] = `false`;
exports[`kibana_usage_collection Runs the setup method without issues 6`] = `true`;
exports[`kibana_usage_collection Runs the setup method without issues 7`] = `false`;

View file

@ -0,0 +1,132 @@
/*
* 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 { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { CoreUsageData, CoreUsageDataStart } from '../../../../../core/server';
export function getCoreUsageCollector(
usageCollection: UsageCollectionSetup,
getCoreUsageDataService: () => CoreUsageDataStart
) {
return usageCollection.makeUsageCollector<CoreUsageData, { core: CoreUsageData }>({
type: 'core',
isReady: () => typeof getCoreUsageDataService() !== 'undefined',
schema: {
config: {
elasticsearch: {
sniffOnStart: { type: 'boolean' },
sniffIntervalMs: { type: 'long' },
sniffOnConnectionFault: { type: 'boolean' },
numberOfHostsConfigured: { type: 'long' },
requestHeadersWhitelistConfigured: { type: 'boolean' },
customHeadersConfigured: { type: 'boolean' },
shardTimeoutMs: { type: 'long' },
requestTimeoutMs: { type: 'long' },
pingTimeoutMs: { type: 'long' },
logQueries: { type: 'boolean' },
ssl: {
verificationMode: { type: 'keyword' },
certificateAuthoritiesConfigured: { type: 'boolean' },
certificateConfigured: { type: 'boolean' },
keyConfigured: { type: 'boolean' },
keystoreConfigured: { type: 'boolean' },
truststoreConfigured: { type: 'boolean' },
alwaysPresentCertificate: { type: 'boolean' },
},
apiVersion: { type: 'keyword' },
healthCheckDelayMs: { type: 'long' },
},
http: {
basePathConfigured: { type: 'boolean' },
maxPayloadInBytes: { type: 'long' },
rewriteBasePath: { type: 'boolean' },
keepaliveTimeout: { type: 'long' },
socketTimeout: { type: 'long' },
compression: {
enabled: { type: 'boolean' },
referrerWhitelistConfigured: { type: 'boolean' },
},
xsrf: {
disableProtection: { type: 'boolean' },
whitelistConfigured: { type: 'boolean' },
},
requestId: {
allowFromAnyIp: { type: 'boolean' },
ipAllowlistConfigured: { type: 'boolean' },
},
ssl: {
certificateAuthoritiesConfigured: { type: 'boolean' },
certificateConfigured: { type: 'boolean' },
cipherSuites: { type: 'array', items: { type: 'keyword' } },
keyConfigured: { type: 'boolean' },
keystoreConfigured: { type: 'boolean' },
truststoreConfigured: { type: 'boolean' },
redirectHttpFromPortConfigured: { type: 'boolean' },
supportedProtocols: { type: 'array', items: { type: 'keyword' } },
clientAuthentication: { type: 'keyword' },
},
},
logging: {
appendersTypesUsed: { type: 'array', items: { type: 'keyword' } },
loggersConfiguredCount: { type: 'long' },
},
savedObjects: {
maxImportPayloadBytes: { type: 'long' },
maxImportExportSizeBytes: { type: 'long' },
},
},
environment: {
memory: {
heapSizeLimit: { type: 'long' },
heapTotalBytes: { type: 'long' },
heapUsedBytes: { type: 'long' },
},
},
services: {
savedObjects: {
indices: {
type: 'array',
items: {
docsCount: { type: 'long' },
docsDeleted: { type: 'long' },
alias: { type: 'text' },
primaryStoreSizeBytes: { type: 'long' },
storeSizeBytes: { type: 'long' },
},
},
},
},
},
fetch() {
return getCoreUsageDataService().getCoreUsageData();
},
});
}
export function registerCoreUsageCollector(
usageCollection: UsageCollectionSetup,
getCoreUsageDataService: () => CoreUsageDataStart
) {
usageCollection.registerCollector(
getCoreUsageCollector(usageCollection, getCoreUsageDataService)
);
}

View file

@ -0,0 +1,53 @@
/*
* 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 {
CollectorOptions,
createUsageCollectionSetupMock,
} from '../../../../usage_collection/server/usage_collection.mock';
import { registerCoreUsageCollector } from '.';
import { coreUsageDataServiceMock } from '../../../../../core/server/mocks';
import { CoreUsageData } from 'src/core/server/';
describe('telemetry_core', () => {
let collector: CollectorOptions;
const usageCollectionMock = createUsageCollectionSetupMock();
usageCollectionMock.makeUsageCollector.mockImplementation((config) => {
collector = config;
return createUsageCollectionSetupMock().makeUsageCollector(config);
});
const callCluster = jest.fn().mockImplementation(() => ({}));
const coreUsageDataStart = coreUsageDataServiceMock.createStartContract();
const getCoreUsageDataReturnValue = (Symbol('core telemetry') as any) as CoreUsageData;
coreUsageDataStart.getCoreUsageData.mockResolvedValue(getCoreUsageDataReturnValue);
beforeAll(() => registerCoreUsageCollector(usageCollectionMock, () => coreUsageDataStart));
test('registered collector is set', () => {
expect(collector).not.toBeUndefined();
expect(collector.type).toBe('core');
});
test('fetch', async () => {
expect(await collector.fetch(callCluster)).toEqual(getCoreUsageDataReturnValue);
});
});

View file

@ -0,0 +1,20 @@
/*
* 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.
*/
export { registerCoreUsageCollector } from './core_usage_collector';

View file

@ -23,3 +23,4 @@ export { registerApplicationUsageCollector } from './application_usage';
export { registerKibanaUsageCollector } from './kibana';
export { registerOpsStatsCollector } from './ops_stats';
export { registerCspCollector } from './csp';
export { registerCoreUsageCollector } from './core';

View file

@ -31,6 +31,7 @@ import {
SavedObjectsServiceSetup,
OpsMetrics,
Logger,
CoreUsageDataStart,
} from '../../../core/server';
import {
registerApplicationUsageCollector,
@ -39,6 +40,7 @@ import {
registerOpsStatsCollector,
registerUiMetricUsageCollector,
registerCspCollector,
registerCoreUsageCollector,
} from './collectors';
interface KibanaUsageCollectionPluginsDepsSetup {
@ -53,6 +55,7 @@ export class KibanaUsageCollectionPlugin implements Plugin {
private savedObjectsClient?: ISavedObjectsRepository;
private uiSettingsClient?: IUiSettingsClient;
private metric$: Subject<OpsMetrics>;
private coreUsageData?: CoreUsageDataStart;
constructor(initializerContext: PluginInitializerContext) {
this.logger = initializerContext.logger.get();
@ -72,6 +75,7 @@ export class KibanaUsageCollectionPlugin implements Plugin {
const savedObjectsClient = new SavedObjectsClient(this.savedObjectsClient);
this.uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient);
core.metrics.getOpsMetrics$().subscribe(this.metric$);
this.coreUsageData = core.coreUsageData;
}
public stop() {
@ -86,6 +90,7 @@ export class KibanaUsageCollectionPlugin implements Plugin {
) {
const getSavedObjectsClient = () => this.savedObjectsClient;
const getUiSettingsClient = () => this.uiSettingsClient;
const getCoreUsageDataService = () => this.coreUsageData!;
registerOpsStatsCollector(usageCollection, metric$);
registerKibanaUsageCollector(usageCollection, this.legacyConfig$);
@ -98,5 +103,6 @@ export class KibanaUsageCollectionPlugin implements Plugin {
getSavedObjectsClient
);
registerCspCollector(usageCollection, coreSetup.http);
registerCoreUsageCollector(usageCollection, getCoreUsageDataService);
}
}