[Telemetry] Telemetry tools check all makeUsageCollector calls (#79840)

This commit is contained in:
Ahmad Bamieh 2020-10-08 14:42:23 +03:00 committed by GitHub
parent 4dc6f3b788
commit 70549c2ffa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 740 additions and 159 deletions

View file

@ -88,7 +88,7 @@ export function runTelemetryCheck() {
task: (context) => new Listr(checkCompatibleTypesTask(context), { exitOnError: true }),
},
{
enabled: (_) => !!ignoreStoredJson,
enabled: (_) => fix || !ignoreStoredJson,
title: 'Checking Matching collector.schema against stored json files',
task: (context) =>
new Listr(checkMatchingSchemasTask(context, !fix), { exitOnError: true }),
@ -96,7 +96,10 @@ export function runTelemetryCheck() {
{
enabled: (_) => fix,
skip: ({ roots }: TaskContext) => {
return roots.every(({ esMappingDiffs }) => !esMappingDiffs || !esMappingDiffs.length);
const noDiffs = roots.every(
({ esMappingDiffs }) => !esMappingDiffs || !esMappingDiffs.length
);
return noDiffs && 'No changes needed.';
},
title: 'Generating new telemetry mappings',
task: (context) => new Listr(generateSchemasTask(context), { exitOnError: true }),
@ -104,7 +107,10 @@ export function runTelemetryCheck() {
{
enabled: (_) => fix,
skip: ({ roots }: TaskContext) => {
return roots.every(({ esMappingDiffs }) => !esMappingDiffs || !esMappingDiffs.length);
const noDiffs = roots.every(
({ esMappingDiffs }) => !esMappingDiffs || !esMappingDiffs.length
);
return noDiffs && 'No changes needed.';
},
title: 'Updating telemetry mapping files',
task: (context) => new Listr(writeToFileTask(context), { exitOnError: true }),

View file

@ -1,5 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`parseUsageCollection throws when \`makeUsageCollector\` argument is a function call 1`] = `
"Error extracting collector in src/fixtures/telemetry_collectors/externally_defined_usage_collector/index.ts
Error: makeUsageCollector argument must be an object."
`;
exports[`parseUsageCollection throws when mapping fields is not defined 1`] = `
"Error extracting collector in src/fixtures/telemetry_collectors/unmapped_collector.ts
Error: usageCollector.schema must be defined."

View file

@ -33,7 +33,10 @@ describe('parseTelemetryRC', () => {
{
root: configRoot,
output: configRoot,
exclude: [path.resolve(configRoot, './unmapped_collector.ts')],
exclude: [
path.resolve(configRoot, './unmapped_collector.ts'),
path.resolve(configRoot, './externally_defined_usage_collector/index.ts'),
],
},
]);
});

View file

@ -33,7 +33,7 @@ export function loadFixtureProgram(fixtureName: string) {
'src',
'fixtures',
'telemetry_collectors',
`${fixtureName}.ts`
`${fixtureName}`
);
const tsConfig = ts.findConfigFile('./', ts.sys.fileExists, 'tsconfig.json');
if (!tsConfig) {
@ -52,49 +52,56 @@ describe('parseUsageCollection', () => {
it.todo('throws when a function is returned from fetch');
it.todo('throws when an object is not returned from fetch');
it('throws when `makeUsageCollector` argument is a function call', () => {
const { program, sourceFile } = loadFixtureProgram(
'externally_defined_usage_collector/index.ts'
);
expect(() => [...parseUsageCollection(sourceFile, program)]).toThrowErrorMatchingSnapshot();
});
it('throws when mapping fields is not defined', () => {
const { program, sourceFile } = loadFixtureProgram('unmapped_collector');
const { program, sourceFile } = loadFixtureProgram('unmapped_collector.ts');
expect(() => [...parseUsageCollection(sourceFile, program)]).toThrowErrorMatchingSnapshot();
});
it('parses root level defined collector', () => {
const { program, sourceFile } = loadFixtureProgram('working_collector');
const { program, sourceFile } = loadFixtureProgram('working_collector.ts');
const result = [...parseUsageCollection(sourceFile, program)];
expect(result).toEqual([parsedWorkingCollector]);
});
it('parses collector with schema defined as union of spreads', () => {
const { program, sourceFile } = loadFixtureProgram('schema_defined_with_spreads_collector');
const { program, sourceFile } = loadFixtureProgram('schema_defined_with_spreads_collector.ts');
const result = [...parseUsageCollection(sourceFile, program)];
expect(result).toEqual([parsedSchemaDefinedWithSpreadsCollector]);
});
it('parses nested collectors', () => {
const { program, sourceFile } = loadFixtureProgram('nested_collector');
const { program, sourceFile } = loadFixtureProgram('nested_collector.ts');
const result = [...parseUsageCollection(sourceFile, program)];
expect(result).toEqual([parsedNestedCollector]);
});
it('parses imported schema property', () => {
const { program, sourceFile } = loadFixtureProgram('imported_schema');
const { program, sourceFile } = loadFixtureProgram('imported_schema.ts');
const result = [...parseUsageCollection(sourceFile, program)];
expect(result).toEqual(parsedImportedSchemaCollector);
});
it('parses externally defined collectors', () => {
const { program, sourceFile } = loadFixtureProgram('externally_defined_collector');
const { program, sourceFile } = loadFixtureProgram('externally_defined_collector.ts');
const result = [...parseUsageCollection(sourceFile, program)];
expect(result).toEqual(parsedExternallyDefinedCollector);
});
it('parses imported Usage interface', () => {
const { program, sourceFile } = loadFixtureProgram('imported_usage_interface');
const { program, sourceFile } = loadFixtureProgram('imported_usage_interface.ts');
const result = [...parseUsageCollection(sourceFile, program)];
expect(result).toEqual(parsedImportedUsageInterface);
});
it('skips files that do not define a collector', () => {
const { program, sourceFile } = loadFixtureProgram('file_with_no_collector');
const { program, sourceFile } = loadFixtureProgram('file_with_no_collector.ts');
const result = [...parseUsageCollection(sourceFile, program)];
expect(result).toEqual([]);
});

View file

@ -178,13 +178,11 @@ export function sourceHasUsageCollector(sourceFile: ts.SourceFile) {
}
const identifiers = (sourceFile as any).identifiers;
if (
(!identifiers.get('makeUsageCollector') && !identifiers.get('type')) ||
!identifiers.get('fetch')
) {
return false;
if (identifiers.get('makeUsageCollector')) {
return true;
}
return false;
return true;
}

View file

@ -2,6 +2,7 @@
"root": ".",
"output": ".",
"exclude": [
"./unmapped_collector.ts"
"./unmapped_collector.ts",
"./externally_defined_usage_collector/index.ts"
]
}

View file

@ -0,0 +1,39 @@
/*
* 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 interface Usage {
collectorName: string;
}
export function getUsageCollector(collectorName: string) {
return {
type: 'externally_defined_usage_collector',
isReady: () => true,
schema: {
collectorName: {
type: 'keyword' as 'keyword',
},
},
fetch(): Usage {
return {
collectorName,
};
},
};
}

View file

@ -0,0 +1,28 @@
/*
* 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 { getUsageCollector } from './get_usage_collector';
export function registerCollector(collectorSet: UsageCollectionSetup) {
const collectorName = 'some_configs';
const collector = collectorSet.makeUsageCollector(getUsageCollector(collectorName));
collectorSet.registerCollector(collector);
}

View file

@ -1290,6 +1290,235 @@
}
}
},
"core": {
"properties": {
"config": {
"properties": {
"elasticsearch": {
"properties": {
"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": {
"properties": {
"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": {
"properties": {
"basePathConfigured": {
"type": "boolean"
},
"maxPayloadInBytes": {
"type": "long"
},
"rewriteBasePath": {
"type": "boolean"
},
"keepaliveTimeout": {
"type": "long"
},
"socketTimeout": {
"type": "long"
},
"compression": {
"properties": {
"enabled": {
"type": "boolean"
},
"referrerWhitelistConfigured": {
"type": "boolean"
}
}
},
"xsrf": {
"properties": {
"disableProtection": {
"type": "boolean"
},
"whitelistConfigured": {
"type": "boolean"
}
}
},
"requestId": {
"properties": {
"allowFromAnyIp": {
"type": "boolean"
},
"ipAllowlistConfigured": {
"type": "boolean"
}
}
},
"ssl": {
"properties": {
"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": {
"properties": {
"appendersTypesUsed": {
"type": "array",
"items": {
"type": "keyword"
}
},
"loggersConfiguredCount": {
"type": "long"
}
}
},
"savedObjects": {
"properties": {
"maxImportPayloadBytes": {
"type": "long"
},
"maxImportExportSizeBytes": {
"type": "long"
}
}
}
}
},
"environment": {
"properties": {
"memory": {
"properties": {
"heapSizeLimit": {
"type": "long"
},
"heapTotalBytes": {
"type": "long"
},
"heapUsedBytes": {
"type": "long"
}
}
}
}
},
"services": {
"properties": {
"savedObjects": {
"properties": {
"indices": {
"type": "array",
"items": {
"properties": {
"docsCount": {
"type": "long"
},
"docsDeleted": {
"type": "long"
},
"alias": {
"type": "text"
},
"primaryStoreSizeBytes": {
"type": "long"
},
"storeSizeBytes": {
"type": "long"
}
}
}
}
}
}
}
}
}
},
"csp": {
"properties": {
"strict": {
@ -2502,6 +2731,48 @@
"type": "long"
}
}
},
"vis_type_vega": {
"properties": {
"vega_lib_specs_total": {
"type": "long"
},
"vega_lite_lib_specs_total": {
"type": "long"
},
"vega_use_map_total": {
"type": "long"
}
}
},
"visualization_types": {
"properties": {
"DYNAMIC_KEY": {
"properties": {
"total": {
"type": "long"
},
"spaces_min": {
"type": "long"
},
"spaces_max": {
"type": "long"
},
"spaces_avg": {
"type": "long"
},
"saved_7_days_total": {
"type": "long"
},
"saved_30_days_total": {
"type": "long"
},
"saved_90_days_total": {
"type": "long"
}
}
}
}
}
}
}

View file

@ -0,0 +1,24 @@
/*
* 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 const mockStats = { somestat: 1 };
export const mockGetStats = jest.fn().mockResolvedValue(mockStats);
jest.doMock('./get_usage_collector', () => ({
getStats: mockGetStats,
}));

View file

@ -17,10 +17,8 @@
* under the License.
*/
import { of } from 'rxjs';
import { LegacyAPICaller } from 'src/core/server';
import { getUsageCollector } from './get_usage_collector';
import { getStats } from './get_usage_collector';
import { HomeServerPluginSetup } from '../../../home/server';
const mockedSavedObjects = [
@ -76,8 +74,8 @@ const getMockCallCluster = (hits?: unknown[]) =>
jest.fn().mockReturnValue(Promise.resolve({ hits: { hits } }) as unknown) as LegacyAPICaller;
describe('Vega visualization usage collector', () => {
const configMock = of({ kibana: { index: '' } });
const usageCollector = getUsageCollector(configMock, {
const mockIndex = 'mock_index';
const mockDeps = {
home: ({
sampleData: {
getSampleDatasets: jest.fn().mockReturnValue([
@ -100,44 +98,37 @@ describe('Vega visualization usage collector', () => {
]),
},
} as unknown) as HomeServerPluginSetup,
});
test('Should fit the shape', () => {
expect(usageCollector.type).toBe('vis_type_vega');
expect(usageCollector.isReady()).toBe(true);
expect(usageCollector.fetch).toEqual(expect.any(Function));
});
};
test('Returns undefined when no results found (undefined)', async () => {
const result = await usageCollector.fetch(getMockCallCluster());
const result = await getStats(getMockCallCluster(), mockIndex, mockDeps);
expect(result).toBeUndefined();
});
test('Returns undefined when no results found (0 results)', async () => {
const result = await usageCollector.fetch(getMockCallCluster([]));
const result = await getStats(getMockCallCluster([]), mockIndex, mockDeps);
expect(result).toBeUndefined();
});
test('Returns undefined when no vega saved objects found', async () => {
const result = await usageCollector.fetch(
getMockCallCluster([
{
_id: 'visualization:myvis-123',
_source: {
type: 'visualization',
visualization: { visState: '{"type": "area"}' },
},
const mockCallCluster = getMockCallCluster([
{
_id: 'visualization:myvis-123',
_source: {
type: 'visualization',
visualization: { visState: '{"type": "area"}' },
},
])
);
},
]);
const result = await getStats(mockCallCluster, mockIndex, mockDeps);
expect(result).toBeUndefined();
});
test('Should ingnore sample data visualizations', async () => {
const callCluster = getMockCallCluster([
const mockCallCluster = getMockCallCluster([
{
_id: 'visualization:sampledata-123',
_source: {
@ -155,13 +146,14 @@ describe('Vega visualization usage collector', () => {
},
]);
const result = await usageCollector.fetch(callCluster);
const result = await getStats(mockCallCluster, mockIndex, mockDeps);
expect(result).toBeUndefined();
});
test('Summarizes visualizations response data', async () => {
const result = await usageCollector.fetch(getMockCallCluster(mockedSavedObjects));
const mockCallCluster = getMockCallCluster(mockedSavedObjects);
const result = await getStats(mockCallCluster, mockIndex, mockDeps);
expect(result).toMatchObject({
vega_lib_specs_total: 2,

View file

@ -17,23 +17,16 @@
* under the License.
*/
import { parse } from 'hjson';
import { first } from 'rxjs/operators';
import { SearchResponse } from 'elasticsearch';
import { LegacyAPICaller, SavedObject } from 'src/core/server';
import {
ConfigObservable,
VegaSavedObjectAttributes,
VisTypeVegaPluginSetupDependencies,
} from '../types';
import { VegaSavedObjectAttributes, VisTypeVegaPluginSetupDependencies } from '../types';
type UsageCollectorDependencies = Pick<VisTypeVegaPluginSetupDependencies, 'home'>;
type ESResponse = SearchResponse<{ visualization: { visState: string } }>;
type VegaType = 'vega' | 'vega-lite';
const VEGA_USAGE_TYPE = 'vis_type_vega';
function isVegaType(attributes: any): attributes is VegaSavedObjectAttributes {
return attributes && attributes.type === 'vega' && attributes.params?.spec;
}
@ -64,11 +57,25 @@ const getDefaultVegaVisualizations = (home: UsageCollectorDependencies['home'])
return titles;
};
const getStats = async (
export interface VegaUsage {
vega_lib_specs_total: number;
vega_lite_lib_specs_total: number;
vega_use_map_total: number;
}
export const getStats = async (
callCluster: LegacyAPICaller,
index: string,
{ home }: UsageCollectorDependencies
) => {
): Promise<VegaUsage | undefined> => {
let shouldPublishTelemetry = false;
const vegaUsage = {
vega_lib_specs_total: 0,
vega_lite_lib_specs_total: 0,
vega_use_map_total: 0,
};
const searchParams = {
size: 10000,
index,
@ -82,9 +89,9 @@ const getStats = async (
},
},
};
const esResponse: ESResponse = await callCluster('search', searchParams);
const size = esResponse?.hits?.hits?.length ?? 0;
let shouldPublishTelemetry = false;
if (!size) {
return;
@ -93,55 +100,32 @@ const getStats = async (
// we want to exclude the Vega Sample Data visualizations from the stats
// in order to have more accurate results
const excludedFromStatsVisualizations = getDefaultVegaVisualizations(home);
for (const hit of esResponse.hits.hits) {
const visualization = hit._source?.visualization;
const visState = JSON.parse(visualization?.visState ?? '{}');
const finalTelemetry = esResponse.hits.hits.reduce(
(telemetry, hit) => {
const visualization = hit._source?.visualization;
const visState = JSON.parse(visualization?.visState ?? '{}');
if (isVegaType(visState) && !excludedFromStatsVisualizations.includes(visState.title)) {
try {
const spec = parse(visState.params.spec, { legacyRoot: false });
if (isVegaType(visState) && !excludedFromStatsVisualizations.includes(visState.title))
try {
const spec = parse(visState.params.spec, { legacyRoot: false });
if (spec) {
shouldPublishTelemetry = true;
if (spec) {
shouldPublishTelemetry = true;
if (checkVegaSchemaType(spec.$schema, 'vega')) {
telemetry.vega_lib_specs_total++;
}
if (checkVegaSchemaType(spec.$schema, 'vega-lite')) {
telemetry.vega_lite_lib_specs_total++;
}
if (spec.config?.kibana?.type === 'map') {
telemetry.vega_use_map_total++;
}
if (checkVegaSchemaType(spec.$schema, 'vega')) {
vegaUsage.vega_lib_specs_total++;
}
if (checkVegaSchemaType(spec.$schema, 'vega-lite')) {
vegaUsage.vega_lite_lib_specs_total++;
}
if (spec.config?.kibana?.type === 'map') {
vegaUsage.vega_use_map_total++;
}
} catch (e) {
// Let it go, the data is invalid and we'll don't need to handle it
}
return telemetry;
},
{
vega_lib_specs_total: 0,
vega_lite_lib_specs_total: 0,
vega_use_map_total: 0,
} catch (e) {
// Let it go, the data is invalid and we'll don't need to handle it
}
}
);
}
return shouldPublishTelemetry ? finalTelemetry : undefined;
return shouldPublishTelemetry ? vegaUsage : undefined;
};
export function getUsageCollector(
config: ConfigObservable,
dependencies: UsageCollectorDependencies
) {
return {
type: VEGA_USAGE_TYPE,
isReady: () => true,
fetch: async (callCluster: LegacyAPICaller) => {
const { index } = (await config.pipe(first()).toPromise()).kibana;
return await getStats(callCluster, index, dependencies);
},
};
}

View file

@ -17,16 +17,4 @@
* under the License.
*/
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { getUsageCollector } from './get_usage_collector';
import { ConfigObservable, VisTypeVegaPluginSetupDependencies } from '../types';
export function registerVegaUsageCollector(
collectorSet: UsageCollectionSetup,
config: ConfigObservable,
dependencies: Pick<VisTypeVegaPluginSetupDependencies, 'home'>
) {
const collector = collectorSet.makeUsageCollector(getUsageCollector(config, dependencies));
collectorSet.registerCollector(collector);
}
export { registerVegaUsageCollector } from './register_vega_collector';

View file

@ -0,0 +1,68 @@
/*
* 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 { of } from 'rxjs';
import { mockStats, mockGetStats } from './get_usage_collector.mock';
import { createUsageCollectionSetupMock } from 'src/plugins/usage_collection/server/usage_collection.mock';
import { HomeServerPluginSetup } from '../../../home/server';
import { registerVegaUsageCollector } from './register_vega_collector';
describe('registerVegaUsageCollector', () => {
const mockIndex = 'mock_index';
const mockDeps = { home: ({} as unknown) as HomeServerPluginSetup };
const mockConfig = of({ kibana: { index: mockIndex } });
it('makes a usage collector and registers it`', () => {
const mockCollectorSet = createUsageCollectionSetupMock();
registerVegaUsageCollector(mockCollectorSet, mockConfig, mockDeps);
expect(mockCollectorSet.makeUsageCollector).toBeCalledTimes(1);
expect(mockCollectorSet.registerCollector).toBeCalledTimes(1);
});
it('makeUsageCollector configs fit the shape', () => {
const mockCollectorSet = createUsageCollectionSetupMock();
registerVegaUsageCollector(mockCollectorSet, mockConfig, mockDeps);
expect(mockCollectorSet.makeUsageCollector).toHaveBeenCalledWith({
type: 'vis_type_vega',
isReady: expect.any(Function),
fetch: expect.any(Function),
schema: expect.any(Object),
});
const usageCollectorConfig = mockCollectorSet.makeUsageCollector.mock.calls[0][0];
expect(usageCollectorConfig.isReady()).toBe(true);
});
it('makeUsageCollector config.isReady returns true', () => {
const mockCollectorSet = createUsageCollectionSetupMock();
registerVegaUsageCollector(mockCollectorSet, mockConfig, mockDeps);
const usageCollectorConfig = mockCollectorSet.makeUsageCollector.mock.calls[0][0];
expect(usageCollectorConfig.isReady()).toBe(true);
});
it('makeUsageCollector config.fetch calls getStats', async () => {
const mockCollectorSet = createUsageCollectionSetupMock();
registerVegaUsageCollector(mockCollectorSet, mockConfig, mockDeps);
const usageCollectorConfig = mockCollectorSet.makeUsageCollector.mock.calls[0][0];
const mockCallCluster = jest.fn();
const fetchResult = await usageCollectorConfig.fetch(mockCallCluster);
expect(mockGetStats).toBeCalledTimes(1);
expect(mockGetStats).toBeCalledWith(mockCallCluster, mockIndex, mockDeps);
expect(fetchResult).toBe(mockStats);
});
});

View file

@ -0,0 +1,46 @@
/*
* 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 { first } from 'rxjs/operators';
import { getStats, VegaUsage } from './get_usage_collector';
import { ConfigObservable, VisTypeVegaPluginSetupDependencies } from '../types';
export function registerVegaUsageCollector(
collectorSet: UsageCollectionSetup,
config: ConfigObservable,
dependencies: Pick<VisTypeVegaPluginSetupDependencies, 'home'>
) {
const collector = collectorSet.makeUsageCollector<VegaUsage | undefined>({
type: 'vis_type_vega',
isReady: () => true,
schema: {
vega_lib_specs_total: { type: 'long' },
vega_lite_lib_specs_total: { type: 'long' },
vega_use_map_total: { type: 'long' },
},
fetch: async (callCluster) => {
const { index } = (await config.pipe(first()).toPromise()).kibana;
return await getStats(callCluster, index, dependencies);
},
});
collectorSet.registerCollector(collector);
}

View file

@ -0,0 +1,24 @@
/*
* 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 const mockStats = { somestat: 1 };
export const mockGetStats = jest.fn().mockResolvedValue(mockStats);
jest.doMock('./get_usage_collector', () => ({
getStats: mockGetStats,
}));

View file

@ -18,10 +18,8 @@
*/
import moment from 'moment';
import { of } from 'rxjs';
import { LegacyAPICaller } from 'src/core/server';
import { getUsageCollector } from './get_usage_collector';
import { getStats } from './get_usage_collector';
const defaultMockSavedObjects = [
{
@ -121,29 +119,22 @@ const enlargedMockSavedObjects = [
];
describe('Visualizations usage collector', () => {
const configMock = of({ kibana: { index: '' } });
const usageCollector = getUsageCollector(configMock);
const mockIndex = '';
const getMockCallCluster = (hits: unknown[]) =>
(() => Promise.resolve({ hits: { hits } }) as unknown) as LegacyAPICaller;
test('Should fit the shape', () => {
expect(usageCollector.type).toBe('visualization_types');
expect(usageCollector.isReady()).toBe(true);
expect(usageCollector.fetch).toEqual(expect.any(Function));
});
test('Returns undefined when no results found (undefined)', async () => {
const result = await usageCollector.fetch(getMockCallCluster(undefined as any));
expect(result).toBe(undefined);
const result = await getStats(getMockCallCluster(undefined as any), mockIndex);
expect(result).toBeUndefined();
});
test('Returns undefined when no results found (0 results)', async () => {
const result = await usageCollector.fetch(getMockCallCluster([]));
expect(result).toBe(undefined);
const result = await getStats(getMockCallCluster([]), mockIndex);
expect(result).toBeUndefined();
});
test('Summarizes visualizations response data', async () => {
const result = await usageCollector.fetch(getMockCallCluster(defaultMockSavedObjects));
const result = await getStats(getMockCallCluster(defaultMockSavedObjects), mockIndex);
expect(result).toMatchObject({
shell_beads: {
@ -198,7 +189,7 @@ describe('Visualizations usage collector', () => {
},
};
const result = await usageCollector.fetch(getMockCallCluster(enlargedMockSavedObjects));
const result = await getStats(getMockCallCluster(enlargedMockSavedObjects), mockIndex);
expect(result).toMatchObject(expectedStats);
});

View file

@ -17,16 +17,12 @@
* under the License.
*/
import { Observable } from 'rxjs';
import { countBy, get, groupBy, mapValues, max, min, values } from 'lodash';
import { first } from 'rxjs/operators';
import { SearchResponse } from 'elasticsearch';
import { LegacyAPICaller } from 'src/core/server';
import { getPastDays } from './get_past_days';
const VIS_USAGE_TYPE = 'visualization_types';
type ESResponse = SearchResponse<{ visualization: { visState: string } }>;
interface VisSummary {
@ -35,10 +31,25 @@ interface VisSummary {
past_days: number;
}
export interface VisualizationUsage {
[x: string]: {
total: number;
spaces_min?: number;
spaces_max?: number;
spaces_avg: number;
saved_7_days_total: number;
saved_30_days_total: number;
saved_90_days_total: number;
};
}
/*
* Parse the response data into telemetry payload
*/
async function getStats(callCluster: LegacyAPICaller, index: string) {
export async function getStats(
callCluster: LegacyAPICaller,
index: string
): Promise<VisualizationUsage | undefined> {
const searchParams = {
size: 10000, // elasticsearch index.max_result_window default value
index,
@ -94,14 +105,3 @@ async function getStats(callCluster: LegacyAPICaller, index: string) {
};
});
}
export function getUsageCollector(config: Observable<{ kibana: { index: string } }>) {
return {
type: VIS_USAGE_TYPE,
isReady: () => true,
fetch: async (callCluster: LegacyAPICaller) => {
const index = (await config.pipe(first()).toPromise()).kibana.index;
return await getStats(callCluster, index);
},
};
}

View file

@ -17,15 +17,4 @@
* under the License.
*/
import { Observable } from 'rxjs';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { getUsageCollector } from './get_usage_collector';
export function registerVisualizationsCollector(
collectorSet: UsageCollectionSetup,
config: Observable<{ kibana: { index: string } }>
) {
const collector = collectorSet.makeUsageCollector(getUsageCollector(config));
collectorSet.registerCollector(collector);
}
export { registerVisualizationsCollector } from './register_visualizations_collector';

View file

@ -0,0 +1,67 @@
/*
* 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 { of } from 'rxjs';
import { mockStats, mockGetStats } from './get_usage_collector.mock';
import { createUsageCollectionSetupMock } from 'src/plugins/usage_collection/server/usage_collection.mock';
import { registerVisualizationsCollector } from './register_visualizations_collector';
describe('registerVisualizationsCollector', () => {
const mockIndex = 'mock_index';
const mockConfig = of({ kibana: { index: mockIndex } });
it('makes a usage collector and registers it`', () => {
const mockCollectorSet = createUsageCollectionSetupMock();
registerVisualizationsCollector(mockCollectorSet, mockConfig);
expect(mockCollectorSet.makeUsageCollector).toBeCalledTimes(1);
expect(mockCollectorSet.registerCollector).toBeCalledTimes(1);
});
it('makeUsageCollector configs fit the shape', () => {
const mockCollectorSet = createUsageCollectionSetupMock();
registerVisualizationsCollector(mockCollectorSet, mockConfig);
expect(mockCollectorSet.makeUsageCollector).toHaveBeenCalledWith({
type: 'visualization_types',
isReady: expect.any(Function),
fetch: expect.any(Function),
schema: expect.any(Object),
});
const usageCollectorConfig = mockCollectorSet.makeUsageCollector.mock.calls[0][0];
expect(usageCollectorConfig.isReady()).toBe(true);
});
it('makeUsageCollector config.isReady returns true', () => {
const mockCollectorSet = createUsageCollectionSetupMock();
registerVisualizationsCollector(mockCollectorSet, mockConfig);
const usageCollectorConfig = mockCollectorSet.makeUsageCollector.mock.calls[0][0];
expect(usageCollectorConfig.isReady()).toBe(true);
});
it('makeUsageCollector config.fetch calls getStats', async () => {
const mockCollectorSet = createUsageCollectionSetupMock();
registerVisualizationsCollector(mockCollectorSet, mockConfig);
const usageCollectorConfig = mockCollectorSet.makeUsageCollector.mock.calls[0][0];
const mockCallCluster = jest.fn();
const fetchResult = await usageCollectorConfig.fetch(mockCallCluster);
expect(mockGetStats).toBeCalledTimes(1);
expect(mockGetStats).toBeCalledWith(mockCallCluster, mockIndex);
expect(fetchResult).toBe(mockStats);
});
});

View file

@ -0,0 +1,50 @@
/*
* 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 { Observable } from 'rxjs';
import { first } from 'rxjs/operators';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { getStats, VisualizationUsage } from './get_usage_collector';
export function registerVisualizationsCollector(
collectorSet: UsageCollectionSetup,
config: Observable<{ kibana: { index: string } }>
) {
const collector = collectorSet.makeUsageCollector<VisualizationUsage | undefined>({
type: 'visualization_types',
isReady: () => true,
schema: {
DYNAMIC_KEY: {
total: { type: 'long' },
spaces_min: { type: 'long' },
spaces_max: { type: 'long' },
spaces_avg: { type: 'long' },
saved_7_days_total: { type: 'long' },
saved_30_days_total: { type: 'long' },
saved_90_days_total: { type: 'long' },
},
},
fetch: async (callCluster) => {
const index = (await config.pipe(first()).toPromise()).kibana.index;
return await getStats(callCluster, index);
},
});
collectorSet.registerCollector(collector);
}