[7.x] [Telemetry] Collector Schema (#64942) (#70141)

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Ahmad Bamieh 2020-06-28 20:34:33 +03:00 committed by GitHub
parent 40cc1ba306
commit f1fcd57ed5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
95 changed files with 4004 additions and 138 deletions

244
.github/CODEOWNERS vendored Normal file
View file

@ -0,0 +1,244 @@
# GitHub CODEOWNERS definition
# Identify which groups will be pinged by changes to different parts of the codebase.
# For more info, see https://help.github.com/articles/about-codeowners/
# App
/x-pack/plugins/dashboard_enhanced/ @elastic/kibana-app
/x-pack/plugins/discover_enhanced/ @elastic/kibana-app
/x-pack/plugins/lens/ @elastic/kibana-app
/x-pack/plugins/graph/ @elastic/kibana-app
/src/legacy/core_plugins/kibana/public/local_application_service/ @elastic/kibana-app
/src/plugins/dashboard/ @elastic/kibana-app
/src/plugins/discover/ @elastic/kibana-app
/src/plugins/input_control_vis/ @elastic/kibana-app
/src/plugins/kibana_legacy/ @elastic/kibana-app
/src/plugins/vis_default_editor/ @elastic/kibana-app
/src/plugins/vis_type_markdown/ @elastic/kibana-app
/src/plugins/vis_type_metric/ @elastic/kibana-app
/src/plugins/vis_type_table/ @elastic/kibana-app
/src/plugins/vis_type_tagcloud/ @elastic/kibana-app
/src/plugins/vis_type_timelion/ @elastic/kibana-app
/src/plugins/vis_type_timeseries/ @elastic/kibana-app
/src/plugins/vis_type_vega/ @elastic/kibana-app
/src/plugins/vis_type_vislib/ @elastic/kibana-app
/src/plugins/vis_type_xy/ @elastic/kibana-app
/src/plugins/visualize/ @elastic/kibana-app
# Core UI
# Exclude tutorials folder for now because they are not owned by Kibana app and most will move out soon
/src/plugins/home/public @elastic/kibana-core-ui
/src/plugins/home/server/*.ts @elastic/kibana-core-ui
/src/plugins/home/server/services/ @elastic/kibana-core-ui
# Exclude tutorial resources folder for now because they are not owned by Kibana app and most will move out soon
/src/legacy/core_plugins/kibana/public/home/*.ts @elastic/kibana-core-ui
/src/legacy/core_plugins/kibana/public/home/*.scss @elastic/kibana-core-ui
/src/legacy/core_plugins/kibana/public/home/np_ready/ @elastic/kibana-core-ui
# App Architecture
/examples/developer_examples/ @elastic/kibana-app-arch
/examples/url_generators_examples/ @elastic/kibana-app-arch
/examples/url_generators_explorer/ @elastic/kibana-app-arch
/packages/kbn-interpreter/ @elastic/kibana-app-arch
/packages/elastic-datemath/ @elastic/kibana-app-arch
/src/legacy/core_plugins/embeddable_api/ @elastic/kibana-app-arch
/src/legacy/core_plugins/interpreter/ @elastic/kibana-app-arch
/src/legacy/core_plugins/kibana_react/ @elastic/kibana-app-arch
/src/legacy/core_plugins/kibana/public/management/ @elastic/kibana-app-arch
/src/legacy/core_plugins/kibana/server/routes/api/management/ @elastic/kibana-app-arch
/src/legacy/core_plugins/visualizations/ @elastic/kibana-app-arch
/src/legacy/server/index_patterns/ @elastic/kibana-app-arch
/src/plugins/advanced_settings/ @elastic/kibana-app-arch
/src/plugins/bfetch/ @elastic/kibana-app-arch
/src/plugins/data/ @elastic/kibana-app-arch
/src/plugins/embeddable/ @elastic/kibana-app-arch
/src/plugins/expressions/ @elastic/kibana-app-arch
/src/plugins/inspector/ @elastic/kibana-app-arch
/src/plugins/kibana_react/ @elastic/kibana-app-arch
/src/plugins/kibana_react/public/code_editor @elastic/kibana-canvas
/src/plugins/kibana_utils/ @elastic/kibana-app-arch
/src/plugins/management/ @elastic/kibana-app-arch
/src/plugins/navigation/ @elastic/kibana-app-arch
/src/plugins/share/ @elastic/kibana-app-arch
/src/plugins/ui_actions/ @elastic/kibana-app-arch
/src/plugins/visualizations/ @elastic/kibana-app-arch
/x-pack/plugins/advanced_ui_actions/ @elastic/kibana-app-arch
/x-pack/plugins/data_enhanced/ @elastic/kibana-app-arch
/x-pack/plugins/drilldowns/ @elastic/kibana-app-arch
# APM
/x-pack/plugins/apm/ @elastic/apm-ui
/x-pack/test/functional/apps/apm/ @elastic/apm-ui
/src/legacy/core_plugins/apm_oss/ @elastic/apm-ui
/src/plugins/apm_oss/ @elastic/apm-ui
/src/apm.js @watson @vigneshshanmugam
# Beats
/x-pack/legacy/plugins/beats_management/ @elastic/beats
# Canvas
/x-pack/plugins/canvas/ @elastic/kibana-canvas
/x-pack/test/functional/apps/canvas/ @elastic/kibana-canvas
# Observability UIs
/x-pack/legacy/plugins/infra/ @elastic/logs-metrics-ui
/x-pack/plugins/infra/ @elastic/logs-metrics-ui
/x-pack/plugins/ingest_manager/ @elastic/ingest-management
/x-pack/legacy/plugins/ingest_manager/ @elastic/ingest-management
/x-pack/plugins/observability/ @elastic/logs-metrics-ui @elastic/apm-ui @elastic/uptime @elastic/ingest-management
/x-pack/legacy/plugins/monitoring/ @elastic/stack-monitoring-ui
/x-pack/plugins/monitoring/ @elastic/stack-monitoring-ui
/x-pack/plugins/uptime @elastic/uptime
# Machine Learning
/x-pack/legacy/plugins/ml/ @elastic/ml-ui
/x-pack/plugins/ml/ @elastic/ml-ui
/x-pack/test/functional/apps/machine_learning/ @elastic/ml-ui
/x-pack/test/functional/services/machine_learning/ @elastic/ml-ui
/x-pack/test/functional/services/ml.ts @elastic/ml-ui
# ML team owns and maintains the transform plugin despite it living in the Elasticsearch management section.
/x-pack/plugins/transform/ @elastic/ml-ui
/x-pack/test/functional/apps/transform/ @elastic/ml-ui
/x-pack/test/functional/services/transform_ui/ @elastic/ml-ui
/x-pack/test/functional/services/transform.ts @elastic/ml-ui
# Maps
/x-pack/legacy/plugins/maps/ @elastic/kibana-gis
/x-pack/plugins/maps/ @elastic/kibana-gis
/x-pack/test/api_integration/apis/maps/ @elastic/kibana-gis
/x-pack/test/functional/apps/maps/ @elastic/kibana-gis
/x-pack/test/functional/es_archives/maps/ @elastic/kibana-gis
/x-pack/test/visual_regression/tests/maps/index.js @elastic/kibana-gis
# Operations
/src/dev/ @elastic/kibana-operations
/src/setup_node_env/ @elastic/kibana-operations
/src/optimize/ @elastic/kibana-operations
/src/es_archiver/ @elastic/kibana-operations
/packages/*eslint*/ @elastic/kibana-operations
/packages/*babel*/ @elastic/kibana-operations
/packages/kbn-dev-utils*/ @elastic/kibana-operations
/packages/kbn-es/ @elastic/kibana-operations
/packages/kbn-optimizer/ @elastic/kibana-operations
/packages/kbn-pm/ @elastic/kibana-operations
/packages/kbn-test/ @elastic/kibana-operations
/packages/kbn-ui-shared-deps/ @elastic/kibana-operations
/src/legacy/server/keystore/ @elastic/kibana-operations
/src/legacy/server/pid/ @elastic/kibana-operations
/src/legacy/server/sass/ @elastic/kibana-operations
/src/legacy/server/utils/ @elastic/kibana-operations
/src/legacy/server/warnings/ @elastic/kibana-operations
/.ci/es-snapshots/ @elastic/kibana-operations
/vars/ @elastic/kibana-operations
# Quality Assurance
/src/dev/code_coverage @elastic/kibana-qa
/test/functional/services/common @elastic/kibana-qa
/test/functional/services/lib @elastic/kibana-qa
/test/functional/services/remote @elastic/kibana-qa
# Platform
/src/core/ @elastic/kibana-platform
/config/kibana.yml @elastic/kibana-platform
/x-pack/plugins/features/ @elastic/kibana-platform
/x-pack/plugins/licensing/ @elastic/kibana-platform
/x-pack/plugins/global_search/ @elastic/kibana-platform
/x-pack/plugins/cloud/ @elastic/kibana-platform
/packages/kbn-config-schema/ @elastic/kibana-platform
/src/legacy/server/config/ @elastic/kibana-platform
/src/legacy/server/http/ @elastic/kibana-platform
/src/legacy/server/logging/ @elastic/kibana-platform
/src/legacy/server/saved_objects/ @elastic/kibana-platform
/src/legacy/server/status/ @elastic/kibana-platform
/src/plugins/status_page/ @elastic/kibana-platform
/src/plugins/saved_objects_management/ @elastic/kibana-platform
/src/dev/run_check_published_api_changes.ts @elastic/kibana-platform
# Security
/src/core/server/csp/ @elastic/kibana-security @elastic/kibana-platform
/x-pack/legacy/plugins/security/ @elastic/kibana-security
/x-pack/legacy/plugins/spaces/ @elastic/kibana-security
/x-pack/plugins/spaces/ @elastic/kibana-security
/x-pack/plugins/encrypted_saved_objects/ @elastic/kibana-security
/x-pack/plugins/security/ @elastic/kibana-security
/x-pack/test/api_integration/apis/security/ @elastic/kibana-security
# Kibana Localization
/src/dev/i18n/ @elastic/kibana-localization
/src/legacy/server/i18n/ @elastic/kibana-localization
/src/core/public/i18n/ @elastic/kibana-localization
/packages/kbn-i18n/ @elastic/kibana-localization
# Kibana Telemetry
/packages/kbn-analytics/ @elastic/kibana-telemetry
/packages/kbn-telemetry-tools/ @elastic/kibana-telemetry
/src/plugins/kibana_usage_collection/ @elastic/kibana-telemetry
/src/plugins/newsfeed/ @elastic/kibana-telemetry
/src/plugins/telemetry/ @elastic/kibana-telemetry
/src/plugins/telemetry_collection_manager/ @elastic/kibana-telemetry
/src/plugins/telemetry_management_section/ @elastic/kibana-telemetry
/src/plugins/usage_collection/ @elastic/kibana-telemetry
/x-pack/plugins/telemetry_collection_xpack/ @elastic/kibana-telemetry
/.telemetryrc.json @elastic/kibana-telemetry
/x-pack/.telemetryrc.json @elastic/kibana-telemetry
src/plugins/telemetry/schema/legacy_oss_plugins.json @elastic/kibana-telemetry
src/plugins/telemetry/schema/oss_plugins.json @elastic/kibana-telemetry
x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kibana-telemetry
# Kibana Alerting Services
/x-pack/plugins/alerts/ @elastic/kibana-alerting-services
/x-pack/plugins/actions/ @elastic/kibana-alerting-services
/x-pack/plugins/event_log/ @elastic/kibana-alerting-services
/x-pack/plugins/task_manager/ @elastic/kibana-alerting-services
/x-pack/test/alerting_api_integration/ @elastic/kibana-alerting-services
/x-pack/test/plugin_api_integration/plugins/task_manager/ @elastic/kibana-alerting-services
/x-pack/test/plugin_api_integration/test_suites/task_manager/ @elastic/kibana-alerting-services
/x-pack/plugins/triggers_actions_ui/ @elastic/kibana-alerting-services
/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/ @elastic/kibana-alerting-services
/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/ @elastic/kibana-alerting-services
# Design
**/*.scss @elastic/kibana-design
# Elasticsearch UI
/src/plugins/dev_tools/ @elastic/es-ui
/src/plugins/console/ @elastic/es-ui
/src/plugins/es_ui_shared/ @elastic/es-ui
/x-pack/legacy/plugins/cross_cluster_replication/ @elastic/es-ui
/x-pack/plugins/index_lifecycle_management/ @elastic/es-ui
/x-pack/legacy/plugins/index_management/ @elastic/es-ui
/x-pack/legacy/plugins/license_management/ @elastic/es-ui
/x-pack/legacy/plugins/rollup/ @elastic/es-ui
/x-pack/legacy/plugins/snapshot_restore/ @elastic/es-ui
/x-pack/legacy/plugins/upgrade_assistant/ @elastic/es-ui
/x-pack/plugins/console_extensions/ @elastic/es-ui
/x-pack/plugins/es_ui_shared/ @elastic/es-ui
/x-pack/plugins/grokdebugger/ @elastic/es-ui
/x-pack/plugins/index_management/ @elastic/es-ui
/x-pack/plugins/license_management/ @elastic/es-ui
/x-pack/plugins/painless_lab/ @elastic/es-ui
/x-pack/plugins/remote_clusters/ @elastic/es-ui
/x-pack/plugins/rollup/ @elastic/es-ui
/x-pack/plugins/searchprofiler/ @elastic/es-ui
/x-pack/plugins/snapshot_restore/ @elastic/es-ui
/x-pack/plugins/upgrade_assistant/ @elastic/es-ui
/x-pack/plugins/watcher/ @elastic/es-ui
/x-pack/plugins/ingest_pipelines/ @elastic/es-ui
# Endpoint
/x-pack/plugins/endpoint/ @elastic/endpoint-app-team @elastic/siem
/x-pack/test/api_integration/apis/endpoint/ @elastic/endpoint-app-team @elastic/siem
/x-pack/test/endpoint_api_integration_no_ingest/ @elastic/endpoint-app-team @elastic/siem
/x-pack/test/security_solution_endpoint/ @elastic/endpoint-app-team @elastic/siem
/x-pack/test/functional/es_archives/endpoint/ @elastic/endpoint-app-team @elastic/siem
/x-pack/test/plugin_functional/plugins/resolver_test/ @elastic/endpoint-app-team @elastic/siem
/x-pack/test/plugin_functional/test_suites/resolver/ @elastic/endpoint-app-team @elastic/siem
# Security Solution
/x-pack/plugins/security_solution/ @elastic/siem @elastic/endpoint-app-team
/x-pack/test/detection_engine_api_integration @elastic/siem @elastic/endpoint-app-team
/x-pack/test/api_integration/apis/security_solution @elastic/siem @elastic/endpoint-app-team
/x-pack/plugins/case @elastic/siem @elastic/endpoint-app-team
/x-pack/plugins/lists @elastic/siem @elastic/endpoint-app-team
# Security Intelligence And Analytics
/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules @elastic/security-intelligence-analytics

25
.telemetryrc.json Normal file
View file

@ -0,0 +1,25 @@
[
{
"output": "src/plugins/telemetry/schema/legacy_oss_plugins.json",
"root": "src/legacy/core_plugins/",
"exclude": [
"src/legacy/core_plugins/testbed",
"src/legacy/core_plugins/elasticsearch",
"src/legacy/core_plugins/tests_bundle"
]
},
{
"output": "src/plugins/telemetry/schema/oss_plugins.json",
"root": "src/plugins/",
"exclude": [
"src/plugins/kibana_react/",
"src/plugins/testbed/",
"src/plugins/kibana_utils/",
"src/plugins/kibana_usage_collection/server/collectors/kibana/kibana_usage_collector.ts",
"src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.ts",
"src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.ts",
"src/plugins/kibana_usage_collection/server/collectors/ui_metric/telemetry_ui_metric_collector.ts",
"src/plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts"
]
}
]

View file

@ -136,6 +136,7 @@
"@kbn/babel-preset": "1.0.0",
"@kbn/config-schema": "1.0.0",
"@kbn/i18n": "1.0.0",
"@kbn/telemetry-tools": "1.0.0",
"@kbn/interpreter": "1.0.0",
"@kbn/pm": "1.0.0",
"@kbn/test-subj-selector": "0.2.1",

View file

@ -0,0 +1,89 @@
# Telemetry Tools
## Schema extraction tool
### Description
The tool is used to extract telemetry collectors schema from all `*.{ts}` files in provided plugins directories to JSON files. The tool looks for `.telemetryrc.json` files in the root of the project and in the `x-pack` dir for its runtime configurations.
It uses typescript parser to build an AST for each file. The tool is able to validate, extract and match collector schemas.
### Examples and restrictions
**Global restrictions**:
The `id` can be only a string literal, it cannot be a template literals w/o expressions or string-only concatenation expressions or anything else.
```
export const myCollector = makeUsageCollector<Usage>({
type: 'string_literal_only',
...
});
```
### Usage
```bash
node scripts/telemetry_extract.js
```
This command has no additional flags or arguments. The `.telemetryrc.json` files specify the path to the directory where searching should start, output json files, and files to exclude.
### Output
The generated JSON files contain an ES mapping for each schema. This mapping is used to verify changes in the collectors and as the basis to map those fields into the external telemetry cluster.
**Example**:
```json
{
"properties": {
"cloud": {
"properties": {
"isCloudEnabled": {
"type": "boolean"
}
}
}
}
}
```
## Schema validation tool
### Description
The tool performs a number of checks on all telemetry collectors and verifies the following:
1. Verifies the collector structure, fields, and returned values are using the appropriate types.
2. Verifies that the collector `fetch` function Type matches the specified `schema` in the collector.
3. Verifies that the collector `schema` matches the stored json schema .
### Notes
We don't catch every possible misuse of the collectors, but only the most common and critical ones.
What will not be caught by the validator:
* Mistyped SavedObject/CallCluster return value. Since the hits returned from ES can be typed to anything without any checks. It is advised to add functional tests that grabs the schema json file and checks that the returned usage matches the types exactly.
* Fields in the schema that are never collected. If you are trying to report a field from ES but that value is never stored in ES, the check will not be able to detect if that field is ever collected in the first palce. It is advised to add unit/functional tests to check that all the fields are being reported as expected.
The tool looks for `.telemetryrc.json` files in the root of the project and in the `x-pack` dir for its runtime configurations.
Currently auto-fixer (`--fix`) can automatically fix the json files with the following errors:
* incompatible schema - this error means that the collector schema was changed but the stored json schema file was not updated.
* unused schemas - this error means that a collector was removed or its `type` renamed, the json schema file contains a schema that does not have a corrisponding collector.
### Usage
```bash
node scripts/telemetry_check --fix
```
* `--path` specifies a collector path instead of checking all collectors specified in the `.telemetryrc.json` files. Accepts a `.ts` file. The file must be discoverable by at least one rc file.
* `--fix` tells the tool to try to fix as many violations as possible. All errors that tool won't be able to fix will be reported.

View file

@ -0,0 +1,23 @@
/*
* 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.
*/
module.exports = {
presets: ['@kbn/babel-preset/node_preset'],
ignore: ['**/*.test.ts', '**/__fixture__/**'],
};

View file

@ -0,0 +1,22 @@
{
"name": "@kbn/telemetry-tools",
"version": "1.0.0",
"license": "Apache-2.0",
"main": "./target/index.js",
"private": true,
"scripts": {
"build": "babel src --out-dir target --delete-dir-on-start --extensions .ts --source-maps=inline",
"kbn:bootstrap": "yarn build",
"kbn:watch": "yarn build --watch"
},
"devDependencies": {
"lodash": "npm:@elastic/lodash@3.10.1-kibana4",
"@kbn/dev-utils": "1.0.0",
"@kbn/utility-types": "1.0.0",
"@types/normalize-path": "^3.0.0",
"normalize-path": "^3.0.0",
"@types/lodash": "^3.10.1",
"moment": "^2.24.0",
"typescript": "3.9.5"
}
}

View file

@ -0,0 +1,109 @@
/*
* 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 Listr from 'listr';
import chalk from 'chalk';
import { createFailError, run } from '@kbn/dev-utils';
import {
createTaskContext,
ErrorReporter,
parseConfigsTask,
extractCollectorsTask,
checkMatchingSchemasTask,
generateSchemasTask,
checkCompatibleTypesTask,
writeToFileTask,
TaskContext,
} from '../tools/tasks';
export function runTelemetryCheck() {
run(
async ({ flags: { fix = false, path }, log }) => {
if (typeof fix !== 'boolean') {
throw createFailError(`${chalk.white.bgRed(' TELEMETRY ERROR ')} --fix can't have a value`);
}
if (typeof path === 'boolean') {
throw createFailError(`${chalk.white.bgRed(' TELEMETRY ERROR ')} --path require a value`);
}
if (fix && typeof path !== 'undefined') {
throw createFailError(
`${chalk.white.bgRed(' TELEMETRY ERROR ')} --fix is incompatible with --path flag.`
);
}
const list = new Listr([
{
title: 'Checking .telemetryrc.json files',
task: () => new Listr(parseConfigsTask(), { exitOnError: true }),
},
{
title: 'Extracting Collectors',
task: (context) => new Listr(extractCollectorsTask(context, path), { exitOnError: true }),
},
{
title: 'Checking Compatible collector.schema with collector.fetch type',
task: (context) => new Listr(checkCompatibleTypesTask(context), { exitOnError: true }),
},
{
title: 'Checking Matching collector.schema against stored json files',
task: (context) => new Listr(checkMatchingSchemasTask(context), { exitOnError: true }),
},
{
enabled: (_) => fix,
skip: ({ roots }: TaskContext) => {
return roots.every(({ esMappingDiffs }) => !esMappingDiffs || !esMappingDiffs.length);
},
title: 'Generating new telemetry mappings',
task: (context) => new Listr(generateSchemasTask(context), { exitOnError: true }),
},
{
enabled: (_) => fix,
skip: ({ roots }: TaskContext) => {
return roots.every(({ esMappingDiffs }) => !esMappingDiffs || !esMappingDiffs.length);
},
title: 'Updating telemetry mapping files',
task: (context) => new Listr(writeToFileTask(context), { exitOnError: true }),
},
]);
try {
const context = createTaskContext();
await list.run(context);
} catch (error) {
process.exitCode = 1;
if (error instanceof ErrorReporter) {
error.errors.forEach((e: string | Error) => log.error(e));
} else {
log.error('Unhandled exception!');
log.error(error);
}
}
process.exit();
},
{
flags: {
allowUnexpected: true,
guessTypesForUnexpectedFlags: true,
},
}
);
}

View file

@ -0,0 +1,75 @@
/*
* 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 Listr from 'listr';
import { run } from '@kbn/dev-utils';
import {
createTaskContext,
ErrorReporter,
parseConfigsTask,
extractCollectorsTask,
generateSchemasTask,
writeToFileTask,
} from '../tools/tasks';
export function runTelemetryExtract() {
run(
async ({ flags: {}, log }) => {
const list = new Listr([
{
title: 'Parsing .telemetryrc.json files',
task: () => new Listr(parseConfigsTask(), { exitOnError: true }),
},
{
title: 'Extracting Telemetry Collectors',
task: (context) => new Listr(extractCollectorsTask(context), { exitOnError: true }),
},
{
title: 'Generating Schema files',
task: (context) => new Listr(generateSchemasTask(context), { exitOnError: true }),
},
{
title: 'Writing to file',
task: (context) => new Listr(writeToFileTask(context), { exitOnError: true }),
},
]);
try {
const context = createTaskContext();
await list.run(context);
} catch (error) {
process.exitCode = 1;
if (error instanceof ErrorReporter) {
error.errors.forEach((e: string | Error) => log.error(e));
} else {
log.error('Unhandled exception');
log.error(error);
}
}
process.exit();
},
{
flags: {
allowUnexpected: true,
guessTypesForUnexpectedFlags: true,
},
}
);
}

View file

@ -0,0 +1,21 @@
/*
* 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 { runTelemetryCheck } from './cli/run_telemetry_check';
export { runTelemetryExtract } from './cli/run_telemetry_extract';

View file

@ -0,0 +1,24 @@
{
"properties": {
"my_working_collector": {
"properties": {
"flat": {
"type": "keyword"
},
"my_str": {
"type": "text"
},
"my_objects": {
"properties": {
"total": {
"type": "number"
},
"type": {
"type": "boolean"
}
}
}
}
}
}
}

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 { SyntaxKind } from 'typescript';
import { ParsedUsageCollection } from '../ts_parser';
export const parsedExternallyDefinedCollector: ParsedUsageCollection[] = [
[
'src/fixtures/telemetry_collectors/externally_defined_collector.ts',
{
collectorName: 'from_variable_collector',
schema: {
value: {
locale: {
type: 'keyword',
},
},
},
fetch: {
typeName: 'Usage',
typeDescriptor: {
locale: {
kind: SyntaxKind.StringKeyword,
type: 'StringKeyword',
},
},
},
},
],
[
'src/fixtures/telemetry_collectors/externally_defined_collector.ts',
{
collectorName: 'from_fn_collector',
schema: {
value: {
locale: {
type: 'keyword',
},
},
},
fetch: {
typeName: 'Usage',
typeDescriptor: {
locale: {
kind: SyntaxKind.StringKeyword,
type: 'StringKeyword',
},
},
},
},
],
];

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 { SyntaxKind } from 'typescript';
import { ParsedUsageCollection } from '../ts_parser';
export const parsedImportedSchemaCollector: ParsedUsageCollection[] = [
[
'src/fixtures/telemetry_collectors/imported_schema.ts',
{
collectorName: 'with_imported_schema',
schema: {
value: {
locale: {
type: 'keyword',
},
},
},
fetch: {
typeName: 'Usage',
typeDescriptor: {
locale: {
kind: SyntaxKind.StringKeyword,
type: 'StringKeyword',
},
},
},
},
],
];

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 { SyntaxKind } from 'typescript';
import { ParsedUsageCollection } from '../ts_parser';
export const parsedImportedUsageInterface: ParsedUsageCollection[] = [
[
'src/fixtures/telemetry_collectors/imported_usage_interface.ts',
{
collectorName: 'imported_usage_interface_collector',
schema: {
value: {
locale: {
type: 'keyword',
},
},
},
fetch: {
typeName: 'Usage',
typeDescriptor: {
locale: {
kind: SyntaxKind.StringKeyword,
type: 'StringKeyword',
},
},
},
},
],
];

View file

@ -0,0 +1,44 @@
/*
* 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 { SyntaxKind } from 'typescript';
import { ParsedUsageCollection } from '../ts_parser';
export const parsedNestedCollector: ParsedUsageCollection = [
'src/fixtures/telemetry_collectors/nested_collector.ts',
{
collectorName: 'my_nested_collector',
schema: {
value: {
locale: {
type: 'keyword',
},
},
},
fetch: {
typeName: 'Usage',
typeDescriptor: {
locale: {
kind: SyntaxKind.StringKeyword,
type: 'StringKeyword',
},
},
},
},
];

View file

@ -0,0 +1,69 @@
/*
* 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 { SyntaxKind } from 'typescript';
import { ParsedUsageCollection } from '../ts_parser';
export const parsedWorkingCollector: ParsedUsageCollection = [
'src/fixtures/telemetry_collectors/working_collector.ts',
{
collectorName: 'my_working_collector',
schema: {
value: {
flat: {
type: 'keyword',
},
my_str: {
type: 'text',
},
my_objects: {
total: {
type: 'number',
},
type: {
type: 'boolean',
},
},
},
},
fetch: {
typeName: 'Usage',
typeDescriptor: {
flat: {
kind: SyntaxKind.StringKeyword,
type: 'StringKeyword',
},
my_str: {
kind: SyntaxKind.StringKeyword,
type: 'StringKeyword',
},
my_objects: {
total: {
kind: SyntaxKind.NumberKeyword,
type: 'NumberKeyword',
},
type: {
kind: SyntaxKind.BooleanKeyword,
type: 'BooleanKeyword',
},
},
},
},
},
];

View file

@ -0,0 +1,163 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`extractCollectors extracts collectors given rc file 1`] = `
Array [
Array [
"src/fixtures/telemetry_collectors/externally_defined_collector.ts",
Object {
"collectorName": "from_variable_collector",
"fetch": Object {
"typeDescriptor": Object {
"locale": Object {
"kind": 143,
"type": "StringKeyword",
},
},
"typeName": "Usage",
},
"schema": Object {
"value": Object {
"locale": Object {
"type": "keyword",
},
},
},
},
],
Array [
"src/fixtures/telemetry_collectors/externally_defined_collector.ts",
Object {
"collectorName": "from_fn_collector",
"fetch": Object {
"typeDescriptor": Object {
"locale": Object {
"kind": 143,
"type": "StringKeyword",
},
},
"typeName": "Usage",
},
"schema": Object {
"value": Object {
"locale": Object {
"type": "keyword",
},
},
},
},
],
Array [
"src/fixtures/telemetry_collectors/imported_schema.ts",
Object {
"collectorName": "with_imported_schema",
"fetch": Object {
"typeDescriptor": Object {
"locale": Object {
"kind": 143,
"type": "StringKeyword",
},
},
"typeName": "Usage",
},
"schema": Object {
"value": Object {
"locale": Object {
"type": "keyword",
},
},
},
},
],
Array [
"src/fixtures/telemetry_collectors/imported_usage_interface.ts",
Object {
"collectorName": "imported_usage_interface_collector",
"fetch": Object {
"typeDescriptor": Object {
"locale": Object {
"kind": 143,
"type": "StringKeyword",
},
},
"typeName": "Usage",
},
"schema": Object {
"value": Object {
"locale": Object {
"type": "keyword",
},
},
},
},
],
Array [
"src/fixtures/telemetry_collectors/nested_collector.ts",
Object {
"collectorName": "my_nested_collector",
"fetch": Object {
"typeDescriptor": Object {
"locale": Object {
"kind": 143,
"type": "StringKeyword",
},
},
"typeName": "Usage",
},
"schema": Object {
"value": Object {
"locale": Object {
"type": "keyword",
},
},
},
},
],
Array [
"src/fixtures/telemetry_collectors/working_collector.ts",
Object {
"collectorName": "my_working_collector",
"fetch": Object {
"typeDescriptor": Object {
"flat": Object {
"kind": 143,
"type": "StringKeyword",
},
"my_objects": Object {
"total": Object {
"kind": 140,
"type": "NumberKeyword",
},
"type": Object {
"kind": 128,
"type": "BooleanKeyword",
},
},
"my_str": Object {
"kind": 143,
"type": "StringKeyword",
},
},
"typeName": "Usage",
},
"schema": Object {
"value": Object {
"flat": Object {
"type": "keyword",
},
"my_objects": Object {
"total": Object {
"type": "number",
},
"type": Object {
"type": "boolean",
},
},
"my_str": Object {
"type": "text",
},
},
},
},
],
]
`;

View file

@ -0,0 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
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

@ -0,0 +1,125 @@
/*
* 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 * as _ from 'lodash';
import * as ts from 'typescript';
import { parsedWorkingCollector } from './__fixture__/parsed_working_collector';
import { checkCompatibleTypeDescriptor, checkMatchingMapping } from './check_collector_integrity';
import * as path from 'path';
import { readFile } from 'fs';
import { promisify } from 'util';
const read = promisify(readFile);
async function parseJsonFile(relativePath: string) {
const schemaPath = path.resolve(__dirname, '__fixture__', relativePath);
const fileContent = await read(schemaPath, 'utf8');
return JSON.parse(fileContent);
}
describe('checkMatchingMapping', () => {
it('returns no diff on matching parsedCollections and stored mapping', async () => {
const mockSchema = await parseJsonFile('mock_schema.json');
const diffs = checkMatchingMapping([parsedWorkingCollector], mockSchema);
expect(diffs).toEqual({});
});
describe('Collector change', () => {
it('returns diff on mismatching parsedCollections and stored mapping', async () => {
const mockSchema = await parseJsonFile('mock_schema.json');
const malformedParsedCollector = _.cloneDeep(parsedWorkingCollector);
const fieldMapping = { type: 'number' };
malformedParsedCollector[1].schema.value.flat = fieldMapping;
const diffs = checkMatchingMapping([malformedParsedCollector], mockSchema);
expect(diffs).toEqual({
properties: {
my_working_collector: {
properties: { flat: fieldMapping },
},
},
});
});
it('returns diff on unknown parsedCollections', async () => {
const mockSchema = await parseJsonFile('mock_schema.json');
const malformedParsedCollector = _.cloneDeep(parsedWorkingCollector);
const collectorName = 'New Collector in town!';
const collectorMapping = { some_usage: { type: 'number' } };
malformedParsedCollector[1].collectorName = collectorName;
malformedParsedCollector[1].schema.value = { some_usage: { type: 'number' } };
const diffs = checkMatchingMapping([malformedParsedCollector], mockSchema);
expect(diffs).toEqual({
properties: {
[collectorName]: {
properties: collectorMapping,
},
},
});
});
});
});
describe('checkCompatibleTypeDescriptor', () => {
it('returns no diff on compatible type descriptor with mapping', () => {
const incompatibles = checkCompatibleTypeDescriptor([parsedWorkingCollector]);
expect(incompatibles).toHaveLength(0);
});
describe('Interface Change', () => {
it('returns diff on incompatible type descriptor with mapping', () => {
const malformedParsedCollector = _.cloneDeep(parsedWorkingCollector);
malformedParsedCollector[1].fetch.typeDescriptor.flat.kind = ts.SyntaxKind.BooleanKeyword;
const incompatibles = checkCompatibleTypeDescriptor([malformedParsedCollector]);
expect(incompatibles).toHaveLength(1);
const { diff, message } = incompatibles[0];
expect(diff).toEqual({ 'flat.kind': 'boolean' });
expect(message).toHaveLength(1);
expect(message).toEqual([
'incompatible Type key (Usage.flat): expected ("string") got ("boolean").',
]);
});
it.todo('returns diff when missing type descriptor');
});
describe('Mapping change', () => {
it('returns no diff when mapping change between text and keyword', () => {
const malformedParsedCollector = _.cloneDeep(parsedWorkingCollector);
malformedParsedCollector[1].schema.value.flat.type = 'text';
const incompatibles = checkCompatibleTypeDescriptor([malformedParsedCollector]);
expect(incompatibles).toHaveLength(0);
});
it('returns diff on incompatible type descriptor with mapping', () => {
const malformedParsedCollector = _.cloneDeep(parsedWorkingCollector);
malformedParsedCollector[1].schema.value.flat.type = 'boolean';
const incompatibles = checkCompatibleTypeDescriptor([malformedParsedCollector]);
expect(incompatibles).toHaveLength(1);
const { diff, message } = incompatibles[0];
expect(diff).toEqual({ 'flat.kind': 'string' });
expect(message).toHaveLength(1);
expect(message).toEqual([
'incompatible Type key (Usage.flat): expected ("boolean") got ("string").',
]);
});
it.todo('returns diff when missing mapping');
});
});

View file

@ -0,0 +1,103 @@
/*
* 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 * as _ from 'lodash';
import { difference, flattenKeys, pickDeep } from './utils';
import { ParsedUsageCollection } from './ts_parser';
import { generateMapping, compatibleSchemaTypes } from './manage_schema';
import { kindToDescriptorName } from './serializer';
export function checkMatchingMapping(
UsageCollections: ParsedUsageCollection[],
esMapping: any
): any {
const generatedMapping = generateMapping(UsageCollections);
return difference(generatedMapping, esMapping);
}
interface IncompatibleDescriptor {
diff: Record<string, number>;
collectorPath: string;
message: string[];
}
export function checkCompatibleTypeDescriptor(
usageCollections: ParsedUsageCollection[]
): IncompatibleDescriptor[] {
const results: Array<IncompatibleDescriptor | false> = usageCollections.map(
([collectorPath, collectorDetails]) => {
const typeDescriptorTypes = flattenKeys(
pickDeep(collectorDetails.fetch.typeDescriptor, 'kind')
);
const typeDescriptorKinds = _.reduce(
typeDescriptorTypes,
(acc: any, type: number, key: string) => {
try {
acc[key] = kindToDescriptorName(type);
} catch (err) {
throw Error(`Unrecognized type (${key}: ${type}) in ${collectorPath}`);
}
return acc;
},
{} as any
);
const schemaTypes = flattenKeys(pickDeep(collectorDetails.schema.value, 'type'));
const transformedMappingKinds = _.reduce(
schemaTypes,
(acc: any, type: string, key: string) => {
try {
acc[key.replace(/.type$/, '.kind')] = compatibleSchemaTypes(type as any);
} catch (err) {
throw Error(`Unrecognized type (${key}: ${type}) in ${collectorPath}`);
}
return acc;
},
{} as any
);
const diff: any = difference(typeDescriptorKinds, transformedMappingKinds);
const diffEntries = Object.entries(diff);
if (!diffEntries.length) {
return false;
}
return {
diff,
collectorPath,
message: diffEntries.map(([key]) => {
const interfaceKey = key.replace('.kind', '');
try {
const expectedDescriptorType = JSON.stringify(transformedMappingKinds[key], null, 2);
const actualDescriptorType = JSON.stringify(typeDescriptorKinds[key], null, 2);
return `incompatible Type key (${collectorDetails.fetch.typeName}.${interfaceKey}): expected (${expectedDescriptorType}) got (${actualDescriptorType}).`;
} catch (err) {
throw Error(`Error converting ${key} in ${collectorPath}.\n${err}`);
}
}),
};
}
);
return results.filter((entry): entry is IncompatibleDescriptor => entry !== false);
}
export function checkCollectorIntegrity(UsageCollections: ParsedUsageCollection[], esMapping: any) {
return UsageCollections;
}

View file

@ -0,0 +1,40 @@
/*
* 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 * as path from 'path';
import { parseTelemetryRC } from './config';
describe('parseTelemetryRC', () => {
it('throw if config path is not absolute', async () => {
const fixtureDir = './__fixture__/';
await expect(parseTelemetryRC(fixtureDir)).rejects.toThrowError();
});
it('returns parsed rc file', async () => {
const configRoot = path.join(process.cwd(), 'src', 'fixtures', 'telemetry_collectors');
const config = await parseTelemetryRC(configRoot);
expect(config).toStrictEqual([
{
root: configRoot,
output: configRoot,
exclude: [path.resolve(configRoot, './unmapped_collector.ts')],
},
]);
});
});

View file

@ -0,0 +1,60 @@
/*
* 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 * as path from 'path';
import { readFileAsync } from './utils';
import { TELEMETRY_RC } from './constants';
export interface TelemetryRC {
root: string;
output: string;
exclude: string[];
}
export async function readRcFile(rcRoot: string) {
if (!path.isAbsolute(rcRoot)) {
throw Error(`config root (${rcRoot}) must be an absolute path.`);
}
const rcFile = path.resolve(rcRoot, TELEMETRY_RC);
const configString = await readFileAsync(rcFile, 'utf8');
return JSON.parse(configString);
}
export async function parseTelemetryRC(rcRoot: string): Promise<TelemetryRC[]> {
const parsedRc = await readRcFile(rcRoot);
const configs = Array.isArray(parsedRc) ? parsedRc : [parsedRc];
return configs.map(({ root, output, exclude = [] }) => {
if (typeof root !== 'string') {
throw Error('config.root must be a string.');
}
if (typeof output !== 'string') {
throw Error('config.output must be a string.');
}
if (!Array.isArray(exclude)) {
throw Error('config.exclude must be an array of strings.');
}
return {
root: path.join(rcRoot, root),
output: path.join(rcRoot, output),
exclude: exclude.map((excludedPath) => path.resolve(rcRoot, excludedPath)),
};
});
}

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 const TELEMETRY_RC = '.telemetryrc.json';

View file

@ -0,0 +1,40 @@
/*
* 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 * as ts from 'typescript';
import * as path from 'path';
import { extractCollectors, getProgramPaths } from './extract_collectors';
import { parseTelemetryRC } from './config';
describe('extractCollectors', () => {
it('extracts collectors given rc file', async () => {
const configRoot = path.join(process.cwd(), 'src', 'fixtures', 'telemetry_collectors');
const tsConfig = ts.findConfigFile('./', ts.sys.fileExists, 'tsconfig.json');
if (!tsConfig) {
throw new Error('Could not find a valid tsconfig.json.');
}
const configs = await parseTelemetryRC(configRoot);
expect(configs).toHaveLength(1);
const programPaths = await getProgramPaths(configs[0]);
const results = [...extractCollectors(programPaths, tsConfig)];
expect(results).toHaveLength(6);
expect(results).toMatchSnapshot();
});
});

View file

@ -0,0 +1,75 @@
/*
* 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 * as ts from 'typescript';
import * as path from 'path';
import { parseUsageCollection } from './ts_parser';
import { globAsync } from './utils';
import { TelemetryRC } from './config';
export async function getProgramPaths({
root,
exclude,
}: Pick<TelemetryRC, 'root' | 'exclude'>): Promise<string[]> {
const filePaths = await globAsync('**/*.ts', {
cwd: root,
ignore: [
'**/node_modules/**',
'**/*.test.*',
'**/*.mock.*',
'**/mocks.*',
'**/__fixture__/**',
'**/__tests__/**',
'**/public/**',
'**/dist/**',
'**/target/**',
'**/*.d.ts',
],
});
if (filePaths.length === 0) {
throw Error(`No files found in ${root}`);
}
const fullPaths = filePaths
.map((filePath) => path.join(root, filePath))
.filter((fullPath) => !exclude.some((excludedPath) => fullPath.startsWith(excludedPath)));
if (fullPaths.length === 0) {
throw Error(`No paths covered from ${root} by the .telemetryrc.json`);
}
return fullPaths;
}
export function* extractCollectors(fullPaths: string[], tsConfig: any) {
const program = ts.createProgram(fullPaths, tsConfig);
program.getTypeChecker();
const sourceFiles = fullPaths.map((fullPath) => {
const sourceFile = program.getSourceFile(fullPath);
if (!sourceFile) {
throw Error(`Unable to get sourceFile ${fullPath}.`);
}
return sourceFile;
});
for (const sourceFile of sourceFiles) {
yield* parseUsageCollection(sourceFile, program);
}
}

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.
*/
import { generateMapping } from './manage_schema';
import { parsedWorkingCollector } from './__fixture__/parsed_working_collector';
import * as path from 'path';
import { readFile } from 'fs';
import { promisify } from 'util';
const read = promisify(readFile);
async function parseJsonFile(relativePath: string) {
const schemaPath = path.resolve(__dirname, '__fixture__', relativePath);
const fileContent = await read(schemaPath, 'utf8');
return JSON.parse(fileContent);
}
describe('generateMapping', () => {
it('generates a mapping file', async () => {
const mockSchema = await parseJsonFile('mock_schema.json');
const result = generateMapping([parsedWorkingCollector]);
expect(result).toEqual(mockSchema);
});
});

View file

@ -0,0 +1,86 @@
/*
* 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 { ParsedUsageCollection } from './ts_parser';
export type AllowedSchemaTypes =
| 'keyword'
| 'text'
| 'number'
| 'boolean'
| 'long'
| 'date'
| 'float';
export function compatibleSchemaTypes(type: AllowedSchemaTypes) {
switch (type) {
case 'keyword':
case 'text':
case 'date':
return 'string';
case 'boolean':
return 'boolean';
case 'number':
case 'float':
case 'long':
return 'number';
default:
throw new Error(`Unknown schema type ${type}`);
}
}
export function isObjectMapping(entity: any) {
if (typeof entity === 'object') {
// 'type' is explicitly specified to be an object.
if (typeof entity.type === 'string' && entity.type === 'object') {
return true;
}
// 'type' is not set; ES defaults to object mapping for when type is unspecified.
if (typeof entity.type === 'undefined') {
return true;
}
// 'type' is a field in the mapping and is not the type of the mapping.
if (typeof entity.type === 'object') {
return true;
}
}
return false;
}
function transformToEsMapping(usageMappingValue: any) {
const fieldMapping: any = { properties: {} };
for (const [key, value] of Object.entries(usageMappingValue)) {
fieldMapping.properties[key] = isObjectMapping(value) ? transformToEsMapping(value) : value;
}
return fieldMapping;
}
export function generateMapping(usageCollections: ParsedUsageCollection[]) {
const esMapping: any = { properties: {} };
for (const [, collecionDetails] of usageCollections) {
esMapping.properties[collecionDetails.collectorName] = transformToEsMapping(
collecionDetails.schema.value
);
}
return esMapping;
}

View file

@ -0,0 +1,105 @@
/*
* 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 * as ts from 'typescript';
import * as path from 'path';
import { getDescriptor, TelemetryKinds } from './serializer';
import { traverseNodes } from './ts_parser';
export function loadFixtureProgram(fixtureName: string) {
const fixturePath = path.resolve(
process.cwd(),
'src',
'fixtures',
'telemetry_collectors',
`${fixtureName}.ts`
);
const tsConfig = ts.findConfigFile('./', ts.sys.fileExists, 'tsconfig.json');
if (!tsConfig) {
throw new Error('Could not find a valid tsconfig.json.');
}
const program = ts.createProgram([fixturePath], tsConfig as any);
const checker = program.getTypeChecker();
const sourceFile = program.getSourceFile(fixturePath);
if (!sourceFile) {
throw Error('sourceFile is undefined!');
}
return { program, checker, sourceFile };
}
describe('getDescriptor', () => {
const usageInterfaces = new Map<string, ts.InterfaceDeclaration>();
let tsProgram: ts.Program;
beforeAll(() => {
const { program, sourceFile } = loadFixtureProgram('constants');
tsProgram = program;
for (const node of traverseNodes(sourceFile)) {
if (ts.isInterfaceDeclaration(node)) {
const interfaceName = node.name.getText();
usageInterfaces.set(interfaceName, node);
}
}
});
it('serializes flat types', () => {
const usageInterface = usageInterfaces.get('Usage');
const descriptor = getDescriptor(usageInterface!, tsProgram);
expect(descriptor).toEqual({
locale: { kind: ts.SyntaxKind.StringKeyword, type: 'StringKeyword' },
});
});
it('serializes union types', () => {
const usageInterface = usageInterfaces.get('WithUnion');
const descriptor = getDescriptor(usageInterface!, tsProgram);
expect(descriptor).toEqual({
prop1: { kind: ts.SyntaxKind.StringKeyword, type: 'StringKeyword' },
prop2: { kind: ts.SyntaxKind.StringKeyword, type: 'StringKeyword' },
prop3: { kind: ts.SyntaxKind.StringKeyword, type: 'StringKeyword' },
prop4: { kind: ts.SyntaxKind.StringLiteral, type: 'StringLiteral' },
prop5: { kind: ts.SyntaxKind.FirstLiteralToken, type: 'FirstLiteralToken' },
});
});
it('serializes Moment Dates', () => {
const usageInterface = usageInterfaces.get('WithMoment');
const descriptor = getDescriptor(usageInterface!, tsProgram);
expect(descriptor).toEqual({
prop1: { kind: TelemetryKinds.MomentDate, type: 'MomentDate' },
prop2: { kind: TelemetryKinds.MomentDate, type: 'MomentDate' },
prop3: { kind: TelemetryKinds.MomentDate, type: 'MomentDate' },
prop4: { kind: TelemetryKinds.Date, type: 'Date' },
});
});
it('throws error on conflicting union types', () => {
const usageInterface = usageInterfaces.get('WithConflictingUnion');
expect(() => getDescriptor(usageInterface!, tsProgram)).toThrowError(
'Mapping does not support conflicting union types.'
);
});
it('throws error on unsupported union types', () => {
const usageInterface = usageInterfaces.get('WithUnsupportedUnion');
expect(() => getDescriptor(usageInterface!, tsProgram)).toThrowError(
'Mapping does not support conflicting union types.'
);
});
});

View file

@ -0,0 +1,169 @@
/*
* 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 * as ts from 'typescript';
import { uniq } from 'lodash';
import {
getResolvedModuleSourceFile,
getIdentifierDeclarationFromSource,
getModuleSpecifier,
} from './utils';
export enum TelemetryKinds {
MomentDate = 1000,
Date = 10001,
}
interface DescriptorValue {
kind: ts.SyntaxKind | TelemetryKinds;
type: keyof typeof ts.SyntaxKind | keyof typeof TelemetryKinds;
}
export interface Descriptor {
[name: string]: Descriptor | DescriptorValue;
}
export function isObjectDescriptor(value: any) {
if (typeof value === 'object') {
if (typeof value.type === 'string' && value.type === 'object') {
return true;
}
if (typeof value.type === 'undefined') {
return true;
}
}
return false;
}
export function kindToDescriptorName(kind: number) {
switch (kind) {
case ts.SyntaxKind.StringKeyword:
case ts.SyntaxKind.StringLiteral:
case ts.SyntaxKind.SetKeyword:
case TelemetryKinds.Date:
case TelemetryKinds.MomentDate:
return 'string';
case ts.SyntaxKind.BooleanKeyword:
return 'boolean';
case ts.SyntaxKind.NumberKeyword:
case ts.SyntaxKind.NumericLiteral:
return 'number';
default:
throw new Error(`Unknown kind ${kind}`);
}
}
export function getDescriptor(node: ts.Node, program: ts.Program): Descriptor | DescriptorValue {
if (ts.isMethodSignature(node) || ts.isPropertySignature(node)) {
if (node.type) {
return getDescriptor(node.type, program);
}
}
if (ts.isTypeLiteralNode(node) || ts.isInterfaceDeclaration(node)) {
return node.members.reduce((acc, m) => {
acc[m.name?.getText() || ''] = getDescriptor(m, program);
return acc;
}, {} as any);
}
if (ts.SyntaxKind.FirstNode === node.kind) {
return getDescriptor((node as any).right, program);
}
if (ts.isIdentifier(node)) {
const identifierName = node.getText();
if (identifierName === 'Date') {
return { kind: TelemetryKinds.Date, type: 'Date' };
}
if (identifierName === 'Moment') {
return { kind: TelemetryKinds.MomentDate, type: 'MomentDate' };
}
throw new Error(`Unsupported Identifier ${identifierName}.`);
}
if (ts.isTypeReferenceNode(node)) {
const typeChecker = program.getTypeChecker();
const symbol = typeChecker.getSymbolAtLocation(node.typeName);
const symbolName = symbol?.getName();
if (symbolName === 'Moment') {
return { kind: TelemetryKinds.MomentDate, type: 'MomentDate' };
}
if (symbolName === 'Date') {
return { kind: TelemetryKinds.Date, type: 'Date' };
}
const declaration = (symbol?.getDeclarations() || [])[0];
if (declaration) {
return getDescriptor(declaration, program);
}
return getDescriptor(node.typeName, program);
}
if (ts.isImportSpecifier(node)) {
const source = node.getSourceFile();
const importedModuleName = getModuleSpecifier(node);
const declarationSource = getResolvedModuleSourceFile(source, program, importedModuleName);
const declarationNode = getIdentifierDeclarationFromSource(node.name, declarationSource);
return getDescriptor(declarationNode, program);
}
if (ts.isArrayTypeNode(node)) {
return getDescriptor(node.elementType, program);
}
if (ts.isLiteralTypeNode(node)) {
return {
kind: node.literal.kind,
type: ts.SyntaxKind[node.literal.kind] as keyof typeof ts.SyntaxKind,
};
}
if (ts.isUnionTypeNode(node)) {
const types = node.types.filter((typeNode) => {
return (
typeNode.kind !== ts.SyntaxKind.NullKeyword &&
typeNode.kind !== ts.SyntaxKind.UndefinedKeyword
);
});
const kinds = types.map((typeNode) => getDescriptor(typeNode, program));
const uniqueKinds = uniq(kinds, 'kind');
if (uniqueKinds.length !== 1) {
throw Error('Mapping does not support conflicting union types.');
}
return uniqueKinds[0];
}
switch (node.kind) {
case ts.SyntaxKind.NumberKeyword:
case ts.SyntaxKind.BooleanKeyword:
case ts.SyntaxKind.StringKeyword:
case ts.SyntaxKind.SetKeyword:
return { kind: node.kind, type: ts.SyntaxKind[node.kind] as keyof typeof ts.SyntaxKind };
case ts.SyntaxKind.UnionType:
case ts.SyntaxKind.AnyKeyword:
default:
throw new Error(`Unknown type ${ts.SyntaxKind[node.kind]}; ${node.getText()}`);
}
}

View file

@ -0,0 +1,43 @@
/*
* 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 { TaskContext } from './task_context';
import { checkCompatibleTypeDescriptor } from '../check_collector_integrity';
export function checkCompatibleTypesTask({ reporter, roots }: TaskContext) {
return roots.map((root) => ({
task: async () => {
if (root.parsedCollections) {
const differences = checkCompatibleTypeDescriptor(root.parsedCollections);
const reporterWithContext = reporter.withContext({ name: root.config.root });
if (differences.length) {
reporterWithContext.report(
`${JSON.stringify(
differences,
null,
2
)}. \nPlease fix the collectors and run the check again.`
);
throw reporter;
}
}
},
title: `Checking in ${root.config.root}`,
}));
}

View file

@ -0,0 +1,40 @@
/*
* 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 * as path from 'path';
import { TaskContext } from './task_context';
import { checkMatchingMapping } from '../check_collector_integrity';
import { readFileAsync } from '../utils';
export function checkMatchingSchemasTask({ roots }: TaskContext) {
return roots.map((root) => ({
task: async () => {
const fullPath = path.resolve(process.cwd(), root.config.output);
const esMappingString = await readFileAsync(fullPath, 'utf-8');
const esMapping = JSON.parse(esMappingString);
if (root.parsedCollections) {
const differences = checkMatchingMapping(root.parsedCollections, esMapping);
root.esMappingDiffs = Object.keys(differences);
}
},
title: `Checking in ${root.config.root}`,
}));
}

View file

@ -0,0 +1,34 @@
/*
* 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 chalk from 'chalk';
import { normalizePath } from '../utils';
export class ErrorReporter {
errors: string[] = [];
withContext(context: any) {
return { report: (error: any) => this.report(error, context) };
}
report(error: any, context: any) {
this.errors.push(
`${chalk.white.bgRed(' TELEMETRY ERROR ')} Error in ${normalizePath(context.name)}\n${error}`
);
}
}

View file

@ -0,0 +1,58 @@
/*
* 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 * as ts from 'typescript';
import * as path from 'path';
import { TaskContext } from './task_context';
import { extractCollectors, getProgramPaths } from '../extract_collectors';
export function extractCollectorsTask(
{ roots }: TaskContext,
restrictProgramToPath?: string | string[]
) {
return roots.map((root) => ({
task: async () => {
const tsConfig = ts.findConfigFile('./', ts.sys.fileExists, 'tsconfig.json');
if (!tsConfig) {
throw new Error('Could not find a valid tsconfig.json.');
}
const programPaths = await getProgramPaths(root.config);
if (typeof restrictProgramToPath !== 'undefined') {
const restrictProgramToPaths = Array.isArray(restrictProgramToPath)
? restrictProgramToPath
: [restrictProgramToPath];
const fullRestrictedPaths = restrictProgramToPaths.map((collectorPath) =>
path.resolve(process.cwd(), collectorPath)
);
const restrictedProgramPaths = programPaths.filter((programPath) =>
fullRestrictedPaths.includes(programPath)
);
if (restrictedProgramPaths.length) {
root.parsedCollections = [...extractCollectors(restrictedProgramPaths, tsConfig)];
}
return;
}
root.parsedCollections = [...extractCollectors(programPaths, tsConfig)];
},
title: `Extracting collectors in ${root.config.root}`,
}));
}

View file

@ -0,0 +1,35 @@
/*
* 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 * as _ from 'lodash';
import { TaskContext } from './task_context';
import { generateMapping } from '../manage_schema';
export function generateSchemasTask({ roots }: TaskContext) {
return roots.map((root) => ({
task: () => {
if (!root.parsedCollections || !root.parsedCollections.length) {
return;
}
const mapping = generateMapping(root.parsedCollections);
root.mapping = mapping;
},
title: `Generating mapping for ${root.config.root}`,
}));
}

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.
*/
export { ErrorReporter } from './error_reporter';
export { TaskContext, createTaskContext } from './task_context';
export { parseConfigsTask } from './parse_configs_task';
export { extractCollectorsTask } from './extract_collectors_task';
export { generateSchemasTask } from './generate_schemas_task';
export { writeToFileTask } from './write_to_file_task';
export { checkMatchingSchemasTask } from './check_matching_schemas_task';
export { checkCompatibleTypesTask } from './check_compatible_types_task';

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 * as path from 'path';
import { parseTelemetryRC } from '../config';
import { TaskContext } from './task_context';
export function parseConfigsTask() {
const kibanaRoot = process.cwd();
const xpackRoot = path.join(kibanaRoot, 'x-pack');
const configRoots = [kibanaRoot, xpackRoot];
return configRoots.map((configRoot) => ({
task: async (context: TaskContext) => {
try {
const configs = await parseTelemetryRC(configRoot);
configs.forEach((config) => {
context.roots.push({ config });
});
} catch (err) {
const { reporter } = context;
const reporterWithContext = reporter.withContext({ name: configRoot });
reporterWithContext.report(err);
throw reporter;
}
},
title: `Parsing configs in ${configRoot}`,
}));
}

View file

@ -0,0 +1,41 @@
/*
* 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 { TelemetryRC } from '../config';
import { ErrorReporter } from './error_reporter';
import { ParsedUsageCollection } from '../ts_parser';
export interface TelemetryRoot {
config: TelemetryRC;
parsedCollections?: ParsedUsageCollection[];
mapping?: any;
esMappingDiffs?: string[];
}
export interface TaskContext {
reporter: ErrorReporter;
roots: TelemetryRoot[];
}
export function createTaskContext(): TaskContext {
const reporter = new ErrorReporter();
return {
roots: [],
reporter,
};
}

View file

@ -0,0 +1,35 @@
/*
* 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 * as path from 'path';
import { writeFileAsync } from '../utils';
import { TaskContext } from './task_context';
export function writeToFileTask({ roots }: TaskContext) {
return roots.map((root) => ({
task: async () => {
const fullPath = path.resolve(process.cwd(), root.config.output);
if (root.mapping && Object.keys(root.mapping.properties).length > 0) {
const serializedMapping = JSON.stringify(root.mapping, null, 2).concat('\n');
await writeFileAsync(fullPath, serializedMapping);
}
},
title: `Writing mapping for ${root.config.root}`,
}));
}

View file

@ -0,0 +1,94 @@
/*
* 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 { parseUsageCollection } from './ts_parser';
import * as ts from 'typescript';
import * as path from 'path';
import { parsedWorkingCollector } from './__fixture__/parsed_working_collector';
import { parsedNestedCollector } from './__fixture__/parsed_nested_collector';
import { parsedExternallyDefinedCollector } from './__fixture__/parsed_externally_defined_collector';
import { parsedImportedUsageInterface } from './__fixture__/parsed_imported_usage_interface';
import { parsedImportedSchemaCollector } from './__fixture__/parsed_imported_schema';
export function loadFixtureProgram(fixtureName: string) {
const fixturePath = path.resolve(
process.cwd(),
'src',
'fixtures',
'telemetry_collectors',
`${fixtureName}.ts`
);
const tsConfig = ts.findConfigFile('./', ts.sys.fileExists, 'tsconfig.json');
if (!tsConfig) {
throw new Error('Could not find a valid tsconfig.json.');
}
const program = ts.createProgram([fixturePath], tsConfig as any);
const checker = program.getTypeChecker();
const sourceFile = program.getSourceFile(fixturePath);
if (!sourceFile) {
throw Error('sourceFile is undefined!');
}
return { program, checker, sourceFile };
}
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 mapping fields is not defined', () => {
const { program, sourceFile } = loadFixtureProgram('unmapped_collector');
expect(() => [...parseUsageCollection(sourceFile, program)]).toThrowErrorMatchingSnapshot();
});
it('parses root level defined collector', () => {
const { program, sourceFile } = loadFixtureProgram('working_collector');
const result = [...parseUsageCollection(sourceFile, program)];
expect(result).toEqual([parsedWorkingCollector]);
});
it('parses nested collectors', () => {
const { program, sourceFile } = loadFixtureProgram('nested_collector');
const result = [...parseUsageCollection(sourceFile, program)];
expect(result).toEqual([parsedNestedCollector]);
});
it('parses imported schema property', () => {
const { program, sourceFile } = loadFixtureProgram('imported_schema');
const result = [...parseUsageCollection(sourceFile, program)];
expect(result).toEqual(parsedImportedSchemaCollector);
});
it('parses externally defined collectors', () => {
const { program, sourceFile } = loadFixtureProgram('externally_defined_collector');
const result = [...parseUsageCollection(sourceFile, program)];
expect(result).toEqual(parsedExternallyDefinedCollector);
});
it('parses imported Usage interface', () => {
const { program, sourceFile } = loadFixtureProgram('imported_usage_interface');
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 result = [...parseUsageCollection(sourceFile, program)];
expect(result).toEqual([]);
});
});

View file

@ -0,0 +1,210 @@
/*
* 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 * as ts from 'typescript';
import { createFailError } from '@kbn/dev-utils';
import * as path from 'path';
import { getProperty, getPropertyValue } from './utils';
import { getDescriptor, Descriptor } from './serializer';
export function* traverseNodes(maybeNodes: ts.Node | ts.Node[]): Generator<ts.Node> {
const nodes: ts.Node[] = Array.isArray(maybeNodes) ? maybeNodes : [maybeNodes];
for (const node of nodes) {
const children: ts.Node[] = [];
yield node;
ts.forEachChild(node, (child) => {
children.push(child);
});
for (const child of children) {
yield* traverseNodes(child);
}
}
}
export function isMakeUsageCollectorFunction(
node: ts.Node,
sourceFile: ts.SourceFile
): node is ts.CallExpression {
if (ts.isCallExpression(node)) {
const isMakeUsageCollector = /makeUsageCollector$/.test(node.expression.getText(sourceFile));
if (isMakeUsageCollector) {
return true;
}
}
return false;
}
export interface CollectorDetails {
collectorName: string;
fetch: { typeName: string; typeDescriptor: Descriptor };
schema: { value: any };
}
function getCollectionConfigNode(
collectorNode: ts.CallExpression,
sourceFile: ts.SourceFile
): ts.Expression {
if (collectorNode.arguments.length > 1) {
throw Error(`makeUsageCollector does not accept more than one argument.`);
}
const collectorConfig = collectorNode.arguments[0];
if (ts.isObjectLiteralExpression(collectorConfig)) {
return collectorConfig;
}
const variableDefintionName = collectorConfig.getText();
for (const node of traverseNodes(sourceFile)) {
if (ts.isVariableDeclaration(node)) {
const declarationName = node.name.getText();
if (declarationName === variableDefintionName) {
if (!node.initializer) {
throw Error(`Unable to parse collector configs.`);
}
if (ts.isObjectLiteralExpression(node.initializer)) {
return node.initializer;
}
if (ts.isCallExpression(node.initializer)) {
const functionName = node.initializer.expression.getText(sourceFile);
for (const sfNode of traverseNodes(sourceFile)) {
if (ts.isFunctionDeclaration(sfNode)) {
const fnDeclarationName = sfNode.name?.getText();
if (fnDeclarationName === functionName) {
const returnStatements: ts.ReturnStatement[] = [];
for (const fnNode of traverseNodes(sfNode)) {
if (ts.isReturnStatement(fnNode) && fnNode.parent === sfNode.body) {
returnStatements.push(fnNode);
}
}
if (returnStatements.length > 1) {
throw Error(`Collector function cannot have multiple return statements.`);
}
if (returnStatements.length === 0) {
throw Error(`Collector function must have a return statement.`);
}
if (!returnStatements[0].expression) {
throw Error(`Collector function return statement must be an expression.`);
}
return returnStatements[0].expression;
}
}
}
}
}
}
}
throw Error(`makeUsageCollector argument must be an object.`);
}
function extractCollectorDetails(
collectorNode: ts.CallExpression,
program: ts.Program,
sourceFile: ts.SourceFile
): CollectorDetails {
if (collectorNode.arguments.length > 1) {
throw Error(`makeUsageCollector does not accept more than one argument.`);
}
const collectorConfig = getCollectionConfigNode(collectorNode, sourceFile);
const typeProperty = getProperty(collectorConfig, 'type');
if (!typeProperty) {
throw Error(`usageCollector.type must be defined.`);
}
const typePropertyValue = getPropertyValue(typeProperty, program);
if (!typePropertyValue || typeof typePropertyValue !== 'string') {
throw Error(`usageCollector.type must be be a non-empty string literal.`);
}
const fetchProperty = getProperty(collectorConfig, 'fetch');
if (!fetchProperty) {
throw Error(`usageCollector.fetch must be defined.`);
}
const schemaProperty = getProperty(collectorConfig, 'schema');
if (!schemaProperty) {
throw Error(`usageCollector.schema must be defined.`);
}
const schemaPropertyValue = getPropertyValue(schemaProperty, program, { chaseImport: true });
if (!schemaPropertyValue || typeof schemaPropertyValue !== 'object') {
throw Error(`usageCollector.schema must be be an object.`);
}
const collectorNodeType = collectorNode.typeArguments;
if (!collectorNodeType || collectorNodeType?.length === 0) {
throw Error(`makeUsageCollector requires a Usage type makeUsageCollector<Usage>({ ... }).`);
}
const usageTypeNode = collectorNodeType[0];
const usageTypeName = usageTypeNode.getText();
const usageType = getDescriptor(usageTypeNode, program) as Descriptor;
return {
collectorName: typePropertyValue,
schema: {
value: schemaPropertyValue,
},
fetch: {
typeName: usageTypeName,
typeDescriptor: usageType,
},
};
}
export function sourceHasUsageCollector(sourceFile: ts.SourceFile) {
if (sourceFile.isDeclarationFile === true || (sourceFile as any).identifierCount === 0) {
return false;
}
const identifiers = (sourceFile as any).identifiers;
if (
(!identifiers.get('makeUsageCollector') && !identifiers.get('type')) ||
!identifiers.get('fetch')
) {
return false;
}
return true;
}
export type ParsedUsageCollection = [string, CollectorDetails];
export function* parseUsageCollection(
sourceFile: ts.SourceFile,
program: ts.Program
): Generator<ParsedUsageCollection> {
const relativePath = path.relative(process.cwd(), sourceFile.fileName);
if (sourceHasUsageCollector(sourceFile)) {
for (const node of traverseNodes(sourceFile)) {
if (isMakeUsageCollectorFunction(node, sourceFile)) {
try {
const collectorDetails = extractCollectorDetails(node, program, sourceFile);
yield [relativePath, collectorDetails];
} catch (err) {
throw createFailError(`Error extracting collector in ${relativePath}\n${err}`);
}
}
}
}
}

View file

@ -0,0 +1,238 @@
/*
* 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 * as ts from 'typescript';
import * as _ from 'lodash';
import * as path from 'path';
import glob from 'glob';
import { readFile, writeFile } from 'fs';
import { promisify } from 'util';
import normalize from 'normalize-path';
import { Optional } from '@kbn/utility-types';
export const readFileAsync = promisify(readFile);
export const writeFileAsync = promisify(writeFile);
export const globAsync = promisify(glob);
export function isPropertyWithKey(property: ts.Node, identifierName: string) {
if (ts.isPropertyAssignment(property) || ts.isMethodDeclaration(property)) {
if (ts.isIdentifier(property.name)) {
return property.name.text === identifierName;
}
}
return false;
}
export function getProperty(objectNode: any, propertyName: string): ts.Node | null {
let foundProperty = null;
ts.visitNodes(objectNode?.properties || [], (node) => {
if (isPropertyWithKey(node, propertyName)) {
foundProperty = node;
return node;
}
});
return foundProperty;
}
export function getModuleSpecifier(node: ts.Node): string {
if ((node as any).moduleSpecifier) {
return (node as any).moduleSpecifier.text;
}
return getModuleSpecifier(node.parent);
}
export function getIdentifierDeclarationFromSource(node: ts.Node, source: ts.SourceFile) {
if (!ts.isIdentifier(node)) {
throw new Error(`node is not an identifier ${node.getText()}`);
}
const identifierName = node.getText();
const identifierDefinition: ts.Node = (source as any).locals.get(identifierName);
if (!identifierDefinition) {
throw new Error(`Unable to fine identifier in source ${identifierName}`);
}
const declarations = (identifierDefinition as any).declarations as ts.Node[];
const latestDeclaration: ts.Node | false | undefined =
Array.isArray(declarations) && declarations[declarations.length - 1];
if (!latestDeclaration) {
throw new Error(`Unable to fine declaration for identifier ${identifierName}`);
}
return latestDeclaration;
}
export function getIdentifierDeclaration(node: ts.Node) {
const source = node.getSourceFile();
if (!source) {
throw new Error('Unable to get source from node; check program configs.');
}
return getIdentifierDeclarationFromSource(node, source);
}
export function getVariableValue(node: ts.Node): string | Record<string, any> {
if (ts.isStringLiteral(node) || ts.isNumericLiteral(node)) {
return node.text;
}
if (ts.isObjectLiteralExpression(node)) {
return serializeObject(node);
}
throw Error(`Unsuppored Node: cannot get value of node (${node.getText()}) of kind ${node.kind}`);
}
export function serializeObject(node: ts.Node) {
if (!ts.isObjectLiteralExpression(node)) {
throw new Error(`Expecting Object literal Expression got ${node.getText()}`);
}
const value: Record<string, any> = {};
for (const property of node.properties) {
const propertyName = property.name?.getText();
if (typeof propertyName === 'undefined') {
throw new Error(`Unable to get property name ${property.getText()}`);
}
if (ts.isPropertyAssignment(property)) {
value[propertyName] = getVariableValue(property.initializer);
} else {
value[propertyName] = getVariableValue(property);
}
}
return value;
}
export function getResolvedModuleSourceFile(
originalSource: ts.SourceFile,
program: ts.Program,
importedModuleName: string
) {
const resolvedModule = (originalSource as any).resolvedModules.get(importedModuleName);
const resolvedModuleSourceFile = program.getSourceFile(resolvedModule.resolvedFileName);
if (!resolvedModuleSourceFile) {
throw new Error(`Unable to find resolved module ${importedModuleName}`);
}
return resolvedModuleSourceFile;
}
export function getPropertyValue(
node: ts.Node,
program: ts.Program,
config: Optional<{ chaseImport: boolean }> = {}
) {
const { chaseImport = false } = config;
if (ts.isPropertyAssignment(node)) {
const { initializer } = node;
if (ts.isIdentifier(initializer)) {
const identifierName = initializer.getText();
const declaration = getIdentifierDeclaration(initializer);
if (ts.isImportSpecifier(declaration)) {
if (!chaseImport) {
throw new Error(
`Value of node ${identifierName} is imported from another file. Chasing imports is not allowed.`
);
}
const importedModuleName = getModuleSpecifier(declaration);
const source = node.getSourceFile();
const declarationSource = getResolvedModuleSourceFile(source, program, importedModuleName);
const declarationNode = getIdentifierDeclarationFromSource(initializer, declarationSource);
if (!ts.isVariableDeclaration(declarationNode)) {
throw new Error(`Expected ${identifierName} to be variable declaration.`);
}
if (!declarationNode.initializer) {
throw new Error(`Expected ${identifierName} to be initialized.`);
}
const serializedObject = serializeObject(declarationNode.initializer);
return serializedObject;
}
return getVariableValue(declaration);
}
return getVariableValue(initializer);
}
}
export function pickDeep(collection: any, identity: any, thisArg?: any) {
const picked: any = _.pick(collection, identity, thisArg);
const collections = _.pick(collection, _.isObject, thisArg);
_.each(collections, function (item, key) {
let object;
if (_.isArray(item)) {
object = _.reduce(
item,
function (result, value) {
const pickedDeep = pickDeep(value, identity, thisArg);
if (!_.isEmpty(pickedDeep)) {
result.push(pickedDeep);
}
return result;
},
[] as any[]
);
} else {
object = pickDeep(item, identity, thisArg);
}
if (!_.isEmpty(object)) {
picked[key || ''] = object;
}
});
return picked;
}
export const flattenKeys = (obj: any, keyPath: any[] = []): any => {
if (_.isObject(obj)) {
return _.reduce(
obj,
(cum, next, key) => {
const keys = [...keyPath, key];
return _.merge(cum, flattenKeys(next, keys));
},
{}
);
}
return { [keyPath.join('.')]: obj };
};
export function difference(actual: any, expected: any) {
function changes(obj: any, base: any) {
return _.transform(obj, function (result, value, key) {
if (key && !_.isEqual(value, base[key])) {
result[key] =
_.isObject(value) && _.isObject(base[key]) ? changes(value, base[key]) : value;
}
});
}
return changes(actual, expected);
}
export function normalizePath(inputPath: string) {
return normalize(path.relative('.', inputPath));
}

View file

@ -0,0 +1,6 @@
{
"extends": "../../tsconfig.json",
"include": [
"src/**/*",
]
}

View file

@ -0,0 +1,21 @@
/*
* 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.
*/
require('../src/setup_node_env/prebuilt_dev_only_entry');
require('@kbn/telemetry-tools').runTelemetryCheck();

View file

@ -0,0 +1,21 @@
/*
* 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.
*/
require('../src/setup_node_env/prebuilt_dev_only_entry');
require('@kbn/telemetry-tools').runTelemetryExtract();

View file

@ -0,0 +1,7 @@
{
"root": ".",
"output": ".",
"exclude": [
"./unmapped_collector.ts"
]
}

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 moment, { Moment } from 'moment';
import { MakeSchemaFrom } from '../../plugins/usage_collection/server';
export interface Usage {
locale: string;
}
export interface WithUnion {
prop1: string | null;
prop2: string | null | undefined;
prop3?: string | null;
prop4: 'opt1' | 'opt2';
prop5: 123 | 431;
}
export interface WithMoment {
prop1: Moment;
prop2: moment.Moment;
prop3: Moment[];
prop4: Date[];
}
export interface WithConflictingUnion {
prop1: 123 | 'str';
}
export interface WithUnsupportedUnion {
prop1: 123 | Moment;
}
export const externallyDefinedSchema: MakeSchemaFrom<{ locale: string }> = {
locale: {
type: 'keyword',
},
};

View file

@ -0,0 +1,71 @@
/*
* 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 { CollectorSet, CollectorOptions } from '../../plugins/usage_collection/server/collector';
import { loggerMock } from '../../core/server/logging/logger.mock';
const collectorSet = new CollectorSet({
logger: loggerMock.create(),
maximumWaitTimeForAllCollectorsInS: 0,
});
interface Usage {
locale: string;
}
function createCollector(): CollectorOptions<Usage> {
return {
type: 'from_fn_collector',
isReady: () => true,
fetch(): Usage {
return {
locale: 'en',
};
},
schema: {
locale: {
type: 'keyword',
},
},
};
}
export function defineCollectorFromVariable() {
const fromVarCollector: CollectorOptions<Usage> = {
type: 'from_variable_collector',
isReady: () => true,
fetch(): Usage {
return {
locale: 'en',
};
},
schema: {
locale: {
type: 'keyword',
},
},
};
collectorSet.makeUsageCollector<Usage>(fromVarCollector);
}
export function defineCollectorFromFn() {
const fromFnCollector = createCollector();
collectorSet.makeUsageCollector<Usage>(fromFnCollector);
}

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 const SOME_CONST: number = 123;

View file

@ -0,0 +1,41 @@
/*
* 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 { CollectorSet } from '../../plugins/usage_collection/server/collector';
import { loggerMock } from '../../core/server/logging/logger.mock';
import { externallyDefinedSchema } from './constants';
const { makeUsageCollector } = new CollectorSet({
logger: loggerMock.create(),
maximumWaitTimeForAllCollectorsInS: 0,
});
interface Usage {
locale?: string;
}
export const myCollector = makeUsageCollector<Usage>({
type: 'with_imported_schema',
isReady: () => true,
schema: externallyDefinedSchema,
fetch(): Usage {
return {
locale: 'en',
};
},
});

View file

@ -0,0 +1,41 @@
/*
* 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 { CollectorSet } from '../../plugins/usage_collection/server/collector';
import { loggerMock } from '../../core/server/logging/logger.mock';
import { Usage } from './constants';
const { makeUsageCollector } = new CollectorSet({
logger: loggerMock.create(),
maximumWaitTimeForAllCollectorsInS: 0,
});
export const myCollector = makeUsageCollector<Usage>({
type: 'imported_usage_interface_collector',
isReady: () => true,
fetch() {
return {
locale: 'en',
};
},
schema: {
locale: {
type: 'keyword',
},
},
});

View file

@ -0,0 +1,49 @@
/*
* 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 { CollectorSet, UsageCollector } from '../../plugins/usage_collection/server/collector';
import { loggerMock } from '../../core/server/logging/logger.mock';
const collectorSet = new CollectorSet({
logger: loggerMock.create(),
maximumWaitTimeForAllCollectorsInS: 0,
});
interface Usage {
locale?: string;
}
export class NestedInside {
collector?: UsageCollector<Usage, Usage>;
createMyCollector() {
this.collector = collectorSet.makeUsageCollector<Usage>({
type: 'my_nested_collector',
isReady: () => true,
fetch: async () => {
return {
locale: 'en',
};
},
schema: {
locale: {
type: 'keyword',
},
},
});
}
}

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.
*/
import { CollectorSet } from '../../plugins/usage_collection/server/collector';
import { loggerMock } from '../../core/server/logging/logger.mock';
const { makeUsageCollector } = new CollectorSet({
logger: loggerMock.create(),
maximumWaitTimeForAllCollectorsInS: 0,
});
interface Usage {
locale: string;
}
export const myCollector = makeUsageCollector<Usage>({
type: 'unmapped_collector',
isReady: () => true,
fetch(): Usage {
return {
locale: 'en',
};
},
});

View file

@ -0,0 +1,81 @@
/*
* 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 { CollectorSet } from '../../plugins/usage_collection/server/collector';
import { loggerMock } from '../../core/server/logging/logger.mock';
const { makeUsageCollector } = new CollectorSet({
logger: loggerMock.create(),
maximumWaitTimeForAllCollectorsInS: 0,
});
interface MyObject {
total: number;
type: boolean;
}
interface Usage {
flat?: string;
my_str?: string;
my_objects: MyObject;
}
const SOME_NUMBER: number = 123;
export const myCollector = makeUsageCollector<Usage>({
type: 'my_working_collector',
isReady: () => true,
fetch() {
const testString = '123';
// query ES and get some data
// summarize the data into a model
// return the modeled object that includes whatever you want to track
try {
return {
flat: 'hello',
my_str: testString,
my_objects: {
total: SOME_NUMBER,
type: true,
},
};
} catch (err) {
return {
my_objects: {
total: 0,
type: true,
},
};
}
},
schema: {
flat: {
type: 'keyword',
},
my_str: {
type: 'text',
},
my_objects: {
total: {
type: 'number',
},
type: { type: 'boolean' },
},
},
});

View file

@ -34,6 +34,7 @@ const createMockKbnServer = () => ({
describe('csp collector', () => {
let kbnServer: ReturnType<typeof createMockKbnServer>;
const mockCallCluster = null as any;
function updateCsp(config: Partial<ICspConfig>) {
kbnServer.newPlatform.setup.core.http.csp = new CspConfig(config);
@ -46,28 +47,28 @@ describe('csp collector', () => {
test('fetches whether strict mode is enabled', async () => {
const collector = createCspCollector(kbnServer as any);
expect((await collector.fetch()).strict).toEqual(false);
expect((await collector.fetch(mockCallCluster)).strict).toEqual(false);
updateCsp({ strict: true });
expect((await collector.fetch()).strict).toEqual(true);
expect((await collector.fetch(mockCallCluster)).strict).toEqual(true);
});
test('fetches whether the legacy browser warning is enabled', async () => {
const collector = createCspCollector(kbnServer as any);
expect((await collector.fetch()).warnLegacyBrowsers).toEqual(true);
expect((await collector.fetch(mockCallCluster)).warnLegacyBrowsers).toEqual(true);
updateCsp({ warnLegacyBrowsers: false });
expect((await collector.fetch()).warnLegacyBrowsers).toEqual(false);
expect((await collector.fetch(mockCallCluster)).warnLegacyBrowsers).toEqual(false);
});
test('fetches whether the csp rules have been changed or not', async () => {
const collector = createCspCollector(kbnServer as any);
expect((await collector.fetch()).rulesChangedFromDefault).toEqual(false);
expect((await collector.fetch(mockCallCluster)).rulesChangedFromDefault).toEqual(false);
updateCsp({ rules: ['not', 'default'] });
expect((await collector.fetch()).rulesChangedFromDefault).toEqual(true);
expect((await collector.fetch(mockCallCluster)).rulesChangedFromDefault).toEqual(true);
});
test('does not include raw csp rules under any property names', async () => {
@ -79,7 +80,7 @@ describe('csp collector', () => {
//
// We use a snapshot here to ensure csp.rules isn't finding its way into the
// payload under some new and unexpected variable name (e.g. cspRules).
expect(await collector.fetch()).toMatchInlineSnapshot(`
expect(await collector.fetch(mockCallCluster)).toMatchInlineSnapshot(`
Object {
"rulesChangedFromDefault": false,
"strict": false,

View file

@ -19,9 +19,18 @@
import { Server } from 'hapi';
import { CspConfig } from '../../../../../../core/server';
import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server';
import {
UsageCollectionSetup,
CollectorOptions,
} from '../../../../../../plugins/usage_collection/server';
export function createCspCollector(server: Server) {
interface Usage {
strict: boolean;
warnLegacyBrowsers: boolean;
rulesChangedFromDefault: boolean;
}
export function createCspCollector(server: Server): CollectorOptions<Usage> {
return {
type: 'csp',
isReady: () => true,
@ -37,10 +46,22 @@ export function createCspCollector(server: Server) {
rulesChangedFromDefault: header !== CspConfig.DEFAULT.header,
};
},
schema: {
strict: {
type: 'boolean',
},
warnLegacyBrowsers: {
type: 'boolean',
},
rulesChangedFromDefault: {
type: 'boolean',
},
},
};
}
export function registerCspCollector(usageCollection: UsageCollectionSetup, server: Server): void {
const collector = usageCollection.makeUsageCollector(createCspCollector(server));
const collectorConfig = createCspCollector(server);
const collector = usageCollection.makeUsageCollector<Usage>(collectorConfig);
usageCollection.registerCollector(collector);
}

View file

@ -23,8 +23,14 @@ import { DEFAULT_QUERY_LANGUAGE, UI_SETTINGS } from '../../../common';
const defaultSearchQueryLanguageSetting = DEFAULT_QUERY_LANGUAGE;
export interface Usage {
optInCount: number;
optOutCount: number;
defaultQueryLanguage: string;
}
export function fetchProvider(index: string) {
return async (callCluster: APICaller) => {
return async (callCluster: APICaller): Promise<Usage> => {
const [response, config] = await Promise.all([
callCluster('get', {
index,
@ -38,7 +44,7 @@ export function fetchProvider(index: string) {
}),
]);
const queryLanguageConfigValue = get(
const queryLanguageConfigValue: string | null | undefined = get(
config,
`hits.hits[0]._source.config.${UI_SETTINGS.SEARCH_QUERY_LANGUAGE}`
);

View file

@ -17,18 +17,22 @@
* under the License.
*/
import { fetchProvider } from './fetch';
import { fetchProvider, Usage } from './fetch';
import { UsageCollectionSetup } from '../../../../usage_collection/server';
export async function makeKQLUsageCollector(
usageCollection: UsageCollectionSetup,
kibanaIndex: string
) {
const fetch = fetchProvider(kibanaIndex);
const kqlUsageCollector = usageCollection.makeUsageCollector({
const kqlUsageCollector = usageCollection.makeUsageCollector<Usage>({
type: 'kql',
fetch,
fetch: fetchProvider(kibanaIndex),
isReady: () => true,
schema: {
optInCount: { type: 'long' },
optOutCount: { type: 'long' },
defaultQueryLanguage: { type: 'keyword' },
},
});
usageCollection.registerCollector(kqlUsageCollector);

View file

@ -19,7 +19,7 @@
import { PluginInitializerContext } from 'kibana/server';
import { first } from 'rxjs/operators';
import { fetchProvider } from './collector_fetch';
import { fetchProvider, TelemetryResponse } from './collector_fetch';
import { UsageCollectionSetup } from '../../../../../usage_collection/server';
export async function makeSampleDataUsageCollector(
@ -33,10 +33,18 @@ export async function makeSampleDataUsageCollector(
} catch (err) {
return; // kibana plugin is not enabled (test environment)
}
const collector = usageCollection.makeUsageCollector({
const collector = usageCollection.makeUsageCollector<TelemetryResponse>({
type: 'sample-data',
fetch: fetchProvider(index),
isReady: () => true,
schema: {
installed: { type: 'keyword' },
last_install_date: { type: 'date' },
last_install_set: { type: 'keyword' },
last_uninstall_date: { type: 'date' },
last_uninstall_set: { type: 'keyword' },
uninstalled: { type: 'keyword' },
},
});
usageCollection.registerCollector(collector);

View file

@ -31,7 +31,7 @@ interface SearchHit {
};
}
interface TelemetryResponse {
export interface TelemetryResponse {
installed: string[];
uninstalled: string[];
last_install_date: moment.Moment | null;

View file

@ -20,27 +20,6 @@
export const PLUGIN_ID = 'kibanaUsageCollection';
export const PLUGIN_NAME = 'kibana_usage_collection';
/**
* UI metric usage type
*/
export const UI_METRIC_USAGE_TYPE = 'ui_metric';
/**
* Application Usage type
*/
export const APPLICATION_USAGE_TYPE = 'application_usage';
/**
* The type name used within the Monitoring index to publish management stats.
*/
export const KIBANA_STACK_MANAGEMENT_STATS_TYPE = 'stack_management';
/**
* The type name used to publish Kibana usage stats.
* NOTE: this string shows as-is in the stats API as a field name for the kibana usage stats
*/
export const KIBANA_USAGE_TYPE = 'kibana';
/**
* The type name used to publish Kibana usage stats in the formatted as bulk.
*/

View file

@ -20,7 +20,6 @@
import moment from 'moment';
import { ISavedObjectsRepository, SavedObjectsServiceSetup } from 'kibana/server';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { APPLICATION_USAGE_TYPE } from '../../../common/constants';
import { findAll } from '../find_all';
import {
ApplicationUsageTotal,
@ -62,7 +61,7 @@ export function registerApplicationUsageCollector(
registerMappings(registerType);
const collector = usageCollection.makeUsageCollector({
type: APPLICATION_USAGE_TYPE,
type: 'application_usage',
isReady: () => typeof getSavedObjectsClient() !== 'undefined',
fetch: async () => {
const savedObjectsClient = getSavedObjectsClient();

View file

@ -21,7 +21,7 @@ import { Observable } from 'rxjs';
import { take } from 'rxjs/operators';
import { SharedGlobalConfig } from 'kibana/server';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { KIBANA_STATS_TYPE, KIBANA_USAGE_TYPE } from '../../../common/constants';
import { KIBANA_STATS_TYPE } from '../../../common/constants';
import { getSavedObjectsCounts } from './get_saved_object_counts';
export function getKibanaUsageCollector(
@ -29,7 +29,7 @@ export function getKibanaUsageCollector(
legacyConfig$: Observable<SharedGlobalConfig>
) {
return usageCollection.makeUsageCollector({
type: KIBANA_USAGE_TYPE,
type: 'kibana',
isReady: () => true,
async fetch(callCluster) {
const {

View file

@ -19,7 +19,6 @@
import { IUiSettingsClient } from 'kibana/server';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { KIBANA_STACK_MANAGEMENT_STATS_TYPE } from '../../../common/constants';
export type UsageStats = Record<string, any>;
@ -47,7 +46,7 @@ export function registerManagementUsageCollector(
getUiSettingsClient: () => IUiSettingsClient | undefined
) {
const collector = usageCollection.makeUsageCollector({
type: KIBANA_STACK_MANAGEMENT_STATS_TYPE,
type: 'stack_management',
isReady: () => typeof getUiSettingsClient() !== 'undefined',
fetch: createCollectorFetch(getUiSettingsClient),
});

View file

@ -23,7 +23,6 @@ import {
SavedObjectsServiceSetup,
} from 'kibana/server';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { UI_METRIC_USAGE_TYPE } from '../../../common/constants';
import { findAll } from '../find_all';
interface UIMetricsSavedObjects extends SavedObjectAttributes {
@ -49,7 +48,7 @@ export function registerUiMetricUsageCollector(
});
const collector = usageCollection.makeUsageCollector({
type: UI_METRIC_USAGE_TYPE,
type: 'ui_metric',
fetch: async () => {
const savedObjectsClient = getSavedObjectsClient();
if (typeof savedObjectsClient === 'undefined') {

View file

@ -56,11 +56,6 @@ export const PATH_TO_ADVANCED_SETTINGS = 'management/kibana/settings';
*/
export const PRIVACY_STATEMENT_URL = `https://www.elastic.co/legal/privacy-statement`;
/**
* The type name used to publish telemetry plugin stats.
*/
export const TELEMETRY_STATS_TYPE = 'telemetry';
/**
* The endpoint version when hitting the remote telemetry service
*/

View file

@ -0,0 +1,17 @@
{
"properties": {
"csp": {
"properties": {
"strict": {
"type": "boolean"
},
"warnLegacyBrowsers": {
"type": "boolean"
},
"rulesChangedFromDefault": {
"type": "boolean"
}
}
}
}
}

View file

@ -0,0 +1,59 @@
{
"properties": {
"kql": {
"properties": {
"optInCount": {
"type": "long"
},
"optOutCount": {
"type": "long"
},
"defaultQueryLanguage": {
"type": "keyword"
}
}
},
"sample-data": {
"properties": {
"installed": {
"type": "keyword"
},
"last_install_date": {
"type": "date"
},
"last_install_set": {
"type": "keyword"
},
"last_uninstall_date": {
"type": "date"
},
"last_uninstall_set": {
"type": "keyword"
},
"uninstalled": {
"type": "keyword"
}
}
},
"telemetry": {
"properties": {
"opt_in_status": {
"type": "boolean"
},
"usage_fetcher": {
"type": "keyword"
},
"last_reported": {
"type": "long"
}
}
},
"tsvb-validation": {
"properties": {
"failed_validations": {
"type": "long"
}
}
}
}
}

View file

@ -20,7 +20,6 @@
import { Observable } from 'rxjs';
import { take } from 'rxjs/operators';
import { ISavedObjectsRepository, SavedObjectsClient } from '../../../../../core/server';
import { TELEMETRY_STATS_TYPE } from '../../../common/constants';
import { getTelemetrySavedObject, TelemetrySavedObject } from '../../telemetry_repository';
import { getTelemetryOptIn, getTelemetrySendUsageFrom } from '../../../common/telemetry_config';
import { UsageCollectionSetup } from '../../../../usage_collection/server';
@ -81,10 +80,15 @@ export function registerTelemetryPluginUsageCollector(
usageCollection: UsageCollectionSetup,
options: TelemetryPluginUsageCollectorOptions
) {
const collector = usageCollection.makeUsageCollector({
type: TELEMETRY_STATS_TYPE,
const collector = usageCollection.makeUsageCollector<TelemetryUsageStats>({
type: 'telemetry',
isReady: () => typeof options.getSavedObjectsClient() !== 'undefined',
fetch: createCollectorFetch(options),
schema: {
opt_in_status: { type: 'boolean' },
usage_fetcher: { type: 'keyword' },
last_reported: { type: 'long' },
},
});
usageCollection.registerCollector(collector);

View file

@ -8,7 +8,7 @@ To integrate with the telemetry services for usage collection of your feature, t
## Creating and Registering Usage Collector
All you need to provide is a `type` for organizing your fields, and a `fetch` method for returning your usage data. Then you need to make the Telemetry service aware of the collector by registering it.
All you need to provide is a `type` for organizing your fields, `schema` field to define the expected types of usage fields reported, and a `fetch` method for returning your usage data. Then you need to make the Telemetry service aware of the collector by registering it.
### New Platform
@ -45,6 +45,12 @@ All you need to provide is a `type` for organizing your fields, and a `fetch` me
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { APICluster } from 'kibana/server';
interface Usage {
my_objects: {
total: number,
},
}
export function registerMyPluginUsageCollector(usageCollection?: UsageCollectionSetup): void {
// usageCollection is an optional dependency, so make sure to return if it is not registered.
if (!usageCollection) {
@ -52,8 +58,13 @@ All you need to provide is a `type` for organizing your fields, and a `fetch` me
}
// create usage collector
const myCollector = usageCollection.makeUsageCollector({
const myCollector = usageCollection.makeUsageCollector<Usage>({
type: MY_USAGE_TYPE,
schema: {
my_objects: {
total: 'long',
},
},
fetch: async (callCluster: APICluster) => {
// query ES and get some data
@ -98,10 +109,8 @@ class Plugin {
```ts
// server/collectors/register.ts
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { ISavedObjectsRepository } from 'kibana/server';
export function registerMyPluginUsageCollector(
getSavedObjectsRepository: () => ISavedObjectsRepository | undefined,
usageCollection?: UsageCollectionSetup
): void {
// usageCollection is an optional dependency, so make sure to return if it is not registered.
@ -110,22 +119,52 @@ export function registerMyPluginUsageCollector(
}
// create usage collector
const myCollector = usageCollection.makeUsageCollector({
type: MY_USAGE_TYPE,
isReady: () => typeof getSavedObjectsRepository() !== 'undefined',
fetch: async () => {
const savedObjectsRepository = getSavedObjectsRepository()!;
// get something from the savedObjects
return { my_objects };
},
});
const myCollector = usageCollection.makeUsageCollector<Usage>(...)
// register usage collector
usageCollection.registerCollector(myCollector);
}
```
## Schema Field
The `schema` field is a proscribed data model assists with detecting changes in usage collector payloads. To define the collector schema add a schema field that specifies every possible field reported when registering the collector. Whenever the `schema` field is set or changed please run `node scripts/telemetry_check.js --fix` to update the stored schema json files.
### Allowed Schema Types
The `AllowedSchemaTypes` is the list of allowed schema types for the usage fields getting reported:
```
'keyword', 'text', 'number', 'boolean', 'long', 'date', 'float'
```
### Example
```ts
export const myCollector = makeUsageCollector<Usage>({
type: 'my_working_collector',
isReady: () => true,
fetch() {
return {
my_greeting: 'hello',
some_obj: {
total: 123,
},
};
},
schema: {
my_greeting: {
type: 'keyword',
},
some_obj: {
total: {
type: 'number',
},
},
},
});
```
## Update the telemetry payload and telemetry cluster field mappings
There is a module in the telemetry service that creates the payload of data that gets sent up to the telemetry cluster.

View file

@ -21,9 +21,33 @@ import { Logger, APICaller } from 'kibana/server';
export type CollectorFormatForBulkUpload<T, U> = (result: T) => { type: string; payload: U };
export type AllowedSchemaTypes =
| 'keyword'
| 'text'
| 'number'
| 'boolean'
| 'long'
| 'date'
| 'float';
export interface SchemaField {
type: string;
}
type Purify<T extends string> = { [P in T]: T }[T];
export type MakeSchemaFrom<Base> = {
[Key in Purify<Extract<keyof Base, string>>]: Base[Key] extends Array<infer U>
? { type: AllowedSchemaTypes }
: Base[Key] extends object
? MakeSchemaFrom<Base[Key]>
: { type: AllowedSchemaTypes };
};
export interface CollectorOptions<T = unknown, U = T> {
type: string;
init?: Function;
schema?: MakeSchemaFrom<T>;
fetch: (callCluster: APICaller) => Promise<T> | T;
/*
* A hook for allowing the fetched data payload to be organized into a typed

View file

@ -42,7 +42,7 @@ export class CollectorSet {
public makeStatsCollector = <T, U>(options: CollectorOptions<T, U>) => {
return new Collector(this.logger, options);
};
public makeUsageCollector = <T, U>(options: CollectorOptions<T, U>) => {
public makeUsageCollector = <T, U = T>(options: CollectorOptions<T, U>) => {
return new UsageCollector(this.logger, options);
};

View file

@ -18,5 +18,11 @@
*/
export { CollectorSet } from './collector_set';
export { Collector } from './collector';
export {
Collector,
AllowedSchemaTypes,
SchemaField,
MakeSchemaFrom,
CollectorOptions,
} from './collector';
export { UsageCollector } from './usage_collector';

View file

@ -20,6 +20,13 @@
import { PluginInitializerContext } from 'kibana/server';
import { UsageCollectionPlugin } from './plugin';
export {
AllowedSchemaTypes,
MakeSchemaFrom,
SchemaField,
CollectorOptions,
Collector,
} from './collector';
export { UsageCollectionSetup } from './plugin';
export { config } from './config';
export const plugin = (initializerContext: PluginInitializerContext) =>

View file

@ -24,6 +24,9 @@ import { tsvbTelemetrySavedObjectType } from '../saved_objects';
export interface ValidationTelemetryServiceSetup {
logFailedValidation: () => void;
}
export interface Usage {
failed_validations: number;
}
export class ValidationTelemetryService implements Plugin<ValidationTelemetryServiceSetup> {
private kibanaIndex: string = '';
@ -43,7 +46,7 @@ export class ValidationTelemetryService implements Plugin<ValidationTelemetrySer
});
if (usageCollection) {
usageCollection.registerCollector(
usageCollection.makeUsageCollector({
usageCollection.makeUsageCollector<Usage>({
type: 'tsvb-validation',
isReady: () => this.kibanaIndex !== '',
fetch: async (callCluster: APICaller) => {
@ -63,6 +66,9 @@ export class ValidationTelemetryService implements Plugin<ValidationTelemetrySer
};
}
},
schema: {
failed_validations: { type: 'long' },
},
})
);
}

View file

@ -149,6 +149,12 @@ module.exports = function (grunt) {
args: ['scripts/i18n_check', '--ignore-missing'],
}),
telemetryCheck: scriptWithGithubChecks({
title: 'Telemetry Schema check',
cmd: NODE,
args: ['scripts/telemetry_check'],
}),
// used by the test:quick task
// runs all node.js/server mocha tests
mocha: scriptWithGithubChecks({

View file

@ -27,6 +27,7 @@ module.exports = function (grunt) {
'run:checkDocApiChanges',
'run:typeCheck',
'run:i18nCheck',
'run:telemetryCheck',
'run:checkFileCasing',
'run:checkLockfileSymlinks',
'run:licenses',

14
x-pack/.telemetryrc.json Normal file
View file

@ -0,0 +1,14 @@
{
"output": "plugins/telemetry_collection_xpack/schema/xpack_plugins.json",
"root": "plugins/",
"exclude": [
"plugins/actions/server/usage/actions_usage_collector.ts",
"plugins/alerts/server/usage/alerts_usage_collector.ts",
"plugins/apm/server/lib/apm_telemetry/index.ts",
"plugins/canvas/server/collectors/collector.ts",
"plugins/infra/server/usage/usage_collector.ts",
"plugins/lens/server/usage/collectors.ts",
"plugins/reporting/server/usage/reporting_usage_collector.ts",
"plugins/maps/server/maps_telemetry/collectors/register.ts"
]
}

View file

@ -13,10 +13,10 @@ export function createActionsUsageCollector(
usageCollection: UsageCollectionSetup,
taskManager: TaskManagerStartContract
) {
return usageCollection.makeUsageCollector({
return usageCollection.makeUsageCollector<ActionsUsage>({
type: 'actions',
isReady: () => true,
fetch: async (): Promise<ActionsUsage> => {
fetch: async () => {
try {
const doc = await getLatestTaskState(await taskManager);
// get the accumulated state from the recurring task

View file

@ -13,10 +13,10 @@ export function createAlertsUsageCollector(
usageCollection: UsageCollectionSetup,
taskManager: TaskManagerStartContract
) {
return usageCollection.makeUsageCollector({
return usageCollection.makeUsageCollector<AlertsUsage>({
type: 'alerts',
isReady: () => true,
fetch: async (): Promise<AlertsUsage> => {
fetch: async () => {
try {
const doc = await getLatestTaskState(await taskManager);
// get the accumulated state from the recurring task

View file

@ -20,7 +20,6 @@ export const LOCALSTORAGE_PREFIX = `kibana.canvas`;
export const LOCALSTORAGE_CLIPBOARD = `${LOCALSTORAGE_PREFIX}.clipboard`;
export const SESSIONSTORAGE_LASTPATH = 'lastPath:canvas';
export const FETCH_TIMEOUT = 30000; // 30 seconds
export const CANVAS_USAGE_TYPE = 'canvas';
export const DEFAULT_WORKPAD_CSS = '.canvasPage {\n\n}';
export const DEFAULT_ELEMENT_CSS = '.canvasRenderEl{\n\n}';
export const VALID_IMAGE_TYPES = ['gif', 'jpeg', 'png', 'svg+xml'];

View file

@ -6,7 +6,6 @@
import { CallCluster } from 'src/legacy/core_plugins/elasticsearch';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { CANVAS_USAGE_TYPE } from '../../common/lib/constants';
import { TelemetryCollector } from '../../types';
import { workpadCollector } from './workpad_collector';
@ -31,20 +30,16 @@ export function registerCanvasUsageCollector(
}
const canvasCollector = usageCollection.makeUsageCollector({
type: CANVAS_USAGE_TYPE,
type: 'canvas',
isReady: () => true,
fetch: async (callCluster: CallCluster) => {
const collectorResults = await Promise.all(
collectors.map((collector) => collector(kibanaIndex, callCluster))
);
return collectorResults.reduce(
(reduction, usage) => {
return { ...reduction, ...usage };
},
{}
);
return collectorResults.reduce((reduction, usage) => {
return { ...reduction, ...usage };
}, {});
},
});

View file

@ -4,5 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const KIBANA_CLOUD_STATS_TYPE = 'cloud';
export const ELASTIC_SUPPORT_LINK = 'https://support.elastic.co/';

View file

@ -5,17 +5,23 @@
*/
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { KIBANA_CLOUD_STATS_TYPE } from '../../common/constants';
interface Config {
isCloudEnabled: boolean;
}
interface CloudUsage {
isCloudEnabled: boolean;
}
export function createCloudUsageCollector(usageCollection: UsageCollectionSetup, config: Config) {
const { isCloudEnabled } = config;
return usageCollection.makeUsageCollector({
type: KIBANA_CLOUD_STATS_TYPE,
return usageCollection.makeUsageCollector<CloudUsage>({
type: 'cloud',
isReady: () => true,
schema: {
isCloudEnabled: { type: 'boolean' },
},
fetch: () => {
return {
isCloudEnabled,

View file

@ -5,15 +5,23 @@
*/
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { getTelemetry, initTelemetry } from './telemetry';
const TELEMETRY_TYPE = 'fileUploadTelemetry';
import { getTelemetry, initTelemetry, Telemetry } from './telemetry';
export function registerFileUploadUsageCollector(usageCollection: UsageCollectionSetup): void {
const fileUploadUsageCollector = usageCollection.makeUsageCollector({
type: TELEMETRY_TYPE,
const fileUploadUsageCollector = usageCollection.makeUsageCollector<Telemetry>({
type: 'fileUploadTelemetry',
isReady: () => true,
fetch: async () => (await getTelemetry()) || initTelemetry(),
fetch: async () => {
const fileUploadUsage = await getTelemetry();
if (!fileUploadUsage) {
return initTelemetry();
}
return fileUploadUsage;
},
schema: {
filesUploadedTotalCount: { type: 'long' },
},
});
usageCollection.registerCollector(fileUploadUsageCollector);

View file

@ -7,8 +7,6 @@
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { InventoryItemType } from '../../common/inventory_models/types';
const KIBANA_REPORTING_TYPE = 'infraops';
interface InfraopsSum {
infraopsHosts: number;
infraopsDocker: number;
@ -24,7 +22,7 @@ export class UsageCollector {
public static getUsageCollector(usageCollection: UsageCollectionSetup) {
return usageCollection.makeUsageCollector({
type: KIBANA_REPORTING_TYPE,
type: 'infraops',
isReady: () => true,
fetch: async () => {
return this.getReport();

View file

@ -7,12 +7,10 @@
import { CoreSetup } from 'kibana/server';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { getTelemetry, initTelemetry } from './telemetry';
import { getTelemetry, initTelemetry, Telemetry } from './telemetry';
import { mlTelemetryMappingsType } from './mappings';
import { setInternalRepository } from './internal_repository';
const TELEMETRY_TYPE = 'mlTelemetry';
export function initMlTelemetry(coreSetup: CoreSetup, usageCollection: UsageCollectionSetup) {
coreSetup.savedObjects.registerType(mlTelemetryMappingsType);
registerMlUsageCollector(usageCollection);
@ -22,10 +20,22 @@ export function initMlTelemetry(coreSetup: CoreSetup, usageCollection: UsageColl
}
function registerMlUsageCollector(usageCollection: UsageCollectionSetup): void {
const mlUsageCollector = usageCollection.makeUsageCollector({
type: TELEMETRY_TYPE,
const mlUsageCollector = usageCollection.makeUsageCollector<Telemetry>({
type: 'mlTelemetry',
isReady: () => true,
fetch: async () => (await getTelemetry()) || initTelemetry(),
schema: {
file_data_visualizer: {
index_creation_count: { type: 'long' },
},
},
fetch: async () => {
const mlUsage = await getTelemetry();
if (!mlUsage) {
return initTelemetry();
}
return mlUsage;
},
});
usageCollection.registerCollector(mlUsageCollector);

View file

@ -11,7 +11,7 @@ import { getInternalRepository } from './internal_repository';
export const TELEMETRY_DOC_ID = 'ml-telemetry';
interface Telemetry {
export interface Telemetry {
file_data_visualizer: {
index_creation_count: number;
};

View file

@ -54,12 +54,6 @@ export const KBN_SCREENSHOT_HEADER_BLACKLIST_STARTS_WITH_PATTERN = ['proxy-'];
export const UI_SETTINGS_CUSTOM_PDF_LOGO = 'xpackReporting:customPdfLogo';
/**
* The type name used within the Monitoring index to publish reporting stats.
* @type {string}
*/
export const KIBANA_REPORTING_TYPE = 'reporting';
export const PDF_JOB_TYPE = 'printable_pdf';
export const PNG_JOB_TYPE = 'PNG';
export const CSV_JOB_TYPE = 'csv';

View file

@ -8,16 +8,22 @@ import { first, map } from 'rxjs/operators';
import { CallCluster } from 'src/legacy/core_plugins/elasticsearch';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { ReportingCore } from '../';
import { KIBANA_REPORTING_TYPE } from '../../common/constants';
import { ExportTypesRegistry } from '../lib/export_types_registry';
import { ReportingSetupDeps } from '../types';
import { GetLicense } from './';
import { getReportingUsage } from './get_reporting_usage';
import { RangeStats } from './types';
import { ReportingUsageType } from './types';
// places the reporting data as kibana stats
const METATYPE = 'kibana_stats';
interface XpackBulkUpload {
usage: {
xpack: {
reporting: ReportingUsageType;
};
};
}
/*
* @return {Object} kibana usage stats type collection object
*/
@ -28,20 +34,19 @@ export function getReportingUsageCollector(
exportTypesRegistry: ExportTypesRegistry,
isReady: () => Promise<boolean>
) {
return usageCollection.makeUsageCollector({
type: KIBANA_REPORTING_TYPE,
return usageCollection.makeUsageCollector<ReportingUsageType, XpackBulkUpload>({
type: 'reporting',
fetch: (callCluster: CallCluster) => {
const config = reporting.getConfig();
return getReportingUsage(config, getLicense, callCluster, exportTypesRegistry);
},
isReady,
/*
* Format the response data into a model for internal upload
* 1. Make this data part of the "kibana_stats" type
* 2. Organize the payload in the usage.xpack.reporting namespace of the data payload
*/
formatForBulkUpload: (result: RangeStats) => {
formatForBulkUpload: (result: ReportingUsageType) => {
return {
type: METATYPE,
payload: {

View file

@ -12,8 +12,6 @@ interface IdToFlagMap {
[key: string]: boolean;
}
const ROLLUP_USAGE_TYPE = 'rollups';
// elasticsearch index.max_result_window default value
const ES_MAX_RESULT_WINDOW_DEFAULT_VALUE = 1000;
@ -174,13 +172,42 @@ async function fetchRollupVisualizations(
};
}
interface Usage {
index_patterns: {
total: number;
};
saved_searches: {
total: number;
};
visualizations: {
total: number;
saved_searches: {
total: number;
};
};
}
export function registerRollupUsageCollector(
usageCollection: UsageCollectionSetup,
kibanaIndex: string
): void {
const collector = usageCollection.makeUsageCollector({
type: ROLLUP_USAGE_TYPE,
const collector = usageCollection.makeUsageCollector<Usage>({
type: 'rollups',
isReady: () => true,
schema: {
index_patterns: {
total: { type: 'long' },
},
saved_searches: {
total: { type: 'long' },
},
visualizations: {
saved_searches: {
total: { type: 'long' },
},
total: { type: 'long' },
},
},
fetch: async (callCluster: CallCluster) => {
const rollupIndexPatterns = await fetchRollupIndexPatterns(kibanaIndex, callCluster);
const rollupIndexPatternToFlagMap = createIdToFlagMap(rollupIndexPatterns);

View file

@ -16,12 +16,6 @@ export const SPACE_SEARCH_COUNT_THRESHOLD = 8;
*/
export const MAX_SPACE_INITIALS = 2;
/**
* The type name used within the Monitoring index to publish spaces stats.
* @type {string}
*/
export const KIBANA_SPACES_STATS_TYPE = 'spaces';
/**
* The path to enter a space.
*/

View file

@ -9,7 +9,6 @@ import { take } from 'rxjs/operators';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { Observable } from 'rxjs';
import { KIBANA_STATS_TYPE_MONITORING } from '../../../monitoring/common/constants';
import { KIBANA_SPACES_STATS_TYPE } from '../../common/constants';
import { PluginsSetup } from '../plugin';
type CallCluster = <T = unknown>(
@ -118,8 +117,25 @@ export interface UsageStats {
enabled: boolean;
count?: number;
usesFeatureControls?: boolean;
disabledFeatures?: {
[featureId: string]: number;
disabledFeatures: {
indexPatterns?: number;
discover?: number;
canvas?: number;
maps?: number;
siem?: number;
monitoring?: number;
graph?: number;
uptime?: number;
savedObjectsManagement?: number;
timelion?: number;
dev_tools?: number;
advancedSettings?: number;
infrastructure?: number;
visualize?: number;
logs?: number;
dashboard?: number;
ml?: number;
apm?: number;
};
}
@ -129,6 +145,11 @@ interface CollectorDeps {
licensing: PluginsSetup['licensing'];
}
interface BulkUpload {
usage: {
spaces: UsageStats;
};
}
/*
* @param {Object} server
* @return {Object} kibana usage stats type collection object
@ -137,9 +158,35 @@ export function getSpacesUsageCollector(
usageCollection: UsageCollectionSetup,
deps: CollectorDeps
) {
return usageCollection.makeUsageCollector({
type: KIBANA_SPACES_STATS_TYPE,
return usageCollection.makeUsageCollector<UsageStats, BulkUpload>({
type: 'spaces',
isReady: () => true,
schema: {
usesFeatureControls: { type: 'boolean' },
disabledFeatures: {
indexPatterns: { type: 'long' },
discover: { type: 'long' },
canvas: { type: 'long' },
maps: { type: 'long' },
siem: { type: 'long' },
monitoring: { type: 'long' },
graph: { type: 'long' },
uptime: { type: 'long' },
savedObjectsManagement: { type: 'long' },
timelion: { type: 'long' },
dev_tools: { type: 'long' },
advancedSettings: { type: 'long' },
infrastructure: { type: 'long' },
visualize: { type: 'long' },
logs: { type: 'long' },
dashboard: { type: 'long' },
ml: { type: 'long' },
apm: { type: 'long' },
},
available: { type: 'boolean' },
enabled: { type: 'boolean' },
count: { type: 'long' },
},
fetch: async (callCluster: CallCluster) => {
const license = await deps.licensing.license$.pipe(take(1)).toPromise();
const available = license.isAvailable; // some form of spaces is available for all valid licenses

View file

@ -0,0 +1,247 @@
{
"properties": {
"cloud": {
"properties": {
"isCloudEnabled": {
"type": "boolean"
}
}
},
"fileUploadTelemetry": {
"properties": {
"filesUploadedTotalCount": {
"type": "long"
}
}
},
"mlTelemetry": {
"properties": {
"file_data_visualizer": {
"properties": {
"index_creation_count": {
"type": "long"
}
}
}
}
},
"rollups": {
"properties": {
"index_patterns": {
"properties": {
"total": {
"type": "long"
}
}
},
"saved_searches": {
"properties": {
"total": {
"type": "long"
}
}
},
"visualizations": {
"properties": {
"saved_searches": {
"properties": {
"total": {
"type": "long"
}
}
},
"total": {
"type": "long"
}
}
}
}
},
"spaces": {
"properties": {
"usesFeatureControls": {
"type": "boolean"
},
"disabledFeatures": {
"properties": {
"indexPatterns": {
"type": "long"
},
"discover": {
"type": "long"
},
"canvas": {
"type": "long"
},
"maps": {
"type": "long"
},
"siem": {
"type": "long"
},
"monitoring": {
"type": "long"
},
"graph": {
"type": "long"
},
"uptime": {
"type": "long"
},
"savedObjectsManagement": {
"type": "long"
},
"timelion": {
"type": "long"
},
"dev_tools": {
"type": "long"
},
"advancedSettings": {
"type": "long"
},
"infrastructure": {
"type": "long"
},
"visualize": {
"type": "long"
},
"logs": {
"type": "long"
},
"dashboard": {
"type": "long"
},
"ml": {
"type": "long"
},
"apm": {
"type": "long"
}
}
},
"available": {
"type": "boolean"
},
"enabled": {
"type": "boolean"
},
"count": {
"type": "long"
}
}
},
"upgrade-assistant-telemetry": {
"properties": {
"features": {
"properties": {
"deprecation_logging": {
"properties": {
"enabled": {
"type": "boolean"
}
}
}
}
},
"ui_open": {
"properties": {
"cluster": {
"type": "long"
},
"indices": {
"type": "long"
},
"overview": {
"type": "long"
}
}
},
"ui_reindex": {
"properties": {
"close": {
"type": "long"
},
"open": {
"type": "long"
},
"start": {
"type": "long"
},
"stop": {
"type": "long"
}
}
}
}
},
"uptime": {
"properties": {
"last_24_hours": {
"properties": {
"hits": {
"properties": {
"autoRefreshEnabled": {
"type": "boolean"
},
"autorefreshInterval": {
"type": "long"
},
"dateRangeEnd": {
"type": "date"
},
"dateRangeStart": {
"type": "date"
},
"monitor_frequency": {
"type": "long"
},
"monitor_name_stats": {
"properties": {
"avg_length": {
"type": "float"
},
"max_length": {
"type": "long"
},
"min_length": {
"type": "long"
}
}
},
"monitor_page": {
"type": "long"
},
"no_of_unique_monitors": {
"type": "long"
},
"no_of_unique_observer_locations": {
"type": "long"
},
"observer_location_name_stats": {
"properties": {
"avg_length": {
"type": "float"
},
"max_length": {
"type": "long"
},
"min_length": {
"type": "long"
}
}
},
"overview_page": {
"type": "long"
},
"settings_page": {
"type": "long"
}
}
}
}
}
}
}
}
}

View file

@ -120,9 +120,29 @@ export function registerUpgradeAssistantUsageCollector({
usageCollection,
savedObjects,
}: Dependencies) {
const upgradeAssistantUsageCollector = usageCollection.makeUsageCollector({
type: UPGRADE_ASSISTANT_TYPE,
const upgradeAssistantUsageCollector = usageCollection.makeUsageCollector<
UpgradeAssistantTelemetry
>({
type: 'upgrade-assistant-telemetry',
isReady: () => true,
schema: {
features: {
deprecation_logging: {
enabled: { type: 'boolean' },
},
},
ui_open: {
cluster: { type: 'long' },
indices: { type: 'long' },
overview: { type: 'long' },
},
ui_reindex: {
close: { type: 'long' },
open: { type: 'long' },
start: { type: 'long' },
stop: { type: 'long' },
},
},
fetch: async () => fetchUpgradeAssistantMetrics(elasticsearch, savedObjects),
});
usageCollection.registerCollector(upgradeAssistantUsageCollector);

View file

@ -7,7 +7,7 @@
import moment from 'moment';
import { ISavedObjectsRepository, SavedObjectsClientContract } from 'kibana/server';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { PageViewParams, UptimeTelemetry } from './types';
import { PageViewParams, UptimeTelemetry, Usage } from './types';
import { APICaller } from '../framework';
import { savedObjectsAdapter } from '../../saved_objects';
@ -39,8 +39,36 @@ export class KibanaTelemetryAdapter {
usageCollector: UsageCollectionSetup,
getSavedObjectsClient: () => ISavedObjectsRepository | undefined
) {
return usageCollector.makeUsageCollector({
return usageCollector.makeUsageCollector<Usage>({
type: 'uptime',
schema: {
last_24_hours: {
hits: {
autoRefreshEnabled: {
type: 'boolean',
},
autorefreshInterval: { type: 'long' },
dateRangeEnd: { type: 'date' },
dateRangeStart: { type: 'date' },
monitor_frequency: { type: 'long' },
monitor_name_stats: {
avg_length: { type: 'float' },
max_length: { type: 'long' },
min_length: { type: 'long' },
},
monitor_page: { type: 'long' },
no_of_unique_monitors: { type: 'long' },
no_of_unique_observer_locations: { type: 'long' },
observer_location_name_stats: {
avg_length: { type: 'float' },
max_length: { type: 'long' },
min_length: { type: 'long' },
},
overview_page: { type: 'long' },
settings_page: { type: 'long' },
},
},
},
fetch: async (callCluster: APICaller) => {
const savedObjectsClient = getSavedObjectsClient()!;
if (savedObjectsClient) {

View file

@ -19,6 +19,12 @@ export interface Stats {
avg_length: number;
}
export interface Usage {
last_24_hours: {
hits: UptimeTelemetry;
};
}
export interface UptimeTelemetry {
overview_page: number;
monitor_page: number;