From 55bd3b3bb80e5ff825e62ede0a882e568ef10ce9 Mon Sep 17 00:00:00 2001
From: Scotty Bollinger
Date: Wed, 9 Sep 2020 10:03:20 -0500
Subject: [PATCH 01/25] Upgrade to Kea 2.2 (#77047)
This PR updates Kea from the last RC to the stable release 2.2.
---
x-pack/package.json | 2 +-
yarn.lock | 8 ++++----
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/x-pack/package.json b/x-pack/package.json
index 899eca1095923..3a074ba1f1d7d 100644
--- a/x-pack/package.json
+++ b/x-pack/package.json
@@ -195,7 +195,7 @@
"jsdom": "13.1.0",
"jsondiffpatch": "0.4.1",
"jsts": "^1.6.2",
- "kea": "2.2.0-rc.4",
+ "kea": "^2.2.0",
"loader-utils": "^1.2.3",
"lz-string": "^1.4.4",
"madge": "3.4.4",
diff --git a/yarn.lock b/yarn.lock
index ddb83b3cf1532..29f99c25b7730 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -18194,10 +18194,10 @@ kdbush@^3.0.0:
resolved "https://registry.yarnpkg.com/kdbush/-/kdbush-3.0.0.tgz#f8484794d47004cc2d85ed3a79353dbe0abc2bf0"
integrity sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==
-kea@2.2.0-rc.4:
- version "2.2.0-rc.4"
- resolved "https://registry.yarnpkg.com/kea/-/kea-2.2.0-rc.4.tgz#cc0376950530a6751f73387c4b25a39efa1faa77"
- integrity sha512-pYuwaCiJkBvHZShi8kqhk8dC4DjeELdK51Lw7Pn0tNdJgZJDF6COhsUiF/yrh9d7woNYDxKfuxH+QWZFfo8PkA==
+kea@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/kea/-/kea-2.2.0.tgz#1ba4a174a53880cca8002a67cf62b19b30d09702"
+ integrity sha512-IzgTC6SC89wTLfiBMPlduG4r4YanxONYK4werz8RMZxPvcYw4XEEK8xQJguwVYtLCEGm4x5YiLCubGqGfRcbEw==
kew@~0.1.7:
version "0.1.7"
From 538bd4be511e8c8a400b0290b8e899361cdbe8d8 Mon Sep 17 00:00:00 2001
From: Devon Thomson
Date: Wed, 9 Sep 2020 11:30:37 -0400
Subject: [PATCH 02/25] fixed typo
---
.../saved_objects/public/save_modal/saved_object_save_modal.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx b/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx
index 3b9efbee22ba6..9cdef8b9392bb 100644
--- a/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx
+++ b/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx
@@ -294,7 +294,7 @@ export class SavedObjectSaveModal extends React.Component
id="savedObjects.saveModal.duplicateTitleDescription"
defaultMessage="Saving '{title}' creates a duplicate title."
values={{
- title: this.props.title,
+ title: this.state.title,
}}
/>
From c7dac8000cef75bd6420be4e755dfa7dae122f4a Mon Sep 17 00:00:00 2001
From: David Roberts
Date: Wed, 9 Sep 2020 16:31:09 +0100
Subject: [PATCH 03/25] [ML] Account for "properties" layer in
find_file_structure mappings (#77035)
This is the UI side companion for elastic/elasticsearch#62158
Previously the "mappings" field of the response from the
find_file_structure endpoint was not a drop-in for the
mappings format of the create index endpoint - the
"properties" layer was missing. The reason for omitting
it initially was that the assumption was that the
find_file_structure endpoint would only ever return very
simple mappings without any nested objects. However,
this will not be true in the future, as we will improve
mappings detection for complex JSON objects. As a first
step it makes sense to move the returned mappings closer
to the standard format.
---
x-pack/plugins/ml/common/types/file_datavisualizer.ts | 7 ++++++-
.../ml/server/models/file_data_visualizer/import_data.ts | 2 +-
2 files changed, 7 insertions(+), 2 deletions(-)
diff --git a/x-pack/plugins/ml/common/types/file_datavisualizer.ts b/x-pack/plugins/ml/common/types/file_datavisualizer.ts
index c997a4e24f868..a8b775c8d5f60 100644
--- a/x-pack/plugins/ml/common/types/file_datavisualizer.ts
+++ b/x-pack/plugins/ml/common/types/file_datavisualizer.ts
@@ -84,7 +84,12 @@ export interface Settings {
}
export interface Mappings {
- [key: string]: any;
+ _meta?: {
+ created_by: string;
+ };
+ properties: {
+ [key: string]: any;
+ };
}
export interface IngestPipelineWrapper {
diff --git a/x-pack/plugins/ml/server/models/file_data_visualizer/import_data.ts b/x-pack/plugins/ml/server/models/file_data_visualizer/import_data.ts
index 6108454c08aa7..26dba7c2f00c1 100644
--- a/x-pack/plugins/ml/server/models/file_data_visualizer/import_data.ts
+++ b/x-pack/plugins/ml/server/models/file_data_visualizer/import_data.ts
@@ -94,7 +94,7 @@ export function importDataProvider({ asCurrentUser }: IScopedClusterClient) {
_meta: {
created_by: INDEX_META_DATA_CREATED_BY,
},
- properties: mappings,
+ properties: mappings.properties,
},
};
From 7955a02437989385152c9c201aafb6a52c10aedf Mon Sep 17 00:00:00 2001
From: Mikhail Shustov
Date: Wed, 9 Sep 2020 18:57:32 +0300
Subject: [PATCH 04/25] legacy utils cleanup (#76608)
* move prompt to cli_keystore
* move binder to cli
* remove unused path_contains
Co-authored-by: Elastic Machine
---
src/{legacy/utils => cli/cluster}/binder.ts | 0
.../utils => cli/cluster}/binder_for.ts | 0
src/cli/cluster/worker.ts | 2 +-
src/cli_keystore/add.js | 2 +-
src/cli_keystore/add.test.js | 2 +-
src/cli_keystore/create.js | 2 +-
src/cli_keystore/create.test.js | 2 +-
.../server => cli_keystore}/utils/index.js | 0
.../server => cli_keystore}/utils/prompt.js | 0
.../utils/prompt.test.js | 0
src/legacy/utils/index.js | 2 --
src/legacy/utils/path_contains.js | 24 -------------------
12 files changed, 5 insertions(+), 31 deletions(-)
rename src/{legacy/utils => cli/cluster}/binder.ts (100%)
rename src/{legacy/utils => cli/cluster}/binder_for.ts (100%)
rename src/{legacy/server => cli_keystore}/utils/index.js (100%)
rename src/{legacy/server => cli_keystore}/utils/prompt.js (100%)
rename src/{legacy/server => cli_keystore}/utils/prompt.test.js (100%)
delete mode 100644 src/legacy/utils/path_contains.js
diff --git a/src/legacy/utils/binder.ts b/src/cli/cluster/binder.ts
similarity index 100%
rename from src/legacy/utils/binder.ts
rename to src/cli/cluster/binder.ts
diff --git a/src/legacy/utils/binder_for.ts b/src/cli/cluster/binder_for.ts
similarity index 100%
rename from src/legacy/utils/binder_for.ts
rename to src/cli/cluster/binder_for.ts
diff --git a/src/cli/cluster/worker.ts b/src/cli/cluster/worker.ts
index 097a549187429..c8a8a067d30bf 100644
--- a/src/cli/cluster/worker.ts
+++ b/src/cli/cluster/worker.ts
@@ -21,7 +21,7 @@ import _ from 'lodash';
import cluster from 'cluster';
import { EventEmitter } from 'events';
-import { BinderFor } from '../../legacy/utils/binder_for';
+import { BinderFor } from './binder_for';
import { fromRoot } from '../../core/server/utils';
const cliPath = fromRoot('src/cli');
diff --git a/src/cli_keystore/add.js b/src/cli_keystore/add.js
index 462259ec942dd..232392f34c63b 100644
--- a/src/cli_keystore/add.js
+++ b/src/cli_keystore/add.js
@@ -18,7 +18,7 @@
*/
import { Logger } from '../cli_plugin/lib/logger';
-import { confirm, question } from '../legacy/server/utils';
+import { confirm, question } from './utils';
import { createPromiseFromStreams, createConcatStream } from '../core/server/utils';
/**
diff --git a/src/cli_keystore/add.test.js b/src/cli_keystore/add.test.js
index b5d5009667eb4..f1adee8879bc2 100644
--- a/src/cli_keystore/add.test.js
+++ b/src/cli_keystore/add.test.js
@@ -42,7 +42,7 @@ import { PassThrough } from 'stream';
import { Keystore } from '../legacy/server/keystore';
import { add } from './add';
import { Logger } from '../cli_plugin/lib/logger';
-import * as prompt from '../legacy/server/utils/prompt';
+import * as prompt from './utils/prompt';
describe('Kibana keystore', () => {
describe('add', () => {
diff --git a/src/cli_keystore/create.js b/src/cli_keystore/create.js
index 8be1eb36882f1..55fe2c151dec0 100644
--- a/src/cli_keystore/create.js
+++ b/src/cli_keystore/create.js
@@ -18,7 +18,7 @@
*/
import { Logger } from '../cli_plugin/lib/logger';
-import { confirm } from '../legacy/server/utils';
+import { confirm } from './utils';
export async function create(keystore, command, options) {
const logger = new Logger(options);
diff --git a/src/cli_keystore/create.test.js b/src/cli_keystore/create.test.js
index f48b3775ddfff..cb85475eab1cb 100644
--- a/src/cli_keystore/create.test.js
+++ b/src/cli_keystore/create.test.js
@@ -41,7 +41,7 @@ import sinon from 'sinon';
import { Keystore } from '../legacy/server/keystore';
import { create } from './create';
import { Logger } from '../cli_plugin/lib/logger';
-import * as prompt from '../legacy/server/utils/prompt';
+import * as prompt from './utils/prompt';
describe('Kibana keystore', () => {
describe('create', () => {
diff --git a/src/legacy/server/utils/index.js b/src/cli_keystore/utils/index.js
similarity index 100%
rename from src/legacy/server/utils/index.js
rename to src/cli_keystore/utils/index.js
diff --git a/src/legacy/server/utils/prompt.js b/src/cli_keystore/utils/prompt.js
similarity index 100%
rename from src/legacy/server/utils/prompt.js
rename to src/cli_keystore/utils/prompt.js
diff --git a/src/legacy/server/utils/prompt.test.js b/src/cli_keystore/utils/prompt.test.js
similarity index 100%
rename from src/legacy/server/utils/prompt.test.js
rename to src/cli_keystore/utils/prompt.test.js
diff --git a/src/legacy/utils/index.js b/src/legacy/utils/index.js
index 529b1ddfd8a4d..e2e2331b3aea6 100644
--- a/src/legacy/utils/index.js
+++ b/src/legacy/utils/index.js
@@ -17,8 +17,6 @@
* under the License.
*/
-export { BinderBase } from './binder';
-export { BinderFor } from './binder_for';
export { deepCloneWithBuffers } from './deep_clone_with_buffers';
export { unset } from './unset';
export { IS_KIBANA_DISTRIBUTABLE } from './artifact_type';
diff --git a/src/legacy/utils/path_contains.js b/src/legacy/utils/path_contains.js
deleted file mode 100644
index 60d05c1099554..0000000000000
--- a/src/legacy/utils/path_contains.js
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * 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 { relative } from 'path';
-
-export default function pathContains(root, child) {
- return relative(child, root).slice(0, 2) !== '..';
-}
From 0ca647286a5ccabfb76203b8cbcb1d13b05f105d Mon Sep 17 00:00:00 2001
From: Chris Cowan
Date: Wed, 9 Sep 2020 09:00:27 -0700
Subject: [PATCH 05/25] [Metrics UI] Replace Snapshot API with Metrics API
(#76253)
- Remove server/lib/snapshot
- Replace backend for /api/infra/snapshot with data from Metrics API
- Fixing tests with updates to the snapshot node
Co-authored-by: Elastic Machine
---
x-pack/plugins/infra/common/http_api/index.ts | 1 +
.../infra/common/http_api/metrics_api.ts | 6 +-
.../infra/common/http_api/metrics_explorer.ts | 5 +-
.../infra/common/http_api/snapshot_api.ts | 5 +-
.../infra/common/inventory_models/types.ts | 5 +
.../waffle/conditional_tooltip.test.tsx | 1 +
.../lib/calculate_bounds_from_nodes.test.ts | 2 +
.../inventory_view/lib/sort_nodes.test.ts | 2 +
.../evaluate_condition.ts | 24 +-
.../inventory_metric_threshold_executor.ts | 4 +-
...review_inventory_metric_threshold_alert.ts | 6 +-
.../metric_threshold/lib/metric_query.ts | 4 +-
.../plugins/infra/server/lib/infra_types.ts | 2 -
.../create_aggregations.test.ts.snap | 1 -
...convert_histogram_buckets_to_timeseries.ts | 18 +-
.../lib/metrics/lib/create_aggregations.ts | 2 +-
.../plugins/infra/server/lib/metrics/types.ts | 36 ++-
.../infra/server/lib/snapshot/constants.ts | 9 -
.../server/lib/snapshot/query_helpers.ts | 106 --------
.../lib/snapshot/response_helpers.test.ts | 119 ---------
.../server/lib/snapshot/response_helpers.ts | 208 ---------------
.../infra/server/lib/snapshot/snapshot.ts | 238 ------------------
.../infra/server/lib/snapshot/types.ts | 15 --
.../infra/server/lib/sources/has_data.ts | 2 +-
x-pack/plugins/infra/server/plugin.ts | 3 -
.../infra/server/routes/alerting/preview.ts | 2 +-
.../lib/find_interval_for_metrics.ts | 2 +-
.../lib/get_dataset_for_field.ts | 2 +-
.../infra/server/routes/snapshot/index.ts | 37 +--
.../lib/apply_metadata_to_last_path.ts | 65 +++++
...alculate_index_pattern_based_on_metrics.ts | 22 ++
.../snapshot/lib/constants.ts} | 2 +-
.../snapshot/lib/copy_missing_metrics.ts | 45 ++++
.../lib}/create_timerange_with_interval.ts | 18 +-
.../snapshot/lib/get_metrics_aggregations.ts | 69 +++++
.../server/routes/snapshot/lib/get_nodes.ts | 34 +++
.../routes/snapshot/lib/query_all_data.ts | 33 +++
...ransform_request_to_metrics_api_request.ts | 84 +++++++
...snapshot_metrics_to_metrics_api_metrics.ts | 38 +++
.../lib/trasform_metrics_ui_response.ts | 87 +++++++
.../server/utils/calculate_metric_interval.ts | 2 +-
.../apis/metrics_ui/snapshot.ts | 5 +-
42 files changed, 592 insertions(+), 779 deletions(-)
delete mode 100644 x-pack/plugins/infra/server/lib/snapshot/constants.ts
delete mode 100644 x-pack/plugins/infra/server/lib/snapshot/query_helpers.ts
delete mode 100644 x-pack/plugins/infra/server/lib/snapshot/response_helpers.test.ts
delete mode 100644 x-pack/plugins/infra/server/lib/snapshot/response_helpers.ts
delete mode 100644 x-pack/plugins/infra/server/lib/snapshot/snapshot.ts
delete mode 100644 x-pack/plugins/infra/server/lib/snapshot/types.ts
create mode 100644 x-pack/plugins/infra/server/routes/snapshot/lib/apply_metadata_to_last_path.ts
create mode 100644 x-pack/plugins/infra/server/routes/snapshot/lib/calculate_index_pattern_based_on_metrics.ts
rename x-pack/plugins/infra/server/{lib/snapshot/index.ts => routes/snapshot/lib/constants.ts} (85%)
create mode 100644 x-pack/plugins/infra/server/routes/snapshot/lib/copy_missing_metrics.ts
rename x-pack/plugins/infra/server/{lib/snapshot => routes/snapshot/lib}/create_timerange_with_interval.ts (80%)
create mode 100644 x-pack/plugins/infra/server/routes/snapshot/lib/get_metrics_aggregations.ts
create mode 100644 x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts
create mode 100644 x-pack/plugins/infra/server/routes/snapshot/lib/query_all_data.ts
create mode 100644 x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts
create mode 100644 x-pack/plugins/infra/server/routes/snapshot/lib/transform_snapshot_metrics_to_metrics_api_metrics.ts
create mode 100644 x-pack/plugins/infra/server/routes/snapshot/lib/trasform_metrics_ui_response.ts
diff --git a/x-pack/plugins/infra/common/http_api/index.ts b/x-pack/plugins/infra/common/http_api/index.ts
index 818009417fb1c..4c729d11ba8c1 100644
--- a/x-pack/plugins/infra/common/http_api/index.ts
+++ b/x-pack/plugins/infra/common/http_api/index.ts
@@ -10,3 +10,4 @@ export * from './log_entries';
export * from './metrics_explorer';
export * from './metrics_api';
export * from './log_alerts';
+export * from './snapshot_api';
diff --git a/x-pack/plugins/infra/common/http_api/metrics_api.ts b/x-pack/plugins/infra/common/http_api/metrics_api.ts
index 7436566f039ca..41657fdce2153 100644
--- a/x-pack/plugins/infra/common/http_api/metrics_api.ts
+++ b/x-pack/plugins/infra/common/http_api/metrics_api.ts
@@ -33,7 +33,6 @@ export const MetricsAPIRequestRT = rt.intersection([
afterKey: rt.union([rt.null, afterKeyObjectRT]),
limit: rt.union([rt.number, rt.null, rt.undefined]),
filters: rt.array(rt.object),
- forceInterval: rt.boolean,
dropLastBucket: rt.boolean,
alignDataToEnd: rt.boolean,
}),
@@ -59,7 +58,10 @@ export const MetricsAPIRowRT = rt.intersection([
rt.type({
timestamp: rt.number,
}),
- rt.record(rt.string, rt.union([rt.string, rt.number, rt.null, rt.undefined])),
+ rt.record(
+ rt.string,
+ rt.union([rt.string, rt.number, rt.null, rt.undefined, rt.array(rt.object)])
+ ),
]);
export const MetricsAPISeriesRT = rt.intersection([
diff --git a/x-pack/plugins/infra/common/http_api/metrics_explorer.ts b/x-pack/plugins/infra/common/http_api/metrics_explorer.ts
index c5776e0b0ced1..460b2bf9d802e 100644
--- a/x-pack/plugins/infra/common/http_api/metrics_explorer.ts
+++ b/x-pack/plugins/infra/common/http_api/metrics_explorer.ts
@@ -89,7 +89,10 @@ export const metricsExplorerRowRT = rt.intersection([
rt.type({
timestamp: rt.number,
}),
- rt.record(rt.string, rt.union([rt.string, rt.number, rt.null, rt.undefined])),
+ rt.record(
+ rt.string,
+ rt.union([rt.string, rt.number, rt.null, rt.undefined, rt.array(rt.object)])
+ ),
]);
export const metricsExplorerSeriesRT = rt.intersection([
diff --git a/x-pack/plugins/infra/common/http_api/snapshot_api.ts b/x-pack/plugins/infra/common/http_api/snapshot_api.ts
index 11cb57238f917..e1b8dfa4770ba 100644
--- a/x-pack/plugins/infra/common/http_api/snapshot_api.ts
+++ b/x-pack/plugins/infra/common/http_api/snapshot_api.ts
@@ -6,7 +6,7 @@
import * as rt from 'io-ts';
import { SnapshotMetricTypeRT, ItemTypeRT } from '../inventory_models/types';
-import { metricsExplorerSeriesRT } from './metrics_explorer';
+import { MetricsAPISeriesRT } from './metrics_api';
export const SnapshotNodePathRT = rt.intersection([
rt.type({
@@ -22,7 +22,7 @@ const SnapshotNodeMetricOptionalRT = rt.partial({
value: rt.union([rt.number, rt.null]),
avg: rt.union([rt.number, rt.null]),
max: rt.union([rt.number, rt.null]),
- timeseries: metricsExplorerSeriesRT,
+ timeseries: MetricsAPISeriesRT,
});
const SnapshotNodeMetricRequiredRT = rt.type({
@@ -36,6 +36,7 @@ export const SnapshotNodeMetricRT = rt.intersection([
export const SnapshotNodeRT = rt.type({
metrics: rt.array(SnapshotNodeMetricRT),
path: rt.array(SnapshotNodePathRT),
+ name: rt.string,
});
export const SnapshotNodeResponseRT = rt.type({
diff --git a/x-pack/plugins/infra/common/inventory_models/types.ts b/x-pack/plugins/infra/common/inventory_models/types.ts
index 570220bbc7aa5..851646ef1fa12 100644
--- a/x-pack/plugins/infra/common/inventory_models/types.ts
+++ b/x-pack/plugins/infra/common/inventory_models/types.ts
@@ -281,6 +281,10 @@ export const ESSumBucketAggRT = rt.type({
}),
});
+export const ESTopHitsAggRT = rt.type({
+ top_hits: rt.object,
+});
+
interface SnapshotTermsWithAggregation {
terms: { field: string };
aggregations: MetricsUIAggregation;
@@ -304,6 +308,7 @@ export const ESAggregationRT = rt.union([
ESSumBucketAggRT,
ESTermsWithAggregationRT,
ESCaridnalityAggRT,
+ ESTopHitsAggRT,
]);
export const MetricsUIAggregationRT = rt.record(rt.string, ESAggregationRT);
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx
index d2c30a4f38ee9..e01ca3ab6e844 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx
@@ -88,6 +88,7 @@ describe('ConditionalToolTip', () => {
mockedUseSnapshot.mockReturnValue({
nodes: [
{
+ name: 'host-01',
path: [{ label: 'host-01', value: 'host-01', ip: '192.168.1.10' }],
metrics: [
{ name: 'cpu', value: 0.1, avg: 0.4, max: 0.7 },
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/calculate_bounds_from_nodes.test.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/calculate_bounds_from_nodes.test.ts
index fbb6aa933219a..49f4b56532936 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/calculate_bounds_from_nodes.test.ts
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/calculate_bounds_from_nodes.test.ts
@@ -7,6 +7,7 @@ import { calculateBoundsFromNodes } from './calculate_bounds_from_nodes';
import { SnapshotNode } from '../../../../../common/http_api/snapshot_api';
const nodes: SnapshotNode[] = [
{
+ name: 'host-01',
path: [{ value: 'host-01', label: 'host-01' }],
metrics: [
{
@@ -18,6 +19,7 @@ const nodes: SnapshotNode[] = [
],
},
{
+ name: 'host-02',
path: [{ value: 'host-02', label: 'host-02' }],
metrics: [
{
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/sort_nodes.test.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/sort_nodes.test.ts
index 2a9f8b911c124..f7d9f029f00df 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/sort_nodes.test.ts
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/sort_nodes.test.ts
@@ -9,6 +9,7 @@ import { SnapshotNode } from '../../../../../common/http_api/snapshot_api';
const nodes: SnapshotNode[] = [
{
+ name: 'host-01',
path: [{ value: 'host-01', label: 'host-01' }],
metrics: [
{
@@ -20,6 +21,7 @@ const nodes: SnapshotNode[] = [
],
},
{
+ name: 'host-02',
path: [{ value: 'host-02', label: 'host-02' }],
metrics: [
{
diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts
index 2f3593a11f664..d6592719d0723 100644
--- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts
@@ -16,12 +16,11 @@ import {
} from '../../adapters/framework/adapter_types';
import { Comparator, InventoryMetricConditions } from './types';
import { AlertServices } from '../../../../../alerts/server';
-import { InfraSnapshot } from '../../snapshot';
-import { parseFilterQuery } from '../../../utils/serialized_query';
import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types';
-import { InfraTimerangeInput } from '../../../../common/http_api/snapshot_api';
-import { InfraSourceConfiguration } from '../../sources';
+import { InfraTimerangeInput, SnapshotRequest } from '../../../../common/http_api/snapshot_api';
+import { InfraSource } from '../../sources';
import { UNGROUPED_FACTORY_KEY } from '../common/utils';
+import { getNodes } from '../../../routes/snapshot/lib/get_nodes';
type ConditionResult = InventoryMetricConditions & {
shouldFire: boolean[];
@@ -33,7 +32,7 @@ type ConditionResult = InventoryMetricConditions & {
export const evaluateCondition = async (
condition: InventoryMetricConditions,
nodeType: InventoryItemType,
- sourceConfiguration: InfraSourceConfiguration,
+ source: InfraSource,
callCluster: AlertServices['callCluster'],
filterQuery?: string,
lookbackSize?: number
@@ -55,7 +54,7 @@ export const evaluateCondition = async (
nodeType,
metric,
timerange,
- sourceConfiguration,
+ source,
filterQuery,
customMetric
);
@@ -94,12 +93,11 @@ const getData = async (
nodeType: InventoryItemType,
metric: SnapshotMetricType,
timerange: InfraTimerangeInput,
- sourceConfiguration: InfraSourceConfiguration,
+ source: InfraSource,
filterQuery?: string,
customMetric?: SnapshotCustomMetricInput
) => {
- const snapshot = new InfraSnapshot();
- const esClient = (
+ const client = (
options: CallWithRequestParams
): Promise> => callCluster('search', options);
@@ -107,17 +105,17 @@ const getData = async (
metric === 'custom' ? (customMetric as SnapshotCustomMetricInput) : { type: metric },
];
- const options = {
- filterQuery: parseFilterQuery(filterQuery),
+ const snapshotRequest: SnapshotRequest = {
+ filterQuery,
nodeType,
groupBy: [],
- sourceConfiguration,
+ sourceId: 'default',
metrics,
timerange,
includeTimeseries: Boolean(timerange.lookbackSize),
};
try {
- const { nodes } = await snapshot.getNodes(esClient, options);
+ const { nodes } = await getNodes(client, snapshotRequest, source);
if (!nodes.length) return { [UNGROUPED_FACTORY_KEY]: null }; // No Data state
diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts
index bdac9dcd1dee8..99904f15b4606 100644
--- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts
@@ -50,9 +50,7 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
);
const results = await Promise.all(
- criteria.map((c) =>
- evaluateCondition(c, nodeType, source.configuration, services.callCluster, filterQuery)
- )
+ criteria.map((c) => evaluateCondition(c, nodeType, source, services.callCluster, filterQuery))
);
const inventoryItems = Object.keys(first(results)!);
diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts
index 755c395818f5a..2ab015b6b37a2 100644
--- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts
@@ -26,7 +26,7 @@ interface InventoryMetricThresholdParams {
interface PreviewInventoryMetricThresholdAlertParams {
callCluster: ILegacyScopedClusterClient['callAsCurrentUser'];
params: InventoryMetricThresholdParams;
- config: InfraSource['configuration'];
+ source: InfraSource;
lookback: Unit;
alertInterval: string;
}
@@ -34,7 +34,7 @@ interface PreviewInventoryMetricThresholdAlertParams {
export const previewInventoryMetricThresholdAlert = async ({
callCluster,
params,
- config,
+ source,
lookback,
alertInterval,
}: PreviewInventoryMetricThresholdAlertParams) => {
@@ -55,7 +55,7 @@ export const previewInventoryMetricThresholdAlert = async ({
try {
const results = await Promise.all(
criteria.map((c) =>
- evaluateCondition(c, nodeType, config, callCluster, filterQuery, lookbackSize)
+ evaluateCondition(c, nodeType, source, callCluster, filterQuery, lookbackSize)
)
);
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts
index 078ca46d42e60..8696081043ff7 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts
@@ -8,8 +8,8 @@ import { networkTraffic } from '../../../../../common/inventory_models/shared/me
import { MetricExpressionParams, Aggregators } from '../types';
import { getIntervalInSeconds } from '../../../../utils/get_interval_in_seconds';
import { roundTimestamp } from '../../../../utils/round_timestamp';
-import { getDateHistogramOffset } from '../../../snapshot/query_helpers';
import { createPercentileAggregation } from './create_percentile_aggregation';
+import { calculateDateHistogramOffset } from '../../../metrics/lib/calculate_date_histogram_offset';
const MINIMUM_BUCKETS = 5;
@@ -46,7 +46,7 @@ export const getElasticsearchMetricQuery = (
timeUnit
);
- const offset = getDateHistogramOffset(from, interval);
+ const offset = calculateDateHistogramOffset({ from, to, interval, field: timefield });
const aggregations =
aggType === Aggregators.COUNT
diff --git a/x-pack/plugins/infra/server/lib/infra_types.ts b/x-pack/plugins/infra/server/lib/infra_types.ts
index 9896ad6ac1cd1..084ece52302b0 100644
--- a/x-pack/plugins/infra/server/lib/infra_types.ts
+++ b/x-pack/plugins/infra/server/lib/infra_types.ts
@@ -8,7 +8,6 @@ import { InfraSourceConfiguration } from '../../common/graphql/types';
import { InfraFieldsDomain } from './domains/fields_domain';
import { InfraLogEntriesDomain } from './domains/log_entries_domain';
import { InfraMetricsDomain } from './domains/metrics_domain';
-import { InfraSnapshot } from './snapshot';
import { InfraSources } from './sources';
import { InfraSourceStatus } from './source_status';
import { InfraConfig } from '../plugin';
@@ -30,7 +29,6 @@ export interface InfraDomainLibs {
export interface InfraBackendLibs extends InfraDomainLibs {
configuration: InfraConfig;
framework: KibanaFramework;
- snapshot: InfraSnapshot;
sources: InfraSources;
sourceStatus: InfraSourceStatus;
}
diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/__snapshots__/create_aggregations.test.ts.snap b/x-pack/plugins/infra/server/lib/metrics/lib/__snapshots__/create_aggregations.test.ts.snap
index d2d90914eced5..2cbbc623aed38 100644
--- a/x-pack/plugins/infra/server/lib/metrics/lib/__snapshots__/create_aggregations.test.ts.snap
+++ b/x-pack/plugins/infra/server/lib/metrics/lib/__snapshots__/create_aggregations.test.ts.snap
@@ -53,7 +53,6 @@ Object {
"groupBy0": Object {
"terms": Object {
"field": "host.name",
- "order": "asc",
},
},
},
diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/convert_histogram_buckets_to_timeseries.ts b/x-pack/plugins/infra/server/lib/metrics/lib/convert_histogram_buckets_to_timeseries.ts
index 95e6ece215133..90e584368e9ad 100644
--- a/x-pack/plugins/infra/server/lib/metrics/lib/convert_histogram_buckets_to_timeseries.ts
+++ b/x-pack/plugins/infra/server/lib/metrics/lib/convert_histogram_buckets_to_timeseries.ts
@@ -5,6 +5,7 @@
*/
import { get, values, first } from 'lodash';
+import * as rt from 'io-ts';
import {
MetricsAPIRequest,
MetricsAPISeries,
@@ -13,15 +14,20 @@ import {
} from '../../../../common/http_api/metrics_api';
import {
HistogramBucket,
- MetricValueType,
BasicMetricValueRT,
NormalizedMetricValueRT,
PercentilesTypeRT,
PercentilesKeyedTypeRT,
+ TopHitsTypeRT,
+ MetricValueTypeRT,
} from '../types';
+
const BASE_COLUMNS = [{ name: 'timestamp', type: 'date' }] as MetricsAPIColumn[];
-const getValue = (valueObject: string | number | MetricValueType) => {
+const ValueObjectTypeRT = rt.union([rt.string, rt.number, MetricValueTypeRT]);
+type ValueObjectType = rt.TypeOf;
+
+const getValue = (valueObject: ValueObjectType) => {
if (NormalizedMetricValueRT.is(valueObject)) {
return valueObject.normalized_value || valueObject.value;
}
@@ -50,6 +56,10 @@ const getValue = (valueObject: string | number | MetricValueType) => {
return valueObject.value;
}
+ if (TopHitsTypeRT.is(valueObject)) {
+ return valueObject.hits.hits.map((hit) => hit._source);
+ }
+
return null;
};
@@ -61,8 +71,8 @@ const convertBucketsToRows = (
const ids = options.metrics.map((metric) => metric.id);
const metrics = ids.reduce((acc, id) => {
const valueObject = get(bucket, [id]);
- return { ...acc, [id]: getValue(valueObject) };
- }, {} as Record);
+ return { ...acc, [id]: ValueObjectTypeRT.is(valueObject) ? getValue(valueObject) : null };
+ }, {} as Record);
return { timestamp: bucket.key as number, ...metrics };
});
};
diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/create_aggregations.ts b/x-pack/plugins/infra/server/lib/metrics/lib/create_aggregations.ts
index 991e5febfc634..63fdbb3d2b30f 100644
--- a/x-pack/plugins/infra/server/lib/metrics/lib/create_aggregations.ts
+++ b/x-pack/plugins/infra/server/lib/metrics/lib/create_aggregations.ts
@@ -33,7 +33,7 @@ export const createAggregations = (options: MetricsAPIRequest) => {
composite: {
size: limit,
sources: options.groupBy.map((field, index) => ({
- [`groupBy${index}`]: { terms: { field, order: 'asc' } },
+ [`groupBy${index}`]: { terms: { field } },
})),
},
aggs: histogramAggregation,
diff --git a/x-pack/plugins/infra/server/lib/metrics/types.ts b/x-pack/plugins/infra/server/lib/metrics/types.ts
index d1866470e0cf9..8746614b559d6 100644
--- a/x-pack/plugins/infra/server/lib/metrics/types.ts
+++ b/x-pack/plugins/infra/server/lib/metrics/types.ts
@@ -25,17 +25,51 @@ export const PercentilesKeyedTypeRT = rt.type({
values: rt.array(rt.type({ key: rt.string, value: NumberOrNullRT })),
});
+export const TopHitsTypeRT = rt.type({
+ hits: rt.type({
+ total: rt.type({
+ value: rt.number,
+ relation: rt.string,
+ }),
+ hits: rt.array(
+ rt.intersection([
+ rt.type({
+ _index: rt.string,
+ _id: rt.string,
+ _score: NumberOrNullRT,
+ _source: rt.object,
+ }),
+ rt.partial({
+ sort: rt.array(rt.union([rt.string, rt.number])),
+ max_score: NumberOrNullRT,
+ }),
+ ])
+ ),
+ }),
+});
+
export const MetricValueTypeRT = rt.union([
BasicMetricValueRT,
NormalizedMetricValueRT,
PercentilesTypeRT,
PercentilesKeyedTypeRT,
+ TopHitsTypeRT,
]);
export type MetricValueType = rt.TypeOf;
+export const TermsWithMetrics = rt.intersection([
+ rt.type({
+ buckets: rt.array(rt.record(rt.string, rt.union([rt.number, rt.string, MetricValueTypeRT]))),
+ }),
+ rt.partial({
+ sum_other_doc_count: rt.number,
+ doc_count_error_upper_bound: rt.number,
+ }),
+]);
+
export const HistogramBucketRT = rt.record(
rt.string,
- rt.union([rt.number, rt.string, MetricValueTypeRT])
+ rt.union([rt.number, rt.string, MetricValueTypeRT, TermsWithMetrics])
);
export const HistogramResponseRT = rt.type({
diff --git a/x-pack/plugins/infra/server/lib/snapshot/constants.ts b/x-pack/plugins/infra/server/lib/snapshot/constants.ts
deleted file mode 100644
index 0420878dbcf50..0000000000000
--- a/x-pack/plugins/infra/server/lib/snapshot/constants.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-// TODO: Make SNAPSHOT_COMPOSITE_REQUEST_SIZE configurable from kibana.yml
-
-export const SNAPSHOT_COMPOSITE_REQUEST_SIZE = 75;
diff --git a/x-pack/plugins/infra/server/lib/snapshot/query_helpers.ts b/x-pack/plugins/infra/server/lib/snapshot/query_helpers.ts
deleted file mode 100644
index ca63043ba868e..0000000000000
--- a/x-pack/plugins/infra/server/lib/snapshot/query_helpers.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { i18n } from '@kbn/i18n';
-import { findInventoryModel, findInventoryFields } from '../../../common/inventory_models/index';
-import { InfraSnapshotRequestOptions } from './types';
-import { getIntervalInSeconds } from '../../utils/get_interval_in_seconds';
-import {
- MetricsUIAggregation,
- MetricsUIAggregationRT,
- InventoryItemType,
-} from '../../../common/inventory_models/types';
-import {
- SnapshotMetricInput,
- SnapshotCustomMetricInputRT,
-} from '../../../common/http_api/snapshot_api';
-import { networkTraffic } from '../../../common/inventory_models/shared/metrics/snapshot/network_traffic';
-
-interface GroupBySource {
- [id: string]: {
- terms: {
- field: string | null | undefined;
- missing_bucket?: boolean;
- };
- };
-}
-
-export const getFieldByNodeType = (options: InfraSnapshotRequestOptions) => {
- const inventoryFields = findInventoryFields(options.nodeType, options.sourceConfiguration.fields);
- return inventoryFields.id;
-};
-
-export const getGroupedNodesSources = (options: InfraSnapshotRequestOptions) => {
- const fields = findInventoryFields(options.nodeType, options.sourceConfiguration.fields);
- const sources: GroupBySource[] = options.groupBy.map((gb) => {
- return { [`${gb.field}`]: { terms: { field: gb.field } } };
- });
- sources.push({
- id: {
- terms: { field: fields.id },
- },
- });
- sources.push({
- name: { terms: { field: fields.name, missing_bucket: true } },
- });
- return sources;
-};
-
-export const getMetricsSources = (options: InfraSnapshotRequestOptions) => {
- const fields = findInventoryFields(options.nodeType, options.sourceConfiguration.fields);
- return [{ id: { terms: { field: fields.id } } }];
-};
-
-export const metricToAggregation = (
- nodeType: InventoryItemType,
- metric: SnapshotMetricInput,
- index: number
-) => {
- const inventoryModel = findInventoryModel(nodeType);
- if (SnapshotCustomMetricInputRT.is(metric)) {
- if (metric.aggregation === 'rate') {
- return networkTraffic(`custom_${index}`, metric.field);
- }
- return {
- [`custom_${index}`]: {
- [metric.aggregation]: {
- field: metric.field,
- },
- },
- };
- }
- return inventoryModel.metrics.snapshot?.[metric.type];
-};
-
-export const getMetricsAggregations = (
- options: InfraSnapshotRequestOptions
-): MetricsUIAggregation => {
- const { metrics } = options;
- return metrics.reduce((aggs, metric, index) => {
- const aggregation = metricToAggregation(options.nodeType, metric, index);
- if (!MetricsUIAggregationRT.is(aggregation)) {
- throw new Error(
- i18n.translate('xpack.infra.snapshot.missingSnapshotMetricError', {
- defaultMessage: 'The aggregation for {metric} for {nodeType} is not available.',
- values: {
- nodeType: options.nodeType,
- metric: metric.type,
- },
- })
- );
- }
- return { ...aggs, ...aggregation };
- }, {});
-};
-
-export const getDateHistogramOffset = (from: number, interval: string): string => {
- const fromInSeconds = Math.floor(from / 1000);
- const bucketSizeInSeconds = getIntervalInSeconds(interval);
-
- // negative offset to align buckets with full intervals (e.g. minutes)
- const offset = (fromInSeconds % bucketSizeInSeconds) - bucketSizeInSeconds;
- return `${offset}s`;
-};
diff --git a/x-pack/plugins/infra/server/lib/snapshot/response_helpers.test.ts b/x-pack/plugins/infra/server/lib/snapshot/response_helpers.test.ts
deleted file mode 100644
index 74840afc157d2..0000000000000
--- a/x-pack/plugins/infra/server/lib/snapshot/response_helpers.test.ts
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import {
- isIPv4,
- getIPFromBucket,
- InfraSnapshotNodeGroupByBucket,
- getMetricValueFromBucket,
- InfraSnapshotMetricsBucket,
-} from './response_helpers';
-
-describe('InfraOps ResponseHelpers', () => {
- describe('isIPv4', () => {
- it('should return true for IPv4', () => {
- expect(isIPv4('192.168.2.4')).toBe(true);
- });
- it('should return false for anything else', () => {
- expect(isIPv4('0:0:0:0:0:0:0:1')).toBe(false);
- });
- });
-
- describe('getIPFromBucket', () => {
- it('should return IPv4 address', () => {
- const bucket: InfraSnapshotNodeGroupByBucket = {
- key: {
- id: 'example-01',
- name: 'example-01',
- },
- ip: {
- hits: {
- total: { value: 1 },
- hits: [
- {
- _index: 'metricbeat-2019-01-01',
- _type: '_doc',
- _id: '29392939',
- _score: null,
- sort: [],
- _source: {
- host: {
- ip: ['2001:db8:85a3::8a2e:370:7334', '192.168.1.4'],
- },
- },
- },
- ],
- },
- },
- };
- expect(getIPFromBucket('host', bucket)).toBe('192.168.1.4');
- });
- it('should NOT return ipv6 address', () => {
- const bucket: InfraSnapshotNodeGroupByBucket = {
- key: {
- id: 'example-01',
- name: 'example-01',
- },
- ip: {
- hits: {
- total: { value: 1 },
- hits: [
- {
- _index: 'metricbeat-2019-01-01',
- _type: '_doc',
- _id: '29392939',
- _score: null,
- sort: [],
- _source: {
- host: {
- ip: ['2001:db8:85a3::8a2e:370:7334'],
- },
- },
- },
- ],
- },
- },
- };
- expect(getIPFromBucket('host', bucket)).toBe(null);
- });
- });
-
- describe('getMetricValueFromBucket', () => {
- it('should return the value of a bucket with data', () => {
- expect(getMetricValueFromBucket('custom', testBucket, 1)).toBe(0.5);
- });
- it('should return the normalized value of a bucket with data', () => {
- expect(getMetricValueFromBucket('cpu', testNormalizedBucket, 1)).toBe(50);
- });
- it('should return null for a bucket with no data', () => {
- expect(getMetricValueFromBucket('custom', testEmptyBucket, 1)).toBe(null);
- });
- });
-});
-
-// Hack to get around TypeScript
-const buckets = [
- {
- key: 'a',
- doc_count: 1,
- custom_1: {
- value: 0.5,
- },
- },
- {
- key: 'b',
- doc_count: 1,
- cpu: {
- value: 0.5,
- normalized_value: 50,
- },
- },
- {
- key: 'c',
- doc_count: 0,
- },
-] as InfraSnapshotMetricsBucket[];
-const [testBucket, testNormalizedBucket, testEmptyBucket] = buckets;
diff --git a/x-pack/plugins/infra/server/lib/snapshot/response_helpers.ts b/x-pack/plugins/infra/server/lib/snapshot/response_helpers.ts
deleted file mode 100644
index 2652e362b7eff..0000000000000
--- a/x-pack/plugins/infra/server/lib/snapshot/response_helpers.ts
+++ /dev/null
@@ -1,208 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { isNumber, last, max, sum, get } from 'lodash';
-import moment from 'moment';
-
-import { MetricsExplorerSeries } from '../../../common/http_api/metrics_explorer';
-import { getIntervalInSeconds } from '../../utils/get_interval_in_seconds';
-import { InfraSnapshotRequestOptions } from './types';
-import { findInventoryModel } from '../../../common/inventory_models';
-import { InventoryItemType, SnapshotMetricType } from '../../../common/inventory_models/types';
-import { SnapshotNodeMetric, SnapshotNodePath } from '../../../common/http_api/snapshot_api';
-
-export interface InfraSnapshotNodeMetricsBucket {
- key: { id: string };
- histogram: {
- buckets: InfraSnapshotMetricsBucket[];
- };
-}
-
-// Jumping through TypeScript hoops here:
-// We need an interface that has the known members 'key' and 'doc_count' and also
-// an unknown number of members with unknown names but known format, containing the
-// metrics.
-// This union type is the only way I found to express this that TypeScript accepts.
-export interface InfraSnapshotBucketWithKey {
- key: string | number;
- doc_count: number;
-}
-
-export interface InfraSnapshotBucketWithValues {
- [name: string]: { value: number; normalized_value?: number };
-}
-
-export type InfraSnapshotMetricsBucket = InfraSnapshotBucketWithKey & InfraSnapshotBucketWithValues;
-
-interface InfraSnapshotIpHit {
- _index: string;
- _type: string;
- _id: string;
- _score: number | null;
- _source: {
- host: {
- ip: string[] | string;
- };
- };
- sort: number[];
-}
-
-export interface InfraSnapshotNodeGroupByBucket {
- key: {
- id: string;
- name: string;
- [groupByField: string]: string;
- };
- ip: {
- hits: {
- total: { value: number };
- hits: InfraSnapshotIpHit[];
- };
- };
-}
-
-export const isIPv4 = (subject: string) => /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(subject);
-
-export const getIPFromBucket = (
- nodeType: InventoryItemType,
- bucket: InfraSnapshotNodeGroupByBucket
-): string | null => {
- const inventoryModel = findInventoryModel(nodeType);
- if (!inventoryModel.fields.ip) {
- return null;
- }
- const ip = get(bucket, `ip.hits.hits[0]._source.${inventoryModel.fields.ip}`, null) as
- | string[]
- | null;
- if (Array.isArray(ip)) {
- return ip.find(isIPv4) || null;
- } else if (typeof ip === 'string') {
- return ip;
- }
-
- return null;
-};
-
-export const getNodePath = (
- groupBucket: InfraSnapshotNodeGroupByBucket,
- options: InfraSnapshotRequestOptions
-): SnapshotNodePath[] => {
- const node = groupBucket.key;
- const path = options.groupBy.map((gb) => {
- return { value: node[`${gb.field}`], label: node[`${gb.field}`] } as SnapshotNodePath;
- });
- const ip = getIPFromBucket(options.nodeType, groupBucket);
- path.push({ value: node.id, label: node.name || node.id, ip });
- return path;
-};
-
-interface NodeMetricsForLookup {
- [nodeId: string]: InfraSnapshotMetricsBucket[];
-}
-
-export const getNodeMetricsForLookup = (
- metrics: InfraSnapshotNodeMetricsBucket[]
-): NodeMetricsForLookup => {
- return metrics.reduce((acc: NodeMetricsForLookup, metric) => {
- acc[`${metric.key.id}`] = metric.histogram.buckets;
- return acc;
- }, {});
-};
-
-// In the returned object,
-// value contains the value from the last bucket spanning a full interval
-// max and avg are calculated from all buckets returned for the timerange
-export const getNodeMetrics = (
- nodeBuckets: InfraSnapshotMetricsBucket[],
- options: InfraSnapshotRequestOptions
-): SnapshotNodeMetric[] => {
- if (!nodeBuckets) {
- return options.metrics.map((metric) => ({
- name: metric.type,
- value: null,
- max: null,
- avg: null,
- }));
- }
- const lastBucket = findLastFullBucket(nodeBuckets, options);
- if (!lastBucket) return [];
- return options.metrics.map((metric, index) => {
- const metricResult: SnapshotNodeMetric = {
- name: metric.type,
- value: getMetricValueFromBucket(metric.type, lastBucket, index),
- max: calculateMax(nodeBuckets, metric.type, index),
- avg: calculateAvg(nodeBuckets, metric.type, index),
- };
- if (options.includeTimeseries) {
- metricResult.timeseries = getTimeseriesData(nodeBuckets, metric.type, index);
- }
- return metricResult;
- });
-};
-
-const findLastFullBucket = (
- buckets: InfraSnapshotMetricsBucket[],
- options: InfraSnapshotRequestOptions
-) => {
- const to = moment.utc(options.timerange.to);
- const bucketSize = getIntervalInSeconds(options.timerange.interval);
- return buckets.reduce((current, item) => {
- const itemKey = isNumber(item.key) ? item.key : parseInt(item.key, 10);
- const date = moment.utc(itemKey + bucketSize * 1000);
- if (!date.isAfter(to) && item.doc_count > 0) {
- return item;
- }
- return current;
- }, last(buckets));
-};
-
-export const getMetricValueFromBucket = (
- type: SnapshotMetricType,
- bucket: InfraSnapshotMetricsBucket,
- index: number
-) => {
- const key = type === 'custom' ? `custom_${index}` : type;
- const metric = bucket[key];
- const value = metric && (metric.normalized_value || metric.value);
- return isFinite(value) ? value : null;
-};
-
-function calculateMax(
- buckets: InfraSnapshotMetricsBucket[],
- type: SnapshotMetricType,
- index: number
-) {
- return max(buckets.map((bucket) => getMetricValueFromBucket(type, bucket, index))) || 0;
-}
-
-function calculateAvg(
- buckets: InfraSnapshotMetricsBucket[],
- type: SnapshotMetricType,
- index: number
-) {
- return (
- sum(buckets.map((bucket) => getMetricValueFromBucket(type, bucket, index))) / buckets.length ||
- 0
- );
-}
-
-function getTimeseriesData(
- buckets: InfraSnapshotMetricsBucket[],
- type: SnapshotMetricType,
- index: number
-): MetricsExplorerSeries {
- return {
- id: type,
- columns: [
- { name: 'timestamp', type: 'date' },
- { name: 'metric_0', type: 'number' },
- ],
- rows: buckets.map((bucket) => ({
- timestamp: bucket.key as number,
- metric_0: getMetricValueFromBucket(type, bucket, index),
- })),
- };
-}
diff --git a/x-pack/plugins/infra/server/lib/snapshot/snapshot.ts b/x-pack/plugins/infra/server/lib/snapshot/snapshot.ts
deleted file mode 100644
index 33d8e738a717e..0000000000000
--- a/x-pack/plugins/infra/server/lib/snapshot/snapshot.ts
+++ /dev/null
@@ -1,238 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-import { InfraDatabaseSearchResponse, CallWithRequestParams } from '../adapters/framework';
-
-import { JsonObject } from '../../../common/typed_json';
-import { SNAPSHOT_COMPOSITE_REQUEST_SIZE } from './constants';
-import {
- getGroupedNodesSources,
- getMetricsAggregations,
- getMetricsSources,
- getDateHistogramOffset,
-} from './query_helpers';
-import {
- getNodeMetrics,
- getNodeMetricsForLookup,
- getNodePath,
- InfraSnapshotNodeGroupByBucket,
- InfraSnapshotNodeMetricsBucket,
-} from './response_helpers';
-import { getAllCompositeData } from '../../utils/get_all_composite_data';
-import { createAfterKeyHandler } from '../../utils/create_afterkey_handler';
-import { findInventoryModel } from '../../../common/inventory_models';
-import { InfraSnapshotRequestOptions } from './types';
-import { createTimeRangeWithInterval } from './create_timerange_with_interval';
-import { SnapshotNode } from '../../../common/http_api/snapshot_api';
-
-type NamedSnapshotNode = SnapshotNode & { name: string };
-
-export type ESSearchClient = (
- options: CallWithRequestParams
-) => Promise>;
-export class InfraSnapshot {
- public async getNodes(
- client: ESSearchClient,
- options: InfraSnapshotRequestOptions
- ): Promise<{ nodes: NamedSnapshotNode[]; interval: string }> {
- // Both requestGroupedNodes and requestNodeMetrics may send several requests to elasticsearch
- // in order to page through the results of their respective composite aggregations.
- // Both chains of requests are supposed to run in parallel, and their results be merged
- // when they have both been completed.
- const timeRangeWithIntervalApplied = await createTimeRangeWithInterval(client, options);
- const optionsWithTimerange = { ...options, timerange: timeRangeWithIntervalApplied };
-
- const groupedNodesPromise = requestGroupedNodes(client, optionsWithTimerange);
- const nodeMetricsPromise = requestNodeMetrics(client, optionsWithTimerange);
- const [groupedNodeBuckets, nodeMetricBuckets] = await Promise.all([
- groupedNodesPromise,
- nodeMetricsPromise,
- ]);
- return {
- nodes: mergeNodeBuckets(groupedNodeBuckets, nodeMetricBuckets, options),
- interval: timeRangeWithIntervalApplied.interval,
- };
- }
-}
-
-const bucketSelector = (
- response: InfraDatabaseSearchResponse<{}, InfraSnapshotAggregationResponse>
-) => (response.aggregations && response.aggregations.nodes.buckets) || [];
-
-const handleAfterKey = createAfterKeyHandler(
- 'body.aggregations.nodes.composite.after',
- (input) => input?.aggregations?.nodes?.after_key
-);
-
-const callClusterFactory = (search: ESSearchClient) => (opts: any) =>
- search<{}, InfraSnapshotAggregationResponse>(opts);
-
-const requestGroupedNodes = async (
- client: ESSearchClient,
- options: InfraSnapshotRequestOptions
-): Promise => {
- const inventoryModel = findInventoryModel(options.nodeType);
- const query = {
- allowNoIndices: true,
- index: `${options.sourceConfiguration.logAlias},${options.sourceConfiguration.metricAlias}`,
- ignoreUnavailable: true,
- body: {
- query: {
- bool: {
- filter: buildFilters(options),
- },
- },
- size: 0,
- aggregations: {
- nodes: {
- composite: {
- size: options.overrideCompositeSize || SNAPSHOT_COMPOSITE_REQUEST_SIZE,
- sources: getGroupedNodesSources(options),
- },
- aggs: {
- ip: {
- top_hits: {
- sort: [{ [options.sourceConfiguration.fields.timestamp]: { order: 'desc' } }],
- _source: {
- includes: inventoryModel.fields.ip ? [inventoryModel.fields.ip] : [],
- },
- size: 1,
- },
- },
- },
- },
- },
- },
- };
- return getAllCompositeData(
- callClusterFactory(client),
- query,
- bucketSelector,
- handleAfterKey
- );
-};
-
-const calculateIndexPatterBasedOnMetrics = (options: InfraSnapshotRequestOptions) => {
- const { metrics } = options;
- if (metrics.every((m) => m.type === 'logRate')) {
- return options.sourceConfiguration.logAlias;
- }
- if (metrics.some((m) => m.type === 'logRate')) {
- return `${options.sourceConfiguration.logAlias},${options.sourceConfiguration.metricAlias}`;
- }
- return options.sourceConfiguration.metricAlias;
-};
-
-const requestNodeMetrics = async (
- client: ESSearchClient,
- options: InfraSnapshotRequestOptions
-): Promise => {
- const index = calculateIndexPatterBasedOnMetrics(options);
- const query = {
- allowNoIndices: true,
- index,
- ignoreUnavailable: true,
- body: {
- query: {
- bool: {
- filter: buildFilters(options, false),
- },
- },
- size: 0,
- aggregations: {
- nodes: {
- composite: {
- size: options.overrideCompositeSize || SNAPSHOT_COMPOSITE_REQUEST_SIZE,
- sources: getMetricsSources(options),
- },
- aggregations: {
- histogram: {
- date_histogram: {
- field: options.sourceConfiguration.fields.timestamp,
- interval: options.timerange.interval || '1m',
- offset: getDateHistogramOffset(options.timerange.from, options.timerange.interval),
- extended_bounds: {
- min: options.timerange.from,
- max: options.timerange.to,
- },
- },
- aggregations: getMetricsAggregations(options),
- },
- },
- },
- },
- },
- };
- return getAllCompositeData(
- callClusterFactory(client),
- query,
- bucketSelector,
- handleAfterKey
- );
-};
-
-// buckets can be InfraSnapshotNodeGroupByBucket[] or InfraSnapshotNodeMetricsBucket[]
-// but typing this in a way that makes TypeScript happy is unreadable (if possible at all)
-interface InfraSnapshotAggregationResponse {
- nodes: {
- buckets: any[];
- after_key: { [id: string]: string };
- };
-}
-
-const mergeNodeBuckets = (
- nodeGroupByBuckets: InfraSnapshotNodeGroupByBucket[],
- nodeMetricsBuckets: InfraSnapshotNodeMetricsBucket[],
- options: InfraSnapshotRequestOptions
-): NamedSnapshotNode[] => {
- const nodeMetricsForLookup = getNodeMetricsForLookup(nodeMetricsBuckets);
-
- return nodeGroupByBuckets.map((node) => {
- return {
- name: node.key.name || node.key.id, // For type safety; name can be derived from getNodePath but not in a TS-friendly way
- path: getNodePath(node, options),
- metrics: getNodeMetrics(nodeMetricsForLookup[node.key.id], options),
- };
- });
-};
-
-const createQueryFilterClauses = (filterQuery: JsonObject | undefined) =>
- filterQuery ? [filterQuery] : [];
-
-const buildFilters = (options: InfraSnapshotRequestOptions, withQuery = true) => {
- let filters: any = [
- {
- range: {
- [options.sourceConfiguration.fields.timestamp]: {
- gte: options.timerange.from,
- lte: options.timerange.to,
- format: 'epoch_millis',
- },
- },
- },
- ];
-
- if (withQuery) {
- filters = [...createQueryFilterClauses(options.filterQuery), ...filters];
- }
-
- if (options.accountId) {
- filters.push({
- term: {
- 'cloud.account.id': options.accountId,
- },
- });
- }
-
- if (options.region) {
- filters.push({
- term: {
- 'cloud.region': options.region,
- },
- });
- }
-
- return filters;
-};
diff --git a/x-pack/plugins/infra/server/lib/snapshot/types.ts b/x-pack/plugins/infra/server/lib/snapshot/types.ts
deleted file mode 100644
index 7e17cb91c6a59..0000000000000
--- a/x-pack/plugins/infra/server/lib/snapshot/types.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { JsonObject } from '../../../common/typed_json';
-import { InfraSourceConfiguration } from '../../../common/graphql/types';
-import { SnapshotRequest } from '../../../common/http_api/snapshot_api';
-
-export interface InfraSnapshotRequestOptions
- extends Omit {
- sourceConfiguration: InfraSourceConfiguration;
- filterQuery: JsonObject | undefined;
-}
diff --git a/x-pack/plugins/infra/server/lib/sources/has_data.ts b/x-pack/plugins/infra/server/lib/sources/has_data.ts
index 79b1375059dcb..53297640e541d 100644
--- a/x-pack/plugins/infra/server/lib/sources/has_data.ts
+++ b/x-pack/plugins/infra/server/lib/sources/has_data.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { ESSearchClient } from '../snapshot';
+import { ESSearchClient } from '../metrics/types';
export const hasData = async (index: string, client: ESSearchClient) => {
const params = {
diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts
index 51f91d7189db7..90b73b9a7585a 100644
--- a/x-pack/plugins/infra/server/plugin.ts
+++ b/x-pack/plugins/infra/server/plugin.ts
@@ -19,7 +19,6 @@ import { InfraElasticsearchSourceStatusAdapter } from './lib/adapters/source_sta
import { InfraFieldsDomain } from './lib/domains/fields_domain';
import { InfraLogEntriesDomain } from './lib/domains/log_entries_domain';
import { InfraMetricsDomain } from './lib/domains/metrics_domain';
-import { InfraSnapshot } from './lib/snapshot';
import { InfraSourceStatus } from './lib/source_status';
import { InfraSources } from './lib/sources';
import { InfraServerPluginDeps } from './lib/adapters/framework';
@@ -105,7 +104,6 @@ export class InfraServerPlugin {
sources,
}
);
- const snapshot = new InfraSnapshot();
// register saved object types
core.savedObjects.registerType(infraSourceConfigurationSavedObjectType);
@@ -129,7 +127,6 @@ export class InfraServerPlugin {
this.libs = {
configuration: this.config,
framework,
- snapshot,
sources,
sourceStatus,
...domainLibs,
diff --git a/x-pack/plugins/infra/server/routes/alerting/preview.ts b/x-pack/plugins/infra/server/routes/alerting/preview.ts
index 5594323d706de..40d09dadfe050 100644
--- a/x-pack/plugins/infra/server/routes/alerting/preview.ts
+++ b/x-pack/plugins/infra/server/routes/alerting/preview.ts
@@ -82,7 +82,7 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs)
callCluster,
params: { criteria, filterQuery, nodeType },
lookback,
- config: source.configuration,
+ source,
alertInterval,
});
diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/find_interval_for_metrics.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/find_interval_for_metrics.ts
index 876bbb4199441..8ab0f4a44c85d 100644
--- a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/find_interval_for_metrics.ts
+++ b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/find_interval_for_metrics.ts
@@ -7,9 +7,9 @@
import { uniq } from 'lodash';
import LRU from 'lru-cache';
import { MetricsExplorerRequestBody } from '../../../../common/http_api';
-import { ESSearchClient } from '../../../lib/snapshot';
import { getDatasetForField } from './get_dataset_for_field';
import { calculateMetricInterval } from '../../../utils/calculate_metric_interval';
+import { ESSearchClient } from '../../../lib/metrics/types';
const cache = new LRU({
max: 100,
diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_dataset_for_field.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_dataset_for_field.ts
index 94e91d32b14bb..85bb5b106c87c 100644
--- a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_dataset_for_field.ts
+++ b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_dataset_for_field.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { ESSearchClient } from '../../../lib/snapshot';
+import { ESSearchClient } from '../../../lib/metrics/types';
interface EventDatasetHit {
_source: {
diff --git a/x-pack/plugins/infra/server/routes/snapshot/index.ts b/x-pack/plugins/infra/server/routes/snapshot/index.ts
index 00bc1e74ea871..3f09ae89bc97e 100644
--- a/x-pack/plugins/infra/server/routes/snapshot/index.ts
+++ b/x-pack/plugins/infra/server/routes/snapshot/index.ts
@@ -10,10 +10,10 @@ import { fold } from 'fp-ts/lib/Either';
import { identity } from 'fp-ts/lib/function';
import { InfraBackendLibs } from '../../lib/infra_types';
import { UsageCollector } from '../../usage/usage_collector';
-import { parseFilterQuery } from '../../utils/serialized_query';
import { SnapshotRequestRT, SnapshotNodeResponseRT } from '../../../common/http_api/snapshot_api';
import { throwErrors } from '../../../common/runtime_types';
import { createSearchClient } from '../../lib/create_search_client';
+import { getNodes } from './lib/get_nodes';
const escapeHatch = schema.object({}, { unknowns: 'allow' });
@@ -30,43 +30,22 @@ export const initSnapshotRoute = (libs: InfraBackendLibs) => {
},
async (requestContext, request, response) => {
try {
- const {
- filterQuery,
- nodeType,
- groupBy,
- sourceId,
- metrics,
- timerange,
- accountId,
- region,
- includeTimeseries,
- overrideCompositeSize,
- } = pipe(
+ const snapshotRequest = pipe(
SnapshotRequestRT.decode(request.body),
fold(throwErrors(Boom.badRequest), identity)
);
+
const source = await libs.sources.getSourceConfiguration(
requestContext.core.savedObjects.client,
- sourceId
+ snapshotRequest.sourceId
);
- UsageCollector.countNode(nodeType);
- const options = {
- filterQuery: parseFilterQuery(filterQuery),
- accountId,
- region,
- nodeType,
- groupBy,
- sourceConfiguration: source.configuration,
- metrics,
- timerange,
- includeTimeseries,
- overrideCompositeSize,
- };
+ UsageCollector.countNode(snapshotRequest.nodeType);
const client = createSearchClient(requestContext, framework);
- const nodesWithInterval = await libs.snapshot.getNodes(client, options);
+ const snapshotResponse = await getNodes(client, snapshotRequest, source);
+
return response.ok({
- body: SnapshotNodeResponseRT.encode(nodesWithInterval),
+ body: SnapshotNodeResponseRT.encode(snapshotResponse),
});
} catch (error) {
return response.internalError({
diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/apply_metadata_to_last_path.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/apply_metadata_to_last_path.ts
new file mode 100644
index 0000000000000..f41d76bbc156f
--- /dev/null
+++ b/x-pack/plugins/infra/server/routes/snapshot/lib/apply_metadata_to_last_path.ts
@@ -0,0 +1,65 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { get, last, first, isArray } from 'lodash';
+import { findInventoryFields } from '../../../../common/inventory_models';
+import {
+ SnapshotRequest,
+ SnapshotNodePath,
+ SnapshotNode,
+ MetricsAPISeries,
+ MetricsAPIRow,
+} from '../../../../common/http_api';
+import { META_KEY } from './constants';
+import { InfraSource } from '../../../lib/sources';
+
+export const isIPv4 = (subject: string) => /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(subject);
+
+type RowWithMetadata = MetricsAPIRow & {
+ [META_KEY]: object[];
+};
+
+export const applyMetadataToLastPath = (
+ series: MetricsAPISeries,
+ node: SnapshotNode,
+ snapshotRequest: SnapshotRequest,
+ source: InfraSource
+): SnapshotNodePath[] => {
+ // First we need to find a row with metadata
+ const rowWithMeta = series.rows.find(
+ (row) => (row[META_KEY] && isArray(row[META_KEY]) && (row[META_KEY] as object[]).length) || 0
+ ) as RowWithMetadata | undefined;
+
+ if (rowWithMeta) {
+ // We need just the first doc, there should only be one
+ const firstMetaDoc = first(rowWithMeta[META_KEY]);
+ // We also need the last path to add the metadata to
+ const lastPath = last(node.path);
+ if (firstMetaDoc && lastPath) {
+ // We will need the inventory fields so we can use the field paths to get
+ // the values from the metadata document
+ const inventoryFields = findInventoryFields(
+ snapshotRequest.nodeType,
+ source.configuration.fields
+ );
+ // Set the label as the name and fallback to the id OR path.value
+ lastPath.label = get(firstMetaDoc, inventoryFields.name, lastPath.value);
+ // If the inventory fields contain an ip address, we need to try and set that
+ // on the path object. IP addersses are typically stored as multiple fields. We will
+ // use the first IPV4 address we find.
+ if (inventoryFields.ip) {
+ const ipAddresses = get(firstMetaDoc, inventoryFields.ip) as string[];
+ if (Array.isArray(ipAddresses)) {
+ lastPath.ip = ipAddresses.find(isIPv4) || null;
+ } else if (typeof ipAddresses === 'string') {
+ lastPath.ip = ipAddresses;
+ }
+ }
+ return [...node.path.slice(0, node.path.length - 1), lastPath];
+ }
+ }
+ return node.path;
+};
diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/calculate_index_pattern_based_on_metrics.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/calculate_index_pattern_based_on_metrics.ts
new file mode 100644
index 0000000000000..4218aecfe74a8
--- /dev/null
+++ b/x-pack/plugins/infra/server/routes/snapshot/lib/calculate_index_pattern_based_on_metrics.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { SnapshotRequest } from '../../../../common/http_api';
+import { InfraSource } from '../../../lib/sources';
+
+export const calculateIndexPatterBasedOnMetrics = (
+ options: SnapshotRequest,
+ source: InfraSource
+) => {
+ const { metrics } = options;
+ if (metrics.every((m) => m.type === 'logRate')) {
+ return source.configuration.logAlias;
+ }
+ if (metrics.some((m) => m.type === 'logRate')) {
+ return `${source.configuration.logAlias},${source.configuration.metricAlias}`;
+ }
+ return source.configuration.metricAlias;
+};
diff --git a/x-pack/plugins/infra/server/lib/snapshot/index.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/constants.ts
similarity index 85%
rename from x-pack/plugins/infra/server/lib/snapshot/index.ts
rename to x-pack/plugins/infra/server/routes/snapshot/lib/constants.ts
index 8db54da803648..563c720224435 100644
--- a/x-pack/plugins/infra/server/lib/snapshot/index.ts
+++ b/x-pack/plugins/infra/server/routes/snapshot/lib/constants.ts
@@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export * from './snapshot';
+export const META_KEY = '__metadata__';
diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/copy_missing_metrics.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/copy_missing_metrics.ts
new file mode 100644
index 0000000000000..36397862e4153
--- /dev/null
+++ b/x-pack/plugins/infra/server/routes/snapshot/lib/copy_missing_metrics.ts
@@ -0,0 +1,45 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { memoize, last, first } from 'lodash';
+import { SnapshotNode, SnapshotNodeResponse } from '../../../../common/http_api';
+
+const createMissingMetricFinder = (nodes: SnapshotNode[]) =>
+ memoize((id: string) => {
+ const nodeWithMetrics = nodes.find((node) => {
+ const lastPath = last(node.path);
+ const metric = first(node.metrics);
+ return lastPath && metric && lastPath.value === id && metric.value !== null;
+ });
+ if (nodeWithMetrics) {
+ return nodeWithMetrics.metrics;
+ }
+ });
+
+/**
+ * This function will look for nodes with missing data and try to find a node to copy the data from.
+ * This functionality exists to suppor the use case where the user requests a group by on "Service type".
+ * Since that grouping naturally excludeds every metric (except the metric for the service.type), we still
+ * want to display the node with a value. A good example is viewing hosts by CPU Usage and grouping by service
+ * Without this every service but `system` would be null.
+ */
+export const copyMissingMetrics = (response: SnapshotNodeResponse) => {
+ const { nodes } = response;
+ const find = createMissingMetricFinder(nodes);
+ const newNodes = nodes.map((node) => {
+ const lastPath = last(node.path);
+ const metric = first(node.metrics);
+ const allRowsNull = metric?.timeseries?.rows.every((r) => r.metric_0 == null) ?? true;
+ if (lastPath && metric && metric.value === null && allRowsNull) {
+ const newMetrics = find(lastPath.value);
+ if (newMetrics) {
+ return { ...node, metrics: newMetrics };
+ }
+ }
+ return node;
+ });
+ return { ...response, nodes: newNodes };
+};
diff --git a/x-pack/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/create_timerange_with_interval.ts
similarity index 80%
rename from x-pack/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts
rename to x-pack/plugins/infra/server/routes/snapshot/lib/create_timerange_with_interval.ts
index 719ffdb8fa7c4..827e0901c1c01 100644
--- a/x-pack/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts
+++ b/x-pack/plugins/infra/server/routes/snapshot/lib/create_timerange_with_interval.ts
@@ -5,14 +5,16 @@
*/
import { uniq } from 'lodash';
-import { InfraSnapshotRequestOptions } from './types';
-import { getMetricsAggregations } from './query_helpers';
-import { calculateMetricInterval } from '../../utils/calculate_metric_interval';
-import { MetricsUIAggregation, ESBasicMetricAggRT } from '../../../common/inventory_models/types';
-import { getDatasetForField } from '../../routes/metrics_explorer/lib/get_dataset_for_field';
-import { InfraTimerangeInput } from '../../../common/http_api/snapshot_api';
-import { ESSearchClient } from '.';
-import { getIntervalInSeconds } from '../../utils/get_interval_in_seconds';
+import { InfraTimerangeInput } from '../../../../common/http_api';
+import { ESSearchClient } from '../../../lib/metrics/types';
+import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds';
+import { calculateMetricInterval } from '../../../utils/calculate_metric_interval';
+import { getMetricsAggregations, InfraSnapshotRequestOptions } from './get_metrics_aggregations';
+import {
+ MetricsUIAggregation,
+ ESBasicMetricAggRT,
+} from '../../../../common/inventory_models/types';
+import { getDatasetForField } from '../../metrics_explorer/lib/get_dataset_for_field';
const createInterval = async (client: ESSearchClient, options: InfraSnapshotRequestOptions) => {
const { timerange } = options;
diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/get_metrics_aggregations.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/get_metrics_aggregations.ts
new file mode 100644
index 0000000000000..2421469eb1bdd
--- /dev/null
+++ b/x-pack/plugins/infra/server/routes/snapshot/lib/get_metrics_aggregations.ts
@@ -0,0 +1,69 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+import { JsonObject } from '../../../../common/typed_json';
+import {
+ InventoryItemType,
+ MetricsUIAggregation,
+ MetricsUIAggregationRT,
+} from '../../../../common/inventory_models/types';
+import {
+ SnapshotMetricInput,
+ SnapshotCustomMetricInputRT,
+ SnapshotRequest,
+} from '../../../../common/http_api';
+import { findInventoryModel } from '../../../../common/inventory_models';
+import { networkTraffic } from '../../../../common/inventory_models/shared/metrics/snapshot/network_traffic';
+import { InfraSourceConfiguration } from '../../../lib/sources';
+
+export interface InfraSnapshotRequestOptions
+ extends Omit {
+ sourceConfiguration: InfraSourceConfiguration;
+ filterQuery: JsonObject | undefined;
+}
+
+export const metricToAggregation = (
+ nodeType: InventoryItemType,
+ metric: SnapshotMetricInput,
+ index: number
+) => {
+ const inventoryModel = findInventoryModel(nodeType);
+ if (SnapshotCustomMetricInputRT.is(metric)) {
+ if (metric.aggregation === 'rate') {
+ return networkTraffic(`custom_${index}`, metric.field);
+ }
+ return {
+ [`custom_${index}`]: {
+ [metric.aggregation]: {
+ field: metric.field,
+ },
+ },
+ };
+ }
+ return inventoryModel.metrics.snapshot?.[metric.type];
+};
+
+export const getMetricsAggregations = (
+ options: InfraSnapshotRequestOptions
+): MetricsUIAggregation => {
+ const { metrics } = options;
+ return metrics.reduce((aggs, metric, index) => {
+ const aggregation = metricToAggregation(options.nodeType, metric, index);
+ if (!MetricsUIAggregationRT.is(aggregation)) {
+ throw new Error(
+ i18n.translate('xpack.infra.snapshot.missingSnapshotMetricError', {
+ defaultMessage: 'The aggregation for {metric} for {nodeType} is not available.',
+ values: {
+ nodeType: options.nodeType,
+ metric: metric.type,
+ },
+ })
+ );
+ }
+ return { ...aggs, ...aggregation };
+ }, {});
+};
diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts
new file mode 100644
index 0000000000000..9332d5aee1f52
--- /dev/null
+++ b/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts
@@ -0,0 +1,34 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { SnapshotRequest } from '../../../../common/http_api';
+import { ESSearchClient } from '../../../lib/metrics/types';
+import { InfraSource } from '../../../lib/sources';
+import { transformRequestToMetricsAPIRequest } from './transform_request_to_metrics_api_request';
+import { queryAllData } from './query_all_data';
+import { transformMetricsApiResponseToSnapshotResponse } from './trasform_metrics_ui_response';
+import { copyMissingMetrics } from './copy_missing_metrics';
+
+export const getNodes = async (
+ client: ESSearchClient,
+ snapshotRequest: SnapshotRequest,
+ source: InfraSource
+) => {
+ const metricsApiRequest = await transformRequestToMetricsAPIRequest(
+ client,
+ source,
+ snapshotRequest
+ );
+ const metricsApiResponse = await queryAllData(client, metricsApiRequest);
+ return copyMissingMetrics(
+ transformMetricsApiResponseToSnapshotResponse(
+ metricsApiRequest,
+ snapshotRequest,
+ source,
+ metricsApiResponse
+ )
+ );
+};
diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/query_all_data.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/query_all_data.ts
new file mode 100644
index 0000000000000..a9d2352cf55b7
--- /dev/null
+++ b/x-pack/plugins/infra/server/routes/snapshot/lib/query_all_data.ts
@@ -0,0 +1,33 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { MetricsAPIRequest, MetricsAPIResponse } from '../../../../common/http_api';
+import { ESSearchClient } from '../../../lib/metrics/types';
+import { query } from '../../../lib/metrics';
+
+const handleResponse = (
+ client: ESSearchClient,
+ options: MetricsAPIRequest,
+ previousResponse?: MetricsAPIResponse
+) => async (resp: MetricsAPIResponse): Promise => {
+ const combinedResponse = previousResponse
+ ? {
+ ...previousResponse,
+ series: [...previousResponse.series, ...resp.series],
+ info: resp.info,
+ }
+ : resp;
+ if (resp.info.afterKey) {
+ return query(client, { ...options, afterKey: resp.info.afterKey }).then(
+ handleResponse(client, options, combinedResponse)
+ );
+ }
+ return combinedResponse;
+};
+
+export const queryAllData = (client: ESSearchClient, options: MetricsAPIRequest) => {
+ return query(client, options).then(handleResponse(client, options));
+};
diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts
new file mode 100644
index 0000000000000..700f4ef39bb66
--- /dev/null
+++ b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts
@@ -0,0 +1,84 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { findInventoryFields } from '../../../../common/inventory_models';
+import { MetricsAPIRequest, SnapshotRequest } from '../../../../common/http_api';
+import { ESSearchClient } from '../../../lib/metrics/types';
+import { InfraSource } from '../../../lib/sources';
+import { createTimeRangeWithInterval } from './create_timerange_with_interval';
+import { parseFilterQuery } from '../../../utils/serialized_query';
+import { transformSnapshotMetricsToMetricsAPIMetrics } from './transform_snapshot_metrics_to_metrics_api_metrics';
+import { calculateIndexPatterBasedOnMetrics } from './calculate_index_pattern_based_on_metrics';
+import { META_KEY } from './constants';
+
+export const transformRequestToMetricsAPIRequest = async (
+ client: ESSearchClient,
+ source: InfraSource,
+ snapshotRequest: SnapshotRequest
+): Promise => {
+ const timeRangeWithIntervalApplied = await createTimeRangeWithInterval(client, {
+ ...snapshotRequest,
+ filterQuery: parseFilterQuery(snapshotRequest.filterQuery),
+ sourceConfiguration: source.configuration,
+ });
+
+ const metricsApiRequest: MetricsAPIRequest = {
+ indexPattern: calculateIndexPatterBasedOnMetrics(snapshotRequest, source),
+ timerange: {
+ field: source.configuration.fields.timestamp,
+ from: timeRangeWithIntervalApplied.from,
+ to: timeRangeWithIntervalApplied.to,
+ interval: timeRangeWithIntervalApplied.interval,
+ },
+ metrics: transformSnapshotMetricsToMetricsAPIMetrics(snapshotRequest),
+ limit: snapshotRequest.overrideCompositeSize ? snapshotRequest.overrideCompositeSize : 10,
+ alignDataToEnd: true,
+ };
+
+ const filters = [];
+ const parsedFilters = parseFilterQuery(snapshotRequest.filterQuery);
+ if (parsedFilters) {
+ filters.push(parsedFilters);
+ }
+
+ if (snapshotRequest.accountId) {
+ filters.push({ term: { 'cloud.account.id': snapshotRequest.accountId } });
+ }
+
+ if (snapshotRequest.region) {
+ filters.push({ term: { 'cloud.region': snapshotRequest.region } });
+ }
+
+ const inventoryFields = findInventoryFields(
+ snapshotRequest.nodeType,
+ source.configuration.fields
+ );
+ const groupBy = snapshotRequest.groupBy.map((g) => g.field).filter(Boolean) as string[];
+ metricsApiRequest.groupBy = [...groupBy, inventoryFields.id];
+
+ const metaAggregation = {
+ id: META_KEY,
+ aggregations: {
+ [META_KEY]: {
+ top_hits: {
+ size: 1,
+ _source: [inventoryFields.name],
+ sort: [{ [source.configuration.fields.timestamp]: 'desc' }],
+ },
+ },
+ },
+ };
+ if (inventoryFields.ip) {
+ metaAggregation.aggregations[META_KEY].top_hits._source.push(inventoryFields.ip);
+ }
+ metricsApiRequest.metrics.push(metaAggregation);
+
+ if (filters.length) {
+ metricsApiRequest.filters = filters;
+ }
+
+ return metricsApiRequest;
+};
diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/transform_snapshot_metrics_to_metrics_api_metrics.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_snapshot_metrics_to_metrics_api_metrics.ts
new file mode 100644
index 0000000000000..6f7c88eda5d7a
--- /dev/null
+++ b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_snapshot_metrics_to_metrics_api_metrics.ts
@@ -0,0 +1,38 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { networkTraffic } from '../../../../common/inventory_models/shared/metrics/snapshot/network_traffic';
+import { findInventoryModel } from '../../../../common/inventory_models';
+import {
+ MetricsAPIMetric,
+ SnapshotRequest,
+ SnapshotCustomMetricInputRT,
+} from '../../../../common/http_api';
+
+export const transformSnapshotMetricsToMetricsAPIMetrics = (
+ snapshotRequest: SnapshotRequest
+): MetricsAPIMetric[] => {
+ return snapshotRequest.metrics.map((metric, index) => {
+ const inventoryModel = findInventoryModel(snapshotRequest.nodeType);
+ if (SnapshotCustomMetricInputRT.is(metric)) {
+ const customId = `custom_${index}`;
+ if (metric.aggregation === 'rate') {
+ return { id: customId, aggregations: networkTraffic(customId, metric.field) };
+ }
+ return {
+ id: customId,
+ aggregations: {
+ [customId]: {
+ [metric.aggregation]: {
+ field: metric.field,
+ },
+ },
+ },
+ };
+ }
+ return { id: metric.type, aggregations: inventoryModel.metrics.snapshot?.[metric.type] };
+ });
+};
diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/trasform_metrics_ui_response.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/trasform_metrics_ui_response.ts
new file mode 100644
index 0000000000000..309598d71c361
--- /dev/null
+++ b/x-pack/plugins/infra/server/routes/snapshot/lib/trasform_metrics_ui_response.ts
@@ -0,0 +1,87 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { get, max, sum, last, isNumber } from 'lodash';
+import { SnapshotMetricType } from '../../../../common/inventory_models/types';
+import {
+ MetricsAPIResponse,
+ SnapshotNodeResponse,
+ MetricsAPIRequest,
+ MetricsExplorerColumnType,
+ MetricsAPIRow,
+ SnapshotRequest,
+ SnapshotNodePath,
+ SnapshotNodeMetric,
+} from '../../../../common/http_api';
+import { META_KEY } from './constants';
+import { InfraSource } from '../../../lib/sources';
+import { applyMetadataToLastPath } from './apply_metadata_to_last_path';
+
+const getMetricValue = (row: MetricsAPIRow) => {
+ if (!isNumber(row.metric_0)) return null;
+ const value = row.metric_0;
+ return isFinite(value) ? value : null;
+};
+
+const calculateMax = (rows: MetricsAPIRow[]) => {
+ return max(rows.map(getMetricValue)) || 0;
+};
+
+const calculateAvg = (rows: MetricsAPIRow[]): number => {
+ return sum(rows.map(getMetricValue)) / rows.length || 0;
+};
+
+const getLastValue = (rows: MetricsAPIRow[]) => {
+ const row = last(rows);
+ if (!row) return null;
+ return getMetricValue(row);
+};
+
+export const transformMetricsApiResponseToSnapshotResponse = (
+ options: MetricsAPIRequest,
+ snapshotRequest: SnapshotRequest,
+ source: InfraSource,
+ metricsApiResponse: MetricsAPIResponse
+): SnapshotNodeResponse => {
+ const nodes = metricsApiResponse.series.map((series) => {
+ const node = {
+ metrics: options.metrics
+ .filter((m) => m.id !== META_KEY)
+ .map((metric) => {
+ const name = metric.id as SnapshotMetricType;
+ const timeseries = {
+ id: name,
+ columns: [
+ { name: 'timestamp', type: 'date' as MetricsExplorerColumnType },
+ { name: 'metric_0', type: 'number' as MetricsExplorerColumnType },
+ ],
+ rows: series.rows.map((row) => {
+ return { timestamp: row.timestamp, metric_0: get(row, metric.id, null) };
+ }),
+ };
+ const maxValue = calculateMax(timeseries.rows);
+ const avg = calculateAvg(timeseries.rows);
+ const value = getLastValue(timeseries.rows);
+ const nodeMetric: SnapshotNodeMetric = { name, max: maxValue, value, avg };
+ if (snapshotRequest.includeTimeseries) {
+ nodeMetric.timeseries = timeseries;
+ }
+ return nodeMetric;
+ }),
+ path:
+ series.keys?.map((key) => {
+ return { value: key, label: key } as SnapshotNodePath;
+ }) ?? [],
+ name: '',
+ };
+
+ const path = applyMetadataToLastPath(series, node, snapshotRequest, source);
+ const lastPath = last(path);
+ const name = (lastPath && lastPath.label) || 'N/A';
+ return { ...node, path, name };
+ });
+ return { nodes, interval: `${metricsApiResponse.info.interval}s` };
+};
diff --git a/x-pack/plugins/infra/server/utils/calculate_metric_interval.ts b/x-pack/plugins/infra/server/utils/calculate_metric_interval.ts
index a3d674b324ae8..6d16e045d26d5 100644
--- a/x-pack/plugins/infra/server/utils/calculate_metric_interval.ts
+++ b/x-pack/plugins/infra/server/utils/calculate_metric_interval.ts
@@ -8,7 +8,7 @@
import { findInventoryModel } from '../../common/inventory_models';
// import { KibanaFramework } from '../lib/adapters/framework/kibana_framework_adapter';
import { InventoryItemType } from '../../common/inventory_models/types';
-import { ESSearchClient } from '../lib/snapshot';
+import { ESSearchClient } from '../lib/metrics/types';
interface Options {
indexPattern: string;
diff --git a/x-pack/test/api_integration/apis/metrics_ui/snapshot.ts b/x-pack/test/api_integration/apis/metrics_ui/snapshot.ts
index bb0934b73a4c7..7339c142fb028 100644
--- a/x-pack/test/api_integration/apis/metrics_ui/snapshot.ts
+++ b/x-pack/test/api_integration/apis/metrics_ui/snapshot.ts
@@ -67,7 +67,6 @@ export default function ({ getService }: FtrProviderContext) {
'value',
'242fddb9d376bbf0e38025d81764847ee5ec0308adfa095918fd3266f9d06c6a'
);
- expect(first(firstNode.path)).to.have.property('label', 'docker-autodiscovery_nginx_1');
expect(firstNode).to.have.property('metrics');
expect(firstNode.metrics).to.eql([
{
@@ -136,7 +135,7 @@ export default function ({ getService }: FtrProviderContext) {
expect(snapshot).to.have.property('nodes');
if (snapshot) {
const { nodes } = snapshot;
- expect(nodes.length).to.equal(136);
+ expect(nodes.length).to.equal(135);
const firstNode = first(nodes) as any;
expect(firstNode).to.have.property('path');
expect(firstNode.path.length).to.equal(1);
@@ -295,7 +294,7 @@ export default function ({ getService }: FtrProviderContext) {
expect(firstNode).to.have.property('metrics');
expect(firstNode.metrics).to.eql([
{
- name: 'custom',
+ name: 'custom_0',
value: 0.0016,
max: 0.0018333333333333333,
avg: 0.0013666666666666669,
From 86582a5501de102162132e475c3d9f4178b4f4e3 Mon Sep 17 00:00:00 2001
From: James Rodewig <40268737+jrodewig@users.noreply.github.com>
Date: Wed, 9 Sep 2020 12:01:28 -0400
Subject: [PATCH 06/25] [Ingest Pipelines] Add descriptions for ingest
processors K-S (#76981)
---
.../manage_processor_form/processors/kv.tsx | 38 +++++++++------
.../processors/lowercase.tsx | 14 +-----
.../processors/pipeline.tsx | 2 +-
.../processors/remove.tsx | 2 +-
.../processors/rename.tsx | 4 +-
.../processors/script.tsx | 8 ++--
.../manage_processor_form/processors/set.tsx | 21 ++++++---
.../processors/set_security_user.tsx | 4 +-
.../manage_processor_form/processors/sort.tsx | 5 +-
.../processors/split.tsx | 6 +--
.../shared/map_processor_type_to_form.tsx | 47 +++++++++++++++----
11 files changed, 94 insertions(+), 57 deletions(-)
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/kv.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/kv.tsx
index f51bf19ad180a..4104e8f727ab1 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/kv.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/kv.tsx
@@ -33,9 +33,15 @@ const fieldsConfig: FieldsConfig = {
label: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.fieldSplitFieldLabel', {
defaultMessage: 'Field split',
}),
- helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.fieldSplitHelpText', {
- defaultMessage: 'Regex pattern for splitting key-value pairs.',
- }),
+ helpText: (
+ {'" "'},
+ }}
+ />
+ ),
validations: [
{
validator: emptyField(
@@ -52,9 +58,15 @@ const fieldsConfig: FieldsConfig = {
label: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.valueSplitFieldLabel', {
defaultMessage: 'Value split',
}),
- helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.valueSplitHelpText', {
- defaultMessage: 'Regex pattern for splitting the key from the value within a key-value pair.',
- }),
+ helpText: (
+ {'"="'},
+ }}
+ />
+ ),
validations: [
{
validator: emptyField(
@@ -75,8 +87,7 @@ const fieldsConfig: FieldsConfig = {
defaultMessage: 'Include keys',
}),
helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.includeKeysHelpText', {
- defaultMessage:
- 'List of keys to filter and insert into document. Defaults to including all keys.',
+ defaultMessage: 'List of extracted keys to include in the output. Defaults to all keys.',
}),
},
@@ -88,7 +99,7 @@ const fieldsConfig: FieldsConfig = {
defaultMessage: 'Exclude keys',
}),
helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.excludeKeysHelpText', {
- defaultMessage: 'List of keys to exclude from document.',
+ defaultMessage: 'List of extracted keys to exclude from the output.',
}),
},
@@ -99,7 +110,7 @@ const fieldsConfig: FieldsConfig = {
defaultMessage: 'Prefix',
}),
helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.prefixHelpText', {
- defaultMessage: 'Prefix to be added to extracted keys.',
+ defaultMessage: 'Prefix to add to extracted keys.',
}),
},
@@ -136,7 +147,7 @@ const fieldsConfig: FieldsConfig = {
helpText: (
{'()'},
angle: <>,
@@ -154,7 +165,7 @@ export const Kv: FunctionComponent = () => {
<>
@@ -166,8 +177,7 @@ export const Kv: FunctionComponent = () => {
helpText={i18n.translate(
'xpack.ingestPipelines.pipelineEditor.kvForm.targetFieldHelpText',
{
- defaultMessage:
- 'Field to insert the extracted keys into. Defaults to the root of the document.',
+ defaultMessage: 'Output field for the extracted fields. Defaults to the document root.',
}
)}
/>
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/lowercase.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/lowercase.tsx
index 9db313a05007f..0d8170338ea10 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/lowercase.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/lowercase.tsx
@@ -6,8 +6,6 @@
import React, { FunctionComponent } from 'react';
import { i18n } from '@kbn/i18n';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiCode } from '@elastic/eui';
import { FieldNameField } from './common_fields/field_name_field';
import { TargetField } from './common_fields/target_field';
@@ -23,17 +21,7 @@ export const Lowercase: FunctionComponent = () => {
)}
/>
- {'field'},
- }}
- />
- }
- />
+
>
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/pipeline.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/pipeline.tsx
index c785cf935833d..57843e2411359 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/pipeline.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/pipeline.tsx
@@ -27,7 +27,7 @@ const fieldsConfig: FieldsConfig = {
helpText: i18n.translate(
'xpack.ingestPipelines.pipelineEditor.pipelineForm.pipelineNameFieldHelpText',
{
- defaultMessage: 'Name of the pipeline to execute.',
+ defaultMessage: 'Name of the ingest pipeline to run.',
}
),
validations: [
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/remove.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/remove.tsx
index 3e90ce2b76f7b..3ba1cdb0c802d 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/remove.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/remove.tsx
@@ -29,7 +29,7 @@ const fieldsConfig: FieldsConfig = {
defaultMessage: 'Fields',
}),
helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.removeForm.fieldNameHelpText', {
- defaultMessage: 'Fields to be removed.',
+ defaultMessage: 'Fields to remove.',
}),
validations: [
{
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/rename.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/rename.tsx
index 8b796d9664586..099e2bd2c80fb 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/rename.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/rename.tsx
@@ -21,7 +21,7 @@ export const Rename: FunctionComponent = () => {
@@ -31,7 +31,7 @@ export const Rename: FunctionComponent = () => {
})}
helpText={i18n.translate(
'xpack.ingestPipelines.pipelineEditor.renameForm.targetFieldHelpText',
- { defaultMessage: 'Name of the new field.' }
+ { defaultMessage: 'New field name. This field cannot already exist.' }
)}
validations={[
{
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/script.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/script.tsx
index ae0bbbb490ae9..de28f66766603 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/script.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/script.tsx
@@ -32,7 +32,7 @@ const fieldsConfig: FieldsConfig = {
helpText: i18n.translate(
'xpack.ingestPipelines.pipelineEditor.scriptForm.storedScriptIDFieldHelpText',
{
- defaultMessage: 'Stored script reference.',
+ defaultMessage: 'ID of the stored script to run.',
}
),
validations: [
@@ -55,7 +55,7 @@ const fieldsConfig: FieldsConfig = {
helpText: i18n.translate(
'xpack.ingestPipelines.pipelineEditor.scriptForm.sourceFieldHelpText',
{
- defaultMessage: 'Script to be executed.',
+ defaultMessage: 'Inline script to run.',
}
),
validations: [
@@ -98,7 +98,7 @@ const fieldsConfig: FieldsConfig = {
helpText: i18n.translate(
'xpack.ingestPipelines.pipelineEditor.scriptForm.paramsFieldHelpText',
{
- defaultMessage: 'Script parameters.',
+ defaultMessage: 'Named parameters passed to the script as variables.',
}
),
validations: [
@@ -128,7 +128,7 @@ export const Script: FormFieldsComponent = ({ initialFieldValues }) => {
setShowId((v) => !v)}
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set.tsx
index c282be35e5071..04ea0c44c3513 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set.tsx
@@ -32,13 +32,13 @@ const fieldsConfig: FieldsConfig = {
defaultMessage: 'Value',
}),
helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.valueFieldHelpText', {
- defaultMessage: 'Value to be set for the field',
+ defaultMessage: 'Value for the field.',
}),
validations: [
{
validator: emptyField(
i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.valueRequiredError', {
- defaultMessage: 'A value is required',
+ defaultMessage: 'A value is required.',
})
),
},
@@ -53,9 +53,15 @@ const fieldsConfig: FieldsConfig = {
label: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.overrideFieldLabel', {
defaultMessage: 'Override',
}),
- helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.overrideFieldHelpText', {
- defaultMessage: 'If disabled, fields containing non-null values will not be updated.',
- }),
+ helpText: (
+ {'null'},
+ }}
+ />
+ ),
},
ignore_empty_value: {
type: FIELD_TYPES.TOGGLE,
@@ -71,7 +77,8 @@ const fieldsConfig: FieldsConfig = {
helpText: (
{'value'},
nullValue: {'null'},
@@ -89,7 +96,7 @@ export const SetProcessor: FunctionComponent = () => {
<>
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set_security_user.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set_security_user.tsx
index 78128b3d54c75..46bfe8c97ebea 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set_security_user.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set_security_user.tsx
@@ -44,7 +44,7 @@ const fieldsConfig: FieldsConfig = {
helpText: (
[{helpTextValues}],
}}
@@ -60,7 +60,7 @@ export const SetSecurityUser: FunctionComponent = () => {
helpText={i18n.translate(
'xpack.ingestPipelines.pipelineEditor.setSecurityUserForm.fieldNameField',
{
- defaultMessage: 'Field to store the user information',
+ defaultMessage: 'Output field.',
}
)}
/>
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/sort.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/sort.tsx
index cdd0ff888accf..c8c0562011fd6 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/sort.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/sort.tsx
@@ -24,7 +24,8 @@ const fieldsConfig: FieldsConfig = {
defaultMessage: 'Order',
}),
helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.sortForm.orderFieldHelpText', {
- defaultMessage: 'Sort order to use',
+ defaultMessage:
+ 'Sort order. Arrays containing a mix of strings and numbers are sorted lexicographically.',
}),
},
};
@@ -35,7 +36,7 @@ export const Sort: FunctionComponent = () => {
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/split.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/split.tsx
index b48ce74110b39..fa178aaddd314 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/split.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/split.tsx
@@ -33,7 +33,7 @@ const fieldsConfig: FieldsConfig = {
helpText: i18n.translate(
'xpack.ingestPipelines.pipelineEditor.splitForm.separatorFieldHelpText',
{
- defaultMessage: 'Regex to match a separator',
+ defaultMessage: 'Regex pattern used to delimit the field value.',
}
),
validations: [
@@ -60,7 +60,7 @@ const fieldsConfig: FieldsConfig = {
),
helpText: i18n.translate(
'xpack.ingestPipelines.pipelineEditor.splitForm.preserveTrailingFieldHelpText',
- { defaultMessage: 'If enabled, preserve any trailing space.' }
+ { defaultMessage: 'Preserve any trailing whitespace in the split field values.' }
),
},
};
@@ -71,7 +71,7 @@ export const Split: FunctionComponent = () => {
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx
index 59ec64944a3c9..9de371f8d0024 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx
@@ -107,7 +107,7 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
defaultMessage: 'CSV',
}),
description: i18n.translate('xpack.ingestPipelines.processors.description.csv', {
- defaultMessage: 'Extracts fields values from CSV data.',
+ defaultMessage: 'Extracts field values from CSV data.',
}),
},
date: {
@@ -306,7 +306,10 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
FieldsComponent: Kv,
docLinkPath: '/kv-processor.html',
label: i18n.translate('xpack.ingestPipelines.processors.label.kv', {
- defaultMessage: 'KV',
+ defaultMessage: 'Key-value (KV)',
+ }),
+ description: i18n.translate('xpack.ingestPipelines.processors.description.kv', {
+ defaultMessage: 'Extracts fields from a string containing key-value pairs.',
}),
},
lowercase: {
@@ -315,6 +318,9 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
label: i18n.translate('xpack.ingestPipelines.processors.label.lowercase', {
defaultMessage: 'Lowercase',
}),
+ description: i18n.translate('xpack.ingestPipelines.processors.description.lowercase', {
+ defaultMessage: 'Converts a string to lowercase.',
+ }),
},
pipeline: {
FieldsComponent: Pipeline,
@@ -322,6 +328,9 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
label: i18n.translate('xpack.ingestPipelines.processors.label.pipeline', {
defaultMessage: 'Pipeline',
}),
+ description: i18n.translate('xpack.ingestPipelines.processors.description.pipeline', {
+ defaultMessage: 'Runs another ingest node pipeline.',
+ }),
},
remove: {
FieldsComponent: Remove,
@@ -329,6 +338,9 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
label: i18n.translate('xpack.ingestPipelines.processors.label.remove', {
defaultMessage: 'Remove',
}),
+ description: i18n.translate('xpack.ingestPipelines.processors.description.remove', {
+ defaultMessage: 'Removes one or more fields.',
+ }),
},
rename: {
FieldsComponent: Rename,
@@ -336,6 +348,9 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
label: i18n.translate('xpack.ingestPipelines.processors.label.rename', {
defaultMessage: 'Rename',
}),
+ description: i18n.translate('xpack.ingestPipelines.processors.description.rename', {
+ defaultMessage: 'Renames an existing field.',
+ }),
},
script: {
FieldsComponent: Script,
@@ -343,6 +358,9 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
label: i18n.translate('xpack.ingestPipelines.processors.label.script', {
defaultMessage: 'Script',
}),
+ description: i18n.translate('xpack.ingestPipelines.processors.description.script', {
+ defaultMessage: 'Runs a script on incoming documents.',
+ }),
},
set: {
FieldsComponent: SetProcessor,
@@ -350,6 +368,9 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
label: i18n.translate('xpack.ingestPipelines.processors.label.set', {
defaultMessage: 'Set',
}),
+ description: i18n.translate('xpack.ingestPipelines.processors.description.set', {
+ defaultMessage: 'Sets the value of a field.',
+ }),
},
set_security_user: {
FieldsComponent: SetSecurityUser,
@@ -357,12 +378,9 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
label: i18n.translate('xpack.ingestPipelines.processors.label.setSecurityUser', {
defaultMessage: 'Set security user',
}),
- },
- split: {
- FieldsComponent: Split,
- docLinkPath: '/split-processor.html',
- label: i18n.translate('xpack.ingestPipelines.processors.label.split', {
- defaultMessage: 'Split',
+ description: i18n.translate('xpack.ingestPipelines.processors.description.setSecurityUser', {
+ defaultMessage:
+ 'Adds details about the current user, such user name and email address, to incoming documents. Requires an authenticated user for the indexing request.',
}),
},
sort: {
@@ -371,6 +389,19 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = {
label: i18n.translate('xpack.ingestPipelines.processors.label.sort', {
defaultMessage: 'Sort',
}),
+ description: i18n.translate('xpack.ingestPipelines.processors.description.sort', {
+ defaultMessage: "Sorts a field's array elements.",
+ }),
+ },
+ split: {
+ FieldsComponent: Split,
+ docLinkPath: '/split-processor.html',
+ label: i18n.translate('xpack.ingestPipelines.processors.label.split', {
+ defaultMessage: 'Split',
+ }),
+ description: i18n.translate('xpack.ingestPipelines.processors.description.split', {
+ defaultMessage: 'Splits a field value into an array.',
+ }),
},
trim: {
FieldsComponent: undefined, // TODO: Implement
From 20b2e31debf59ee0a0d368fd5ab4f4c790f94c39 Mon Sep 17 00:00:00 2001
From: Thomas Watson
Date: Wed, 9 Sep 2020 18:18:25 +0200
Subject: [PATCH 07/25] Bump eventemitter3 from 4.0.0 to 4.0.7 (#77016)
---
yarn.lock | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/yarn.lock b/yarn.lock
index 29f99c25b7730..105c5e3cba5ae 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -12288,9 +12288,9 @@ eventemitter2@~0.4.13:
integrity sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=
eventemitter3@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.0.tgz#d65176163887ee59f386d64c82610b696a4a74eb"
- integrity sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg==
+ version "4.0.7"
+ resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
+ integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
events@^1.0.2:
version "1.1.1"
From 619108cac345ed02928e96e54a84cefef2ddab72 Mon Sep 17 00:00:00 2001
From: Quynh Nguyen <43350163+qn895@users.noreply.github.com>
Date: Wed, 9 Sep 2020 11:41:52 -0500
Subject: [PATCH 08/25] [ML] Add decision path charts to exploration results
table (#73561)
Co-authored-by: Elastic Machine
---
.../common/constants/data_frame_analytics.ts | 7 +
.../ml/common/types/data_frame_analytics.ts | 6 +
.../ml/common/types/feature_importance.ts | 23 +++
.../plugins/ml/common/util/analytics_utils.ts | 79 ++++++++
.../components/data_grid/common.ts | 13 +-
.../components/data_grid/data_grid.tsx | 58 +++++-
.../decision_path_chart.tsx | 166 +++++++++++++++++
.../decision_path_classification.tsx | 105 +++++++++++
.../decision_path_json_viewer.tsx | 16 ++
.../decision_path_popover.tsx | 134 ++++++++++++++
.../decision_path_regression.tsx | 79 ++++++++
.../missing_decision_path_callout.tsx | 20 ++
.../use_classification_path_data.tsx | 173 ++++++++++++++++++
.../application/components/data_grid/types.ts | 6 +
.../data_frame_analytics/common/analytics.ts | 91 ++-------
.../data_frame_analytics/common/constants.ts | 2 -
.../data_frame_analytics/common/fields.ts | 7 +-
.../classification_exploration.tsx | 1 -
.../exploration_page_wrapper.tsx | 1 -
.../exploration_results_table.tsx | 16 +-
.../use_exploration_results.ts | 68 ++++++-
.../outlier_exploration/use_outlier_data.ts | 3 +-
.../action_clone/clone_action_name.tsx | 2 +-
.../ml_api_service/data_frame_analytics.ts | 6 +
.../feature_importance.ts | 69 +++++++
.../ml/server/routes/data_frame_analytics.ts | 35 ++++
.../ml/data_frame_analytics_creation.ts | 22 +--
27 files changed, 1083 insertions(+), 125 deletions(-)
create mode 100644 x-pack/plugins/ml/common/constants/data_frame_analytics.ts
create mode 100644 x-pack/plugins/ml/common/types/feature_importance.ts
create mode 100644 x-pack/plugins/ml/common/util/analytics_utils.ts
create mode 100644 x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx
create mode 100644 x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_classification.tsx
create mode 100644 x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_json_viewer.tsx
create mode 100644 x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_popover.tsx
create mode 100644 x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_regression.tsx
create mode 100644 x-pack/plugins/ml/public/application/components/data_grid/feature_importance/missing_decision_path_callout.tsx
create mode 100644 x-pack/plugins/ml/public/application/components/data_grid/feature_importance/use_classification_path_data.tsx
create mode 100644 x-pack/plugins/ml/server/models/data_frame_analytics/feature_importance.ts
diff --git a/x-pack/plugins/ml/common/constants/data_frame_analytics.ts b/x-pack/plugins/ml/common/constants/data_frame_analytics.ts
new file mode 100644
index 0000000000000..830537cbadbc8
--- /dev/null
+++ b/x-pack/plugins/ml/common/constants/data_frame_analytics.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export const DEFAULT_RESULTS_FIELD = 'ml';
diff --git a/x-pack/plugins/ml/common/types/data_frame_analytics.ts b/x-pack/plugins/ml/common/types/data_frame_analytics.ts
index f0aac75047585..60d2ca63dda59 100644
--- a/x-pack/plugins/ml/common/types/data_frame_analytics.ts
+++ b/x-pack/plugins/ml/common/types/data_frame_analytics.ts
@@ -79,3 +79,9 @@ export interface DataFrameAnalyticsConfig {
version: string;
allow_lazy_start?: boolean;
}
+
+export enum ANALYSIS_CONFIG_TYPE {
+ OUTLIER_DETECTION = 'outlier_detection',
+ REGRESSION = 'regression',
+ CLASSIFICATION = 'classification',
+}
diff --git a/x-pack/plugins/ml/common/types/feature_importance.ts b/x-pack/plugins/ml/common/types/feature_importance.ts
new file mode 100644
index 0000000000000..d2ab9f6c58608
--- /dev/null
+++ b/x-pack/plugins/ml/common/types/feature_importance.ts
@@ -0,0 +1,23 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export interface ClassFeatureImportance {
+ class_name: string | boolean;
+ importance: number;
+}
+export interface FeatureImportance {
+ feature_name: string;
+ importance?: number;
+ classes?: ClassFeatureImportance[];
+}
+
+export interface TopClass {
+ class_name: string;
+ class_probability: number;
+ class_score: number;
+}
+
+export type TopClasses = TopClass[];
diff --git a/x-pack/plugins/ml/common/util/analytics_utils.ts b/x-pack/plugins/ml/common/util/analytics_utils.ts
new file mode 100644
index 0000000000000..d725984a47d66
--- /dev/null
+++ b/x-pack/plugins/ml/common/util/analytics_utils.ts
@@ -0,0 +1,79 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import {
+ AnalysisConfig,
+ ClassificationAnalysis,
+ OutlierAnalysis,
+ RegressionAnalysis,
+ ANALYSIS_CONFIG_TYPE,
+} from '../types/data_frame_analytics';
+
+export const isOutlierAnalysis = (arg: any): arg is OutlierAnalysis => {
+ const keys = Object.keys(arg);
+ return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION;
+};
+
+export const isRegressionAnalysis = (arg: any): arg is RegressionAnalysis => {
+ const keys = Object.keys(arg);
+ return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.REGRESSION;
+};
+
+export const isClassificationAnalysis = (arg: any): arg is ClassificationAnalysis => {
+ const keys = Object.keys(arg);
+ return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.CLASSIFICATION;
+};
+
+export const getDependentVar = (
+ analysis: AnalysisConfig
+):
+ | RegressionAnalysis['regression']['dependent_variable']
+ | ClassificationAnalysis['classification']['dependent_variable'] => {
+ let depVar = '';
+
+ if (isRegressionAnalysis(analysis)) {
+ depVar = analysis.regression.dependent_variable;
+ }
+
+ if (isClassificationAnalysis(analysis)) {
+ depVar = analysis.classification.dependent_variable;
+ }
+ return depVar;
+};
+
+export const getPredictionFieldName = (
+ analysis: AnalysisConfig
+):
+ | RegressionAnalysis['regression']['prediction_field_name']
+ | ClassificationAnalysis['classification']['prediction_field_name'] => {
+ // If undefined will be defaulted to dependent_variable when config is created
+ let predictionFieldName;
+ if (isRegressionAnalysis(analysis) && analysis.regression.prediction_field_name !== undefined) {
+ predictionFieldName = analysis.regression.prediction_field_name;
+ } else if (
+ isClassificationAnalysis(analysis) &&
+ analysis.classification.prediction_field_name !== undefined
+ ) {
+ predictionFieldName = analysis.classification.prediction_field_name;
+ }
+ return predictionFieldName;
+};
+
+export const getDefaultPredictionFieldName = (analysis: AnalysisConfig) => {
+ return `${getDependentVar(analysis)}_prediction`;
+};
+export const getPredictedFieldName = (
+ resultsField: string,
+ analysis: AnalysisConfig,
+ forSort?: boolean
+) => {
+ // default is 'ml'
+ const predictionFieldName = getPredictionFieldName(analysis);
+ const predictedField = `${resultsField}.${
+ predictionFieldName ? predictionFieldName : getDefaultPredictionFieldName(analysis)
+ }`;
+ return predictedField;
+};
diff --git a/x-pack/plugins/ml/public/application/components/data_grid/common.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.ts
index 1f0fcb63f019d..f252729cc20cd 100644
--- a/x-pack/plugins/ml/public/application/components/data_grid/common.ts
+++ b/x-pack/plugins/ml/public/application/components/data_grid/common.ts
@@ -119,13 +119,14 @@ export const getDataGridSchemasFromFieldTypes = (fieldTypes: FieldTypes, results
schema = 'numeric';
}
- if (
- field.includes(`${resultsField}.${FEATURE_IMPORTANCE}`) ||
- field.includes(`${resultsField}.${TOP_CLASSES}`)
- ) {
+ if (field.includes(`${resultsField}.${TOP_CLASSES}`)) {
schema = 'json';
}
+ if (field.includes(`${resultsField}.${FEATURE_IMPORTANCE}`)) {
+ schema = 'featureImportance';
+ }
+
return { id: field, schema, isSortable };
});
};
@@ -250,10 +251,6 @@ export const useRenderCellValue = (
return cellValue ? 'true' : 'false';
}
- if (typeof cellValue === 'object' && cellValue !== null) {
- return JSON.stringify(cellValue);
- }
-
return cellValue;
};
}, [indexPattern?.fields, pagination.pageIndex, pagination.pageSize, tableItems]);
diff --git a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx
index d4be2eab13d26..22815fe593d57 100644
--- a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx
+++ b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx
@@ -5,8 +5,7 @@
*/
import { isEqual } from 'lodash';
-import React, { memo, useEffect, FC } from 'react';
-
+import React, { memo, useEffect, FC, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import {
@@ -24,13 +23,16 @@ import {
} from '@elastic/eui';
import { CoreSetup } from 'src/core/public';
-
import { DEFAULT_SAMPLER_SHARD_SIZE } from '../../../../common/constants/field_histograms';
-import { INDEX_STATUS } from '../../data_frame_analytics/common';
+import { ANALYSIS_CONFIG_TYPE, INDEX_STATUS } from '../../data_frame_analytics/common';
import { euiDataGridStyle, euiDataGridToolbarSettings } from './common';
import { UseIndexDataReturnType } from './types';
+import { DecisionPathPopover } from './feature_importance/decision_path_popover';
+import { TopClasses } from '../../../../common/types/feature_importance';
+import { DEFAULT_RESULTS_FIELD } from '../../../../common/constants/data_frame_analytics';
+
// TODO Fix row hovering + bar highlighting
// import { hoveredRow$ } from './column_chart';
@@ -41,6 +43,9 @@ export const DataGridTitle: FC<{ title: string }> = ({ title }) => (
);
interface PropsWithoutHeader extends UseIndexDataReturnType {
+ baseline?: number;
+ analysisType?: ANALYSIS_CONFIG_TYPE;
+ resultsField?: string;
dataTestSubj: string;
toastNotifications: CoreSetup['notifications']['toasts'];
}
@@ -60,6 +65,7 @@ type Props = PropsWithHeader | PropsWithoutHeader;
export const DataGrid: FC = memo(
(props) => {
const {
+ baseline,
chartsVisible,
chartsButtonVisible,
columnsWithCharts,
@@ -80,8 +86,10 @@ export const DataGrid: FC = memo(
toastNotifications,
toggleChartVisibility,
visibleColumns,
+ predictionFieldName,
+ resultsField,
+ analysisType,
} = props;
-
// TODO Fix row hovering + bar highlighting
// const getRowProps = (item: any) => {
// return {
@@ -90,6 +98,45 @@ export const DataGrid: FC = memo(
// };
// };
+ const popOverContent = useMemo(() => {
+ return analysisType === ANALYSIS_CONFIG_TYPE.REGRESSION ||
+ analysisType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION
+ ? {
+ featureImportance: ({ children }: { cellContentsElement: any; children: any }) => {
+ const rowIndex = children?.props?.visibleRowIndex;
+ const row = data[rowIndex];
+ if (!row) return ;
+ // if resultsField for some reason is not available then use ml
+ const mlResultsField = resultsField ?? DEFAULT_RESULTS_FIELD;
+ const parsedFIArray = row[mlResultsField].feature_importance;
+ let predictedValue: string | number | undefined;
+ let topClasses: TopClasses = [];
+ if (
+ predictionFieldName !== undefined &&
+ row &&
+ row[mlResultsField][predictionFieldName] !== undefined
+ ) {
+ predictedValue = row[mlResultsField][predictionFieldName];
+ topClasses = row[mlResultsField].top_classes;
+ }
+
+ return (
+
+ );
+ },
+ }
+ : undefined;
+ }, [baseline, data]);
+
useEffect(() => {
if (invalidSortingColumnns.length > 0) {
invalidSortingColumnns.forEach((columnId) => {
@@ -225,6 +272,7 @@ export const DataGrid: FC = memo(
}
: {}),
}}
+ popoverContents={popOverContent}
pagination={{
...pagination,
pageSizeOptions: [5, 10, 25],
diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx
new file mode 100644
index 0000000000000..b546ac1db57dd
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx
@@ -0,0 +1,166 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import {
+ AnnotationDomainTypes,
+ Axis,
+ AxisStyle,
+ Chart,
+ LineAnnotation,
+ LineAnnotationStyle,
+ LineAnnotationDatum,
+ LineSeries,
+ PartialTheme,
+ Position,
+ RecursivePartial,
+ ScaleType,
+ Settings,
+} from '@elastic/charts';
+import { EuiIcon } from '@elastic/eui';
+
+import React, { useCallback, useMemo } from 'react';
+import { i18n } from '@kbn/i18n';
+import euiVars from '@elastic/eui/dist/eui_theme_light.json';
+import { DecisionPathPlotData } from './use_classification_path_data';
+
+const { euiColorFullShade, euiColorMediumShade } = euiVars;
+const axisColor = euiColorMediumShade;
+
+const baselineStyle: LineAnnotationStyle = {
+ line: {
+ strokeWidth: 1,
+ stroke: euiColorFullShade,
+ opacity: 0.75,
+ },
+ details: {
+ fontFamily: 'Arial',
+ fontSize: 10,
+ fontStyle: 'bold',
+ fill: euiColorMediumShade,
+ padding: 0,
+ },
+};
+
+const axes: RecursivePartial = {
+ axisLine: {
+ stroke: axisColor,
+ },
+ tickLabel: {
+ fontSize: 10,
+ fill: axisColor,
+ },
+ tickLine: {
+ stroke: axisColor,
+ },
+ gridLine: {
+ horizontal: {
+ dash: [1, 2],
+ },
+ vertical: {
+ strokeWidth: 0,
+ },
+ },
+};
+const theme: PartialTheme = {
+ axes,
+};
+
+interface DecisionPathChartProps {
+ decisionPathData: DecisionPathPlotData;
+ predictionFieldName?: string;
+ baseline?: number;
+ minDomain: number | undefined;
+ maxDomain: number | undefined;
+}
+
+const DECISION_PATH_MARGIN = 125;
+const DECISION_PATH_ROW_HEIGHT = 10;
+const NUM_PRECISION = 3;
+const AnnotationBaselineMarker = ;
+
+export const DecisionPathChart = ({
+ decisionPathData,
+ predictionFieldName,
+ minDomain,
+ maxDomain,
+ baseline,
+}: DecisionPathChartProps) => {
+ // adjust the height so it's compact for items with more features
+ const baselineData: LineAnnotationDatum[] = useMemo(
+ () => [
+ {
+ dataValue: baseline,
+ header: baseline ? baseline.toPrecision(NUM_PRECISION) : '',
+ details: i18n.translate(
+ 'xpack.ml.dataframe.analytics.explorationResults.decisionPathBaselineText',
+ {
+ defaultMessage:
+ 'baseline (average of predictions for all data points in the training data set)',
+ }
+ ),
+ },
+ ],
+ [baseline]
+ );
+ // guarantee up to num_precision significant digits
+ // without having it in scientific notation
+ const tickFormatter = useCallback((d) => Number(d.toPrecision(NUM_PRECISION)).toString(), []);
+
+ return (
+
+
+ {baseline && (
+
+ )}
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_classification.tsx b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_classification.tsx
new file mode 100644
index 0000000000000..bd001fa81a582
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_classification.tsx
@@ -0,0 +1,105 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FC, useMemo, useState } from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiHealth, EuiSpacer, EuiSuperSelect, EuiTitle } from '@elastic/eui';
+import d3 from 'd3';
+import {
+ isDecisionPathData,
+ useDecisionPathData,
+ getStringBasedClassName,
+} from './use_classification_path_data';
+import { FeatureImportance, TopClasses } from '../../../../../common/types/feature_importance';
+import { DecisionPathChart } from './decision_path_chart';
+import { MissingDecisionPathCallout } from './missing_decision_path_callout';
+
+interface ClassificationDecisionPathProps {
+ predictedValue: string | boolean;
+ predictionFieldName?: string;
+ featureImportance: FeatureImportance[];
+ topClasses: TopClasses;
+}
+
+export const ClassificationDecisionPath: FC = ({
+ featureImportance,
+ predictedValue,
+ topClasses,
+ predictionFieldName,
+}) => {
+ const [currentClass, setCurrentClass] = useState(
+ getStringBasedClassName(topClasses[0].class_name)
+ );
+ const { decisionPathData } = useDecisionPathData({
+ featureImportance,
+ predictedValue: currentClass,
+ });
+ const options = useMemo(() => {
+ const predictionValueStr = getStringBasedClassName(predictedValue);
+
+ return Array.isArray(topClasses)
+ ? topClasses.map((c) => {
+ const className = getStringBasedClassName(c.class_name);
+ return {
+ value: className,
+ inputDisplay:
+ className === predictionValueStr ? (
+
+ {className}
+
+ ) : (
+ className
+ ),
+ };
+ })
+ : undefined;
+ }, [topClasses, predictedValue]);
+
+ const domain = useMemo(() => {
+ let maxDomain;
+ let minDomain;
+ // if decisionPathData has calculated cumulative path
+ if (decisionPathData && isDecisionPathData(decisionPathData)) {
+ const [min, max] = d3.extent(decisionPathData, (d: [string, number, number]) => d[2]);
+ const buffer = Math.abs(max - min) * 0.1;
+ maxDomain = max + buffer;
+ minDomain = min - buffer;
+ }
+ return { maxDomain, minDomain };
+ }, [decisionPathData]);
+
+ if (!decisionPathData) return ;
+
+ return (
+ <>
+
+
+
+ {i18n.translate(
+ 'xpack.ml.dataframe.analytics.explorationResults.classificationDecisionPathClassNameTitle',
+ {
+ defaultMessage: 'Class name',
+ }
+ )}
+
+
+ {options !== undefined && (
+
+ )}
+
+ >
+ );
+};
diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_json_viewer.tsx b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_json_viewer.tsx
new file mode 100644
index 0000000000000..343324b27f9b5
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_json_viewer.tsx
@@ -0,0 +1,16 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FC } from 'react';
+import { EuiCodeBlock } from '@elastic/eui';
+import { FeatureImportance } from '../../../../../common/types/feature_importance';
+
+interface DecisionPathJSONViewerProps {
+ featureImportance: FeatureImportance[];
+}
+export const DecisionPathJSONViewer: FC = ({ featureImportance }) => {
+ return {JSON.stringify(featureImportance)};
+};
diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_popover.tsx b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_popover.tsx
new file mode 100644
index 0000000000000..263337f93e9a8
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_popover.tsx
@@ -0,0 +1,134 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FC, useState } from 'react';
+import { EuiLink, EuiTab, EuiTabs, EuiText } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { RegressionDecisionPath } from './decision_path_regression';
+import { DecisionPathJSONViewer } from './decision_path_json_viewer';
+import { FeatureImportance, TopClasses } from '../../../../../common/types/feature_importance';
+import { ANALYSIS_CONFIG_TYPE } from '../../../data_frame_analytics/common';
+import { ClassificationDecisionPath } from './decision_path_classification';
+import { useMlKibana } from '../../../contexts/kibana';
+
+interface DecisionPathPopoverProps {
+ featureImportance: FeatureImportance[];
+ analysisType: ANALYSIS_CONFIG_TYPE;
+ predictionFieldName?: string;
+ baseline?: number;
+ predictedValue?: number | string | undefined;
+ topClasses?: TopClasses;
+}
+
+enum DECISION_PATH_TABS {
+ CHART = 'decision_path_chart',
+ JSON = 'decision_path_json',
+}
+
+export interface ExtendedFeatureImportance extends FeatureImportance {
+ absImportance?: number;
+}
+
+export const DecisionPathPopover: FC = ({
+ baseline,
+ featureImportance,
+ predictedValue,
+ topClasses,
+ analysisType,
+ predictionFieldName,
+}) => {
+ const [selectedTabId, setSelectedTabId] = useState(DECISION_PATH_TABS.CHART);
+ const {
+ services: { docLinks },
+ } = useMlKibana();
+ const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks;
+
+ if (featureImportance.length < 2) {
+ return ;
+ }
+
+ const tabs = [
+ {
+ id: DECISION_PATH_TABS.CHART,
+ name: (
+
+ ),
+ },
+ {
+ id: DECISION_PATH_TABS.JSON,
+ name: (
+
+ ),
+ },
+ ];
+
+ return (
+ <>
+
+
+ {tabs.map((tab) => (
+ setSelectedTabId(tab.id)}
+ key={tab.id}
+ >
+ {tab.name}
+
+ ))}
+
+
+ {selectedTabId === DECISION_PATH_TABS.CHART && (
+ <>
+
+
+
+
+ ),
+ }}
+ />
+
+ {analysisType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION && (
+
+ )}
+ {analysisType === ANALYSIS_CONFIG_TYPE.REGRESSION && (
+
+ )}
+ >
+ )}
+ {selectedTabId === DECISION_PATH_TABS.JSON && (
+
+ )}
+ >
+ );
+};
diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_regression.tsx b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_regression.tsx
new file mode 100644
index 0000000000000..345269a944f02
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_regression.tsx
@@ -0,0 +1,79 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FC, useMemo } from 'react';
+import { EuiCallOut } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import d3 from 'd3';
+import { FeatureImportance, TopClasses } from '../../../../../common/types/feature_importance';
+import { useDecisionPathData, isDecisionPathData } from './use_classification_path_data';
+import { DecisionPathChart } from './decision_path_chart';
+import { MissingDecisionPathCallout } from './missing_decision_path_callout';
+
+interface RegressionDecisionPathProps {
+ predictionFieldName?: string;
+ baseline?: number;
+ predictedValue?: number | undefined;
+ featureImportance: FeatureImportance[];
+ topClasses?: TopClasses;
+}
+
+export const RegressionDecisionPath: FC = ({
+ baseline,
+ featureImportance,
+ predictedValue,
+ predictionFieldName,
+}) => {
+ const { decisionPathData } = useDecisionPathData({
+ baseline,
+ featureImportance,
+ predictedValue,
+ });
+ const domain = useMemo(() => {
+ let maxDomain;
+ let minDomain;
+ // if decisionPathData has calculated cumulative path
+ if (decisionPathData && isDecisionPathData(decisionPathData)) {
+ const [min, max] = d3.extent(decisionPathData, (d: [string, number, number]) => d[2]);
+ maxDomain = max;
+ minDomain = min;
+ const buffer = Math.abs(maxDomain - minDomain) * 0.1;
+ maxDomain =
+ (typeof baseline === 'number' ? Math.max(maxDomain, baseline) : maxDomain) + buffer;
+ minDomain =
+ (typeof baseline === 'number' ? Math.min(minDomain, baseline) : minDomain) - buffer;
+ }
+ return { maxDomain, minDomain };
+ }, [decisionPathData, baseline]);
+
+ if (!decisionPathData) return ;
+
+ return (
+ <>
+ {baseline === undefined && (
+
+ }
+ color="warning"
+ iconType="alert"
+ />
+ )}
+
+ >
+ );
+};
diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/missing_decision_path_callout.tsx b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/missing_decision_path_callout.tsx
new file mode 100644
index 0000000000000..66eb2047b1314
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/missing_decision_path_callout.tsx
@@ -0,0 +1,20 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { EuiCallOut } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+export const MissingDecisionPathCallout = () => {
+ return (
+
+
+
+ );
+};
diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/use_classification_path_data.tsx b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/use_classification_path_data.tsx
new file mode 100644
index 0000000000000..90216c4a58ffc
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/use_classification_path_data.tsx
@@ -0,0 +1,173 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { useMemo } from 'react';
+import { i18n } from '@kbn/i18n';
+import { FeatureImportance, TopClasses } from '../../../../../common/types/feature_importance';
+import { ExtendedFeatureImportance } from './decision_path_popover';
+
+export type DecisionPathPlotData = Array<[string, number, number]>;
+
+interface UseDecisionPathDataParams {
+ featureImportance: FeatureImportance[];
+ baseline?: number;
+ predictedValue?: string | number | undefined;
+ topClasses?: TopClasses;
+}
+
+interface RegressionDecisionPathProps {
+ baseline?: number;
+ predictedValue?: number | undefined;
+ featureImportance: FeatureImportance[];
+ topClasses?: TopClasses;
+}
+const FEATURE_NAME = 'feature_name';
+const FEATURE_IMPORTANCE = 'importance';
+
+export const isDecisionPathData = (decisionPathData: any): boolean => {
+ return (
+ Array.isArray(decisionPathData) &&
+ decisionPathData.length > 0 &&
+ decisionPathData[0].length === 3
+ );
+};
+
+// cast to 'True' | 'False' | value to match Eui display
+export const getStringBasedClassName = (v: string | boolean | undefined | number): string => {
+ if (v === undefined) {
+ return '';
+ }
+ if (typeof v === 'boolean') {
+ return v ? 'True' : 'False';
+ }
+ if (typeof v === 'number') {
+ return v.toString();
+ }
+ return v;
+};
+
+export const useDecisionPathData = ({
+ baseline,
+ featureImportance,
+ predictedValue,
+}: UseDecisionPathDataParams): { decisionPathData: DecisionPathPlotData | undefined } => {
+ const decisionPathData = useMemo(() => {
+ return baseline
+ ? buildRegressionDecisionPathData({
+ baseline,
+ featureImportance,
+ predictedValue: predictedValue as number | undefined,
+ })
+ : buildClassificationDecisionPathData({
+ featureImportance,
+ currentClass: predictedValue as string | undefined,
+ });
+ }, [baseline, featureImportance, predictedValue]);
+
+ return { decisionPathData };
+};
+
+export const buildDecisionPathData = (featureImportance: ExtendedFeatureImportance[]) => {
+ const finalResult: DecisionPathPlotData = featureImportance
+ // sort so absolute importance so it goes from bottom (baseline) to top
+ .sort(
+ (a: ExtendedFeatureImportance, b: ExtendedFeatureImportance) =>
+ b.absImportance! - a.absImportance!
+ )
+ .map((d) => [d[FEATURE_NAME] as string, d[FEATURE_IMPORTANCE] as number, NaN]);
+
+ // start at the baseline and end at predicted value
+ // for regression, cumulativeSum should add up to baseline
+ let cumulativeSum = 0;
+ for (let i = featureImportance.length - 1; i >= 0; i--) {
+ cumulativeSum += finalResult[i][1];
+ finalResult[i][2] = cumulativeSum;
+ }
+ return finalResult;
+};
+export const buildRegressionDecisionPathData = ({
+ baseline,
+ featureImportance,
+ predictedValue,
+}: RegressionDecisionPathProps): DecisionPathPlotData | undefined => {
+ let mappedFeatureImportance: ExtendedFeatureImportance[] = featureImportance;
+ mappedFeatureImportance = mappedFeatureImportance.map((d) => ({
+ ...d,
+ absImportance: Math.abs(d[FEATURE_IMPORTANCE] as number),
+ }));
+
+ if (baseline && predictedValue !== undefined && Number.isFinite(predictedValue)) {
+ // get the adjusted importance needed for when # of fields included in c++ analysis != max allowed
+ // if num fields included = num features allowed exactly, adjustedImportance should be 0
+ const adjustedImportance =
+ predictedValue -
+ mappedFeatureImportance.reduce(
+ (accumulator, currentValue) => accumulator + currentValue.importance!,
+ 0
+ ) -
+ baseline;
+
+ mappedFeatureImportance.push({
+ [FEATURE_NAME]: i18n.translate(
+ 'xpack.ml.dataframe.analytics.decisionPathFeatureBaselineTitle',
+ {
+ defaultMessage: 'baseline',
+ }
+ ),
+ [FEATURE_IMPORTANCE]: baseline,
+ absImportance: -1,
+ });
+
+ // if the difference is small enough then no need to plot the residual feature importance
+ if (Math.abs(adjustedImportance) > 1e-5) {
+ mappedFeatureImportance.push({
+ [FEATURE_NAME]: i18n.translate(
+ 'xpack.ml.dataframe.analytics.decisionPathFeatureOtherTitle',
+ {
+ defaultMessage: 'other',
+ }
+ ),
+ [FEATURE_IMPORTANCE]: adjustedImportance,
+ absImportance: 0, // arbitrary importance so this will be of higher importance than baseline
+ });
+ }
+ }
+ const filteredFeatureImportance = mappedFeatureImportance.filter(
+ (f) => f !== undefined
+ ) as ExtendedFeatureImportance[];
+
+ return buildDecisionPathData(filteredFeatureImportance);
+};
+
+export const buildClassificationDecisionPathData = ({
+ featureImportance,
+ currentClass,
+}: {
+ featureImportance: FeatureImportance[];
+ currentClass: string | undefined;
+}): DecisionPathPlotData | undefined => {
+ if (currentClass === undefined) return [];
+ const mappedFeatureImportance: Array<
+ ExtendedFeatureImportance | undefined
+ > = featureImportance.map((feature) => {
+ const classFeatureImportance = Array.isArray(feature.classes)
+ ? feature.classes.find((c) => getStringBasedClassName(c.class_name) === currentClass)
+ : feature;
+ if (classFeatureImportance && typeof classFeatureImportance[FEATURE_IMPORTANCE] === 'number') {
+ return {
+ [FEATURE_NAME]: feature[FEATURE_NAME],
+ [FEATURE_IMPORTANCE]: classFeatureImportance[FEATURE_IMPORTANCE],
+ absImportance: Math.abs(classFeatureImportance[FEATURE_IMPORTANCE] as number),
+ };
+ }
+ return undefined;
+ });
+ const filteredFeatureImportance = mappedFeatureImportance.filter(
+ (f) => f !== undefined
+ ) as ExtendedFeatureImportance[];
+
+ return buildDecisionPathData(filteredFeatureImportance);
+};
diff --git a/x-pack/plugins/ml/public/application/components/data_grid/types.ts b/x-pack/plugins/ml/public/application/components/data_grid/types.ts
index 756f74c8f9302..f9ee8c37fabf7 100644
--- a/x-pack/plugins/ml/public/application/components/data_grid/types.ts
+++ b/x-pack/plugins/ml/public/application/components/data_grid/types.ts
@@ -74,6 +74,9 @@ export interface UseIndexDataReturnType
| 'tableItems'
| 'toggleChartVisibility'
| 'visibleColumns'
+ | 'baseline'
+ | 'predictionFieldName'
+ | 'resultsField'
> {
renderCellValue: RenderCellValue;
}
@@ -105,4 +108,7 @@ export interface UseDataGridReturnType {
tableItems: DataGridItem[];
toggleChartVisibility: () => void;
visibleColumns: ColumnId[];
+ baseline?: number;
+ predictionFieldName?: string;
+ resultsField?: string;
}
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts
index 8ad861e616b7a..97098ea9e75c6 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts
@@ -15,18 +15,19 @@ import { SavedSearchQuery } from '../../contexts/ml';
import {
AnalysisConfig,
ClassificationAnalysis,
- OutlierAnalysis,
RegressionAnalysis,
+ ANALYSIS_CONFIG_TYPE,
} from '../../../../common/types/data_frame_analytics';
-
+import {
+ isOutlierAnalysis,
+ isRegressionAnalysis,
+ isClassificationAnalysis,
+ getPredictionFieldName,
+ getDependentVar,
+ getPredictedFieldName,
+} from '../../../../common/util/analytics_utils';
export type IndexPattern = string;
-export enum ANALYSIS_CONFIG_TYPE {
- OUTLIER_DETECTION = 'outlier_detection',
- REGRESSION = 'regression',
- CLASSIFICATION = 'classification',
-}
-
export enum ANALYSIS_ADVANCED_FIELDS {
ETA = 'eta',
FEATURE_BAG_FRACTION = 'feature_bag_fraction',
@@ -156,23 +157,6 @@ export const getAnalysisType = (analysis: AnalysisConfig): string => {
return 'unknown';
};
-export const getDependentVar = (
- analysis: AnalysisConfig
-):
- | RegressionAnalysis['regression']['dependent_variable']
- | ClassificationAnalysis['classification']['dependent_variable'] => {
- let depVar = '';
-
- if (isRegressionAnalysis(analysis)) {
- depVar = analysis.regression.dependent_variable;
- }
-
- if (isClassificationAnalysis(analysis)) {
- depVar = analysis.classification.dependent_variable;
- }
- return depVar;
-};
-
export const getTrainingPercent = (
analysis: AnalysisConfig
):
@@ -190,24 +174,6 @@ export const getTrainingPercent = (
return trainingPercent;
};
-export const getPredictionFieldName = (
- analysis: AnalysisConfig
-):
- | RegressionAnalysis['regression']['prediction_field_name']
- | ClassificationAnalysis['classification']['prediction_field_name'] => {
- // If undefined will be defaulted to dependent_variable when config is created
- let predictionFieldName;
- if (isRegressionAnalysis(analysis) && analysis.regression.prediction_field_name !== undefined) {
- predictionFieldName = analysis.regression.prediction_field_name;
- } else if (
- isClassificationAnalysis(analysis) &&
- analysis.classification.prediction_field_name !== undefined
- ) {
- predictionFieldName = analysis.classification.prediction_field_name;
- }
- return predictionFieldName;
-};
-
export const getNumTopClasses = (
analysis: AnalysisConfig
): ClassificationAnalysis['classification']['num_top_classes'] => {
@@ -238,35 +204,6 @@ export const getNumTopFeatureImportanceValues = (
return numTopFeatureImportanceValues;
};
-export const getPredictedFieldName = (
- resultsField: string,
- analysis: AnalysisConfig,
- forSort?: boolean
-) => {
- // default is 'ml'
- const predictionFieldName = getPredictionFieldName(analysis);
- const defaultPredictionField = `${getDependentVar(analysis)}_prediction`;
- const predictedField = `${resultsField}.${
- predictionFieldName ? predictionFieldName : defaultPredictionField
- }`;
- return predictedField;
-};
-
-export const isOutlierAnalysis = (arg: any): arg is OutlierAnalysis => {
- const keys = Object.keys(arg);
- return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION;
-};
-
-export const isRegressionAnalysis = (arg: any): arg is RegressionAnalysis => {
- const keys = Object.keys(arg);
- return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.REGRESSION;
-};
-
-export const isClassificationAnalysis = (arg: any): arg is ClassificationAnalysis => {
- const keys = Object.keys(arg);
- return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.CLASSIFICATION;
-};
-
export const isResultsSearchBoolQuery = (arg: any): arg is ResultsSearchBoolQuery => {
if (arg === undefined) return false;
const keys = Object.keys(arg);
@@ -607,3 +544,13 @@ export const loadDocsCount = async ({
};
}
};
+
+export {
+ isOutlierAnalysis,
+ isRegressionAnalysis,
+ isClassificationAnalysis,
+ getPredictionFieldName,
+ ANALYSIS_CONFIG_TYPE,
+ getDependentVar,
+ getPredictedFieldName,
+};
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/constants.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/constants.ts
index 2f14dfdfdfca3..c2295a92af89c 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/constants.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/constants.ts
@@ -3,8 +3,6 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-
-export const DEFAULT_RESULTS_FIELD = 'ml';
export const FEATURE_IMPORTANCE = 'feature_importance';
export const FEATURE_INFLUENCE = 'feature_influence';
export const TOP_CLASSES = 'top_classes';
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts
index 847aefefbc6c8..f9c9bf26a9d16 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts
@@ -4,17 +4,16 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { getNumTopClasses, getNumTopFeatureImportanceValues } from './analytics';
+import { Field } from '../../../../common/types/fields';
import {
- getNumTopClasses,
- getNumTopFeatureImportanceValues,
getPredictedFieldName,
getDependentVar,
getPredictionFieldName,
isClassificationAnalysis,
isOutlierAnalysis,
isRegressionAnalysis,
-} from './analytics';
-import { Field } from '../../../../common/types/fields';
+} from '../../../../common/util/analytics_utils';
import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public';
import { newJobCapsService } from '../../services/new_job_capabilities_service';
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx
index ccac9a697210b..2e3a5d89367ce 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx
@@ -9,7 +9,6 @@ import React, { FC } from 'react';
import { i18n } from '@kbn/i18n';
import { ExplorationPageWrapper } from '../exploration_page_wrapper';
-
import { EvaluatePanel } from './evaluate_panel';
interface Props {
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx
index 34ff36c59fa6c..84b44ef0d349f 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx
@@ -51,7 +51,6 @@ export const ExplorationPageWrapper: FC = ({ jobId, title, EvaluatePanel
/>
);
}
-
return (
<>
{isLoadingJobConfig === true && jobConfig === undefined && }
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx
index 8395a11bd6fda..eea579ef1d064 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx
@@ -28,6 +28,8 @@ import {
INDEX_STATUS,
SEARCH_SIZE,
defaultSearchQuery,
+ getAnalysisType,
+ ANALYSIS_CONFIG_TYPE,
} from '../../../../common';
import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/use_columns';
import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common';
@@ -36,6 +38,7 @@ import { ExplorationQueryBar } from '../exploration_query_bar';
import { IndexPatternPrompt } from '../index_pattern_prompt';
import { useExplorationResults } from './use_exploration_results';
+import { useMlKibana } from '../../../../../contexts/kibana';
const showingDocs = i18n.translate(
'xpack.ml.dataframe.analytics.explorationResults.documentsShownHelpText',
@@ -70,18 +73,27 @@ export const ExplorationResultsTable: FC = React.memo(
setEvaluateSearchQuery,
title,
}) => {
+ const {
+ services: {
+ mlServices: { mlApiServices },
+ },
+ } = useMlKibana();
const [searchQuery, setSearchQuery] = useState(defaultSearchQuery);
useEffect(() => {
setEvaluateSearchQuery(searchQuery);
}, [JSON.stringify(searchQuery)]);
+ const analysisType = getAnalysisType(jobConfig.analysis);
+
const classificationData = useExplorationResults(
indexPattern,
jobConfig,
searchQuery,
- getToastNotifications()
+ getToastNotifications(),
+ mlApiServices
);
+
const docFieldsCount = classificationData.columnsWithCharts.length;
const {
columnsWithCharts,
@@ -94,7 +106,6 @@ export const ExplorationResultsTable: FC = React.memo(
if (jobConfig === undefined || classificationData === undefined) {
return null;
}
-
// if it's a searchBar syntax error leave the table visible so they can try again
if (status === INDEX_STATUS.ERROR && !errorMessage.includes('failed to create query')) {
return (
@@ -184,6 +195,7 @@ export const ExplorationResultsTable: FC = React.memo(
{...classificationData}
dataTestSubj="mlExplorationDataGrid"
toastNotifications={getToastNotifications()}
+ analysisType={(analysisType as unknown) as ANALYSIS_CONFIG_TYPE}
/>
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts
index 8d53214d23d47..a56345017258e 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts
@@ -4,12 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { useEffect, useMemo } from 'react';
+import { useCallback, useEffect, useMemo, useState } from 'react';
import { EuiDataGridColumn } from '@elastic/eui';
import { CoreSetup } from 'src/core/public';
+import { i18n } from '@kbn/i18n';
+import { MlApiServices } from '../../../../../services/ml_api_service';
import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public';
import { DataLoader } from '../../../../../datavisualizer/index_based/data_loader';
@@ -23,21 +25,26 @@ import {
UseIndexDataReturnType,
} from '../../../../../components/data_grid';
import { SavedSearchQuery } from '../../../../../contexts/ml';
-
import { getIndexData, getIndexFields, DataFrameAnalyticsConfig } from '../../../../common';
import {
- DEFAULT_RESULTS_FIELD,
- FEATURE_IMPORTANCE,
- TOP_CLASSES,
-} from '../../../../common/constants';
+ getPredictionFieldName,
+ getDefaultPredictionFieldName,
+} from '../../../../../../../common/util/analytics_utils';
+import { FEATURE_IMPORTANCE, TOP_CLASSES } from '../../../../common/constants';
+import { DEFAULT_RESULTS_FIELD } from '../../../../../../../common/constants/data_frame_analytics';
import { sortExplorationResultsFields, ML__ID_COPY } from '../../../../common/fields';
+import { isRegressionAnalysis } from '../../../../common/analytics';
+import { extractErrorMessage } from '../../../../../../../common/util/errors';
export const useExplorationResults = (
indexPattern: IndexPattern | undefined,
jobConfig: DataFrameAnalyticsConfig | undefined,
searchQuery: SavedSearchQuery,
- toastNotifications: CoreSetup['notifications']['toasts']
+ toastNotifications: CoreSetup['notifications']['toasts'],
+ mlApiServices: MlApiServices
): UseIndexDataReturnType => {
+ const [baseline, setBaseLine] = useState();
+
const needsDestIndexFields =
indexPattern !== undefined && indexPattern.title === jobConfig?.source.index[0];
@@ -52,7 +59,6 @@ export const useExplorationResults = (
)
);
}
-
const dataGrid = useDataGrid(
columns,
25,
@@ -107,16 +113,60 @@ export const useExplorationResults = (
jobConfig?.dest.index,
JSON.stringify([searchQuery, dataGrid.visibleColumns]),
]);
+ const predictionFieldName = useMemo(() => {
+ if (jobConfig) {
+ return (
+ getPredictionFieldName(jobConfig.analysis) ??
+ getDefaultPredictionFieldName(jobConfig.analysis)
+ );
+ }
+ return undefined;
+ }, [jobConfig]);
+
+ const getAnalyticsBaseline = useCallback(async () => {
+ try {
+ if (
+ jobConfig !== undefined &&
+ jobConfig.analysis !== undefined &&
+ isRegressionAnalysis(jobConfig.analysis)
+ ) {
+ const result = await mlApiServices.dataFrameAnalytics.getAnalyticsBaseline(jobConfig.id);
+ if (result?.baseline) {
+ setBaseLine(result.baseline);
+ }
+ }
+ } catch (e) {
+ const error = extractErrorMessage(e);
+
+ toastNotifications.addDanger({
+ title: i18n.translate(
+ 'xpack.ml.dataframe.analytics.explorationResults.baselineErrorMessageToast',
+ {
+ defaultMessage: 'An error occurred getting feature importance baseline',
+ }
+ ),
+ text: error,
+ });
+ }
+ }, [mlApiServices, jobConfig]);
+
+ useEffect(() => {
+ getAnalyticsBaseline();
+ }, [jobConfig]);
+ const resultsField = jobConfig?.dest.results_field ?? DEFAULT_RESULTS_FIELD;
const renderCellValue = useRenderCellValue(
indexPattern,
dataGrid.pagination,
dataGrid.tableItems,
- jobConfig?.dest.results_field ?? DEFAULT_RESULTS_FIELD
+ resultsField
);
return {
...dataGrid,
renderCellValue,
+ baseline,
+ predictionFieldName,
+ resultsField,
};
};
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts
index 24649ae5f1e71..151e5ea4e6feb 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts
@@ -29,7 +29,8 @@ import { SavedSearchQuery } from '../../../../../contexts/ml';
import { getToastNotifications } from '../../../../../util/dependency_cache';
import { getIndexData, getIndexFields, DataFrameAnalyticsConfig } from '../../../../common';
-import { DEFAULT_RESULTS_FIELD, FEATURE_INFLUENCE } from '../../../../common/constants';
+import { FEATURE_INFLUENCE } from '../../../../common/constants';
+import { DEFAULT_RESULTS_FIELD } from '../../../../../../../common/constants/data_frame_analytics';
import { sortExplorationResultsFields, ML__ID_COPY } from '../../../../common/fields';
import { getFeatureCount, getOutlierScoreFieldName } from './common';
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx
index 60c699ba0d370..ce24892c9de45 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx
@@ -12,7 +12,7 @@ import { IIndexPattern } from 'src/plugins/data/common';
import { DeepReadonly } from '../../../../../../../common/types/common';
import { DataFrameAnalyticsConfig, isOutlierAnalysis } from '../../../../common';
import { isClassificationAnalysis, isRegressionAnalysis } from '../../../../common/analytics';
-import { DEFAULT_RESULTS_FIELD } from '../../../../common/constants';
+import { DEFAULT_RESULTS_FIELD } from '../../../../../../../common/constants/data_frame_analytics';
import { useMlKibana, useNavigateToPath } from '../../../../../contexts/kibana';
import { DEFAULT_NUM_TOP_FEATURE_IMPORTANCE_VALUES } from '../../hooks/use_create_analytics_form';
import { State } from '../../hooks/use_create_analytics_form/state';
diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts
index 7de39d91047ef..434200d0383f5 100644
--- a/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts
+++ b/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts
@@ -135,4 +135,10 @@ export const dataFrameAnalytics = {
method: 'GET',
});
},
+ getAnalyticsBaseline(analyticsId: string) {
+ return http({
+ path: `${basePath()}/data_frame/analytics/${analyticsId}/baseline`,
+ method: 'POST',
+ });
+ },
};
diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/feature_importance.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/feature_importance.ts
new file mode 100644
index 0000000000000..94f54a5654873
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_frame_analytics/feature_importance.ts
@@ -0,0 +1,69 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { IScopedClusterClient } from 'kibana/server';
+import {
+ getDefaultPredictionFieldName,
+ getPredictionFieldName,
+ isRegressionAnalysis,
+} from '../../../common/util/analytics_utils';
+import { DEFAULT_RESULTS_FIELD } from '../../../common/constants/data_frame_analytics';
+// Obtains data for the data frame analytics feature importance functionalities
+// such as baseline, decision paths, or importance summary.
+export function analyticsFeatureImportanceProvider({
+ asInternalUser,
+ asCurrentUser,
+}: IScopedClusterClient) {
+ async function getRegressionAnalyticsBaseline(analyticsId: string): Promise {
+ const { body } = await asInternalUser.ml.getDataFrameAnalytics({
+ id: analyticsId,
+ });
+ const jobConfig = body.data_frame_analytics[0];
+ if (!isRegressionAnalysis) return undefined;
+ const destinationIndex = jobConfig.dest.index;
+ const predictionFieldName = getPredictionFieldName(jobConfig.analysis);
+ const mlResultsField = jobConfig.dest?.results_field ?? DEFAULT_RESULTS_FIELD;
+ const predictedField = `${mlResultsField}.${
+ predictionFieldName ? predictionFieldName : getDefaultPredictionFieldName(jobConfig.analysis)
+ }`;
+ const isTrainingField = `${mlResultsField}.is_training`;
+
+ const params = {
+ index: destinationIndex,
+ size: 0,
+ body: {
+ query: {
+ bool: {
+ filter: [
+ {
+ term: {
+ [isTrainingField]: true,
+ },
+ },
+ ],
+ },
+ },
+ aggs: {
+ featureImportanceBaseline: {
+ avg: {
+ field: predictedField,
+ },
+ },
+ },
+ },
+ };
+ let baseline;
+ const { body: aggregationResult } = await asCurrentUser.search(params);
+ if (aggregationResult) {
+ baseline = aggregationResult.aggregations.featureImportanceBaseline.value;
+ }
+ return baseline;
+ }
+
+ return {
+ getRegressionAnalyticsBaseline,
+ };
+}
diff --git a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts
index dea4803e8275e..7606420eacefc 100644
--- a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts
+++ b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts
@@ -20,6 +20,7 @@ import {
import { IndexPatternHandler } from '../models/data_frame_analytics/index_patterns';
import { DeleteDataFrameAnalyticsWithIndexStatus } from '../../common/types/data_frame_analytics';
import { getAuthorizationHeader } from '../lib/request_authorization';
+import { analyticsFeatureImportanceProvider } from '../models/data_frame_analytics/feature_importance';
function getIndexPatternId(context: RequestHandlerContext, patternName: string) {
const iph = new IndexPatternHandler(context.core.savedObjects.client);
@@ -545,4 +546,38 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat
}
})
);
+
+ /**
+ * @apiGroup DataFrameAnalytics
+ *
+ * @api {get} /api/ml/data_frame/analytics/baseline Get analytics's feature importance baseline
+ * @apiName GetDataFrameAnalyticsBaseline
+ * @apiDescription Returns the baseline for data frame analytics job.
+ *
+ * @apiSchema (params) analyticsIdSchema
+ */
+ router.post(
+ {
+ path: '/api/ml/data_frame/analytics/{analyticsId}/baseline',
+ validate: {
+ params: analyticsIdSchema,
+ },
+ options: {
+ tags: ['access:ml:canGetDataFrameAnalytics'],
+ },
+ },
+ mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => {
+ try {
+ const { analyticsId } = request.params;
+ const { getRegressionAnalyticsBaseline } = analyticsFeatureImportanceProvider(client);
+ const baseline = await getRegressionAnalyticsBaseline(analyticsId);
+
+ return response.ok({
+ body: { baseline },
+ });
+ } catch (e) {
+ return response.customError(wrapError(e));
+ }
+ })
+ );
}
diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts b/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts
index ffa1d9fd46c75..e01e065867ac7 100644
--- a/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts
+++ b/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts
@@ -10,25 +10,9 @@ import { FtrProviderContext } from '../../ftr_provider_context';
import { MlCommonUI } from './common_ui';
import { MlApi } from './api';
import {
- ClassificationAnalysis,
- RegressionAnalysis,
-} from '../../../../plugins/ml/common/types/data_frame_analytics';
-
-enum ANALYSIS_CONFIG_TYPE {
- OUTLIER_DETECTION = 'outlier_detection',
- REGRESSION = 'regression',
- CLASSIFICATION = 'classification',
-}
-
-const isRegressionAnalysis = (arg: any): arg is RegressionAnalysis => {
- const keys = Object.keys(arg);
- return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.REGRESSION;
-};
-
-const isClassificationAnalysis = (arg: any): arg is ClassificationAnalysis => {
- const keys = Object.keys(arg);
- return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.CLASSIFICATION;
-};
+ isRegressionAnalysis,
+ isClassificationAnalysis,
+} from '../../../../plugins/ml/common/util/analytics_utils';
export function MachineLearningDataFrameAnalyticsCreationProvider(
{ getService }: FtrProviderContext,
From a0defb81963a134b7d7b3e3327149de1a26a5a02 Mon Sep 17 00:00:00 2001
From: Constance
Date: Wed, 9 Sep 2020 10:01:28 -0700
Subject: [PATCH 09/25] [Enterprise Search] Update config data endpoint to v2
(#76970)
* Update our internal config/app data to v2 specs
- Update endpoint to v2
- Update data accordingly to new API structures
- Update types accordingly
* Fix failing type check for other endpoints that use IAccount
* Update role type casing
- ent search was fixed from camel to snake
---
.../common/__mocks__/initial_app_data.ts | 20 +++--
.../common/types/app_search.ts | 7 ++
.../enterprise_search/common/types/index.ts | 15 +++-
.../common/types/workplace_search.ts | 12 ++-
.../lib/enterprise_search_config_api.test.ts | 88 ++++++++++++-------
.../lib/enterprise_search_config_api.ts | 68 ++++++++------
6 files changed, 139 insertions(+), 71 deletions(-)
diff --git a/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts b/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts
index 2d31be65dd30e..4533383ebd80e 100644
--- a/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts
+++ b/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts
@@ -7,9 +7,20 @@
export const DEFAULT_INITIAL_APP_DATA = {
readOnlyMode: false,
ilmEnabled: true,
+ isFederatedAuth: false,
configuredLimits: {
- maxDocumentByteSize: 102400,
- maxEnginesPerMetaEngine: 15,
+ appSearch: {
+ engine: {
+ maxDocumentByteSize: 102400,
+ maxEnginesPerMetaEngine: 15,
+ },
+ },
+ workplaceSearch: {
+ customApiSource: {
+ maxDocumentByteSize: 102400,
+ totalFields: 64,
+ },
+ },
},
appSearch: {
accountId: 'some-id-string',
@@ -29,17 +40,16 @@ export const DEFAULT_INITIAL_APP_DATA = {
},
},
workplaceSearch: {
- canCreateInvitations: true,
- isFederatedAuth: false,
organization: {
name: 'ACME Donuts',
defaultOrgName: 'My Organization',
},
- fpAccount: {
+ account: {
id: 'some-id-string',
groups: ['Default', 'Cats'],
isAdmin: true,
canCreatePersonalSources: true,
+ canCreateInvitations: true,
isCurated: false,
viewedOnboardingPage: true,
},
diff --git a/x-pack/plugins/enterprise_search/common/types/app_search.ts b/x-pack/plugins/enterprise_search/common/types/app_search.ts
index 5d6ec079e66e0..72259ecd2343d 100644
--- a/x-pack/plugins/enterprise_search/common/types/app_search.ts
+++ b/x-pack/plugins/enterprise_search/common/types/app_search.ts
@@ -23,3 +23,10 @@ export interface IRole {
availableRoleTypes: string[];
};
}
+
+export interface IConfiguredLimits {
+ engine: {
+ maxDocumentByteSize: number;
+ maxEnginesPerMetaEngine: number;
+ };
+}
diff --git a/x-pack/plugins/enterprise_search/common/types/index.ts b/x-pack/plugins/enterprise_search/common/types/index.ts
index 008afb234a376..a41a42da477ee 100644
--- a/x-pack/plugins/enterprise_search/common/types/index.ts
+++ b/x-pack/plugins/enterprise_search/common/types/index.ts
@@ -4,18 +4,25 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { IAccount as IAppSearchAccount } from './app_search';
-import { IWorkplaceSearchInitialData } from './workplace_search';
+import {
+ IAccount as IAppSearchAccount,
+ IConfiguredLimits as IAppSearchConfiguredLimits,
+} from './app_search';
+import {
+ IWorkplaceSearchInitialData,
+ IConfiguredLimits as IWorkplaceSearchConfiguredLimits,
+} from './workplace_search';
export interface IInitialAppData {
readOnlyMode?: boolean;
ilmEnabled?: boolean;
+ isFederatedAuth?: boolean;
configuredLimits?: IConfiguredLimits;
appSearch?: IAppSearchAccount;
workplaceSearch?: IWorkplaceSearchInitialData;
}
export interface IConfiguredLimits {
- maxDocumentByteSize: number;
- maxEnginesPerMetaEngine: number;
+ appSearch: IAppSearchConfiguredLimits;
+ workplaceSearch: IWorkplaceSearchConfiguredLimits;
}
diff --git a/x-pack/plugins/enterprise_search/common/types/workplace_search.ts b/x-pack/plugins/enterprise_search/common/types/workplace_search.ts
index bc4e39b0788d9..6c82206706b32 100644
--- a/x-pack/plugins/enterprise_search/common/types/workplace_search.ts
+++ b/x-pack/plugins/enterprise_search/common/types/workplace_search.ts
@@ -10,6 +10,7 @@ export interface IAccount {
isAdmin: boolean;
isCurated: boolean;
canCreatePersonalSources: boolean;
+ canCreateInvitations?: boolean;
viewedOnboardingPage: boolean;
}
@@ -19,8 +20,13 @@ export interface IOrganization {
}
export interface IWorkplaceSearchInitialData {
- canCreateInvitations: boolean;
- isFederatedAuth: boolean;
organization: IOrganization;
- fpAccount: IAccount;
+ account: IAccount;
+}
+
+export interface IConfiguredLimits {
+ customApiSource: {
+ maxDocumentByteSize: number;
+ totalFields: number;
+ };
}
diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts
index 323f79e63bc6f..8e3ae2cfbeb86 100644
--- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts
+++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts
@@ -38,51 +38,63 @@ describe('callEnterpriseSearchConfigAPI', () => {
external_url: 'http://some.vanity.url/',
read_only_mode: false,
ilm_enabled: true,
+ is_federated_auth: false,
configured_limits: {
- max_document_byte_size: 102400,
- max_engines_per_meta_engine: 15,
+ app_search: {
+ engine: {
+ document_size_in_bytes: 102400,
+ source_engines_per_meta_engine: 15,
+ },
+ },
+ workplace_search: {
+ custom_api_source: {
+ document_size_in_bytes: 102400,
+ total_fields: 64,
+ },
+ },
+ },
+ },
+ current_user: {
+ name: 'someuser',
+ access: {
+ app_search: true,
+ workplace_search: false,
},
app_search: {
- account_id: 'some-id-string',
- onboarding_complete: true,
+ account: {
+ id: 'some-id-string',
+ onboarding_complete: true,
+ },
+ role: {
+ id: 'account_id:somestring|user_oid:somestring',
+ role_type: 'owner',
+ ability: {
+ access_all_engines: true,
+ destroy: ['session'],
+ manage: ['account_credentials', 'account_engines'], // etc
+ edit: ['LocoMoco::Account'], // etc
+ view: ['Engine'], // etc
+ credential_types: ['admin', 'private', 'search'],
+ available_role_types: ['owner', 'admin'],
+ },
+ },
},
workplace_search: {
- can_create_invitations: true,
- is_federated_auth: false,
organization: {
name: 'ACME Donuts',
default_org_name: 'My Organization',
},
- fp_account: {
+ account: {
id: 'some-id-string',
groups: ['Default', 'Cats'],
is_admin: true,
can_create_personal_sources: true,
+ can_create_invitations: true,
is_curated: false,
viewed_onboarding_page: true,
},
},
},
- current_user: {
- name: 'someuser',
- access: {
- app_search: true,
- workplace_search: false,
- },
- app_search_role: {
- id: 'account_id:somestring|user_oid:somestring',
- role_type: 'owner',
- ability: {
- access_all_engines: true,
- destroy: ['session'],
- manage: ['account_credentials', 'account_engines'], // etc
- edit: ['LocoMoco::Account'], // etc
- view: ['Engine'], // etc
- credential_types: ['admin', 'private', 'search'],
- available_role_types: ['owner', 'admin'],
- },
- },
- },
};
beforeEach(() => {
@@ -91,7 +103,7 @@ describe('callEnterpriseSearchConfigAPI', () => {
it('calls the config API endpoint', async () => {
fetchMock.mockImplementationOnce((url: string) => {
- expect(url).toEqual('http://localhost:3002/api/ent/v1/internal/client_config');
+ expect(url).toEqual('http://localhost:3002/api/ent/v2/internal/client_config');
return Promise.resolve(new Response(JSON.stringify(mockResponse)));
});
@@ -116,9 +128,20 @@ describe('callEnterpriseSearchConfigAPI', () => {
publicUrl: undefined,
readOnlyMode: false,
ilmEnabled: false,
+ isFederatedAuth: false,
configuredLimits: {
- maxDocumentByteSize: undefined,
- maxEnginesPerMetaEngine: undefined,
+ appSearch: {
+ engine: {
+ maxDocumentByteSize: undefined,
+ maxEnginesPerMetaEngine: undefined,
+ },
+ },
+ workplaceSearch: {
+ customApiSource: {
+ maxDocumentByteSize: undefined,
+ totalFields: undefined,
+ },
+ },
},
appSearch: {
accountId: undefined,
@@ -138,17 +161,16 @@ describe('callEnterpriseSearchConfigAPI', () => {
},
},
workplaceSearch: {
- canCreateInvitations: false,
- isFederatedAuth: false,
organization: {
name: undefined,
defaultOrgName: undefined,
},
- fpAccount: {
+ account: {
id: undefined,
groups: [],
isAdmin: false,
canCreatePersonalSources: false,
+ canCreateInvitations: false,
isCurated: false,
viewedOnboardingPage: false,
},
diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts
index c9cbec15169d9..10a75e59cb249 100644
--- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts
+++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts
@@ -29,7 +29,7 @@ interface IReturn extends IInitialAppData {
* useful various settings (e.g. product access, external URL)
* needed by the Kibana plugin at the setup stage
*/
-const ENDPOINT = '/api/ent/v1/internal/client_config';
+const ENDPOINT = '/api/ent/v2/internal/client_config';
export const callEnterpriseSearchConfigAPI = async ({
config,
@@ -67,44 +67,60 @@ export const callEnterpriseSearchConfigAPI = async ({
publicUrl: stripTrailingSlash(data?.settings?.external_url),
readOnlyMode: !!data?.settings?.read_only_mode,
ilmEnabled: !!data?.settings?.ilm_enabled,
+ isFederatedAuth: !!data?.settings?.is_federated_auth, // i.e., not standard auth
configuredLimits: {
- maxDocumentByteSize: data?.settings?.configured_limits?.max_document_byte_size,
- maxEnginesPerMetaEngine: data?.settings?.configured_limits?.max_engines_per_meta_engine,
+ appSearch: {
+ engine: {
+ maxDocumentByteSize:
+ data?.settings?.configured_limits?.app_search?.engine?.document_size_in_bytes,
+ maxEnginesPerMetaEngine:
+ data?.settings?.configured_limits?.app_search?.engine?.source_engines_per_meta_engine,
+ },
+ },
+ workplaceSearch: {
+ customApiSource: {
+ maxDocumentByteSize:
+ data?.settings?.configured_limits?.workplace_search?.custom_api_source
+ ?.document_size_in_bytes,
+ totalFields:
+ data?.settings?.configured_limits?.workplace_search?.custom_api_source?.total_fields,
+ },
+ },
},
appSearch: {
- accountId: data?.settings?.app_search?.account_id,
- onBoardingComplete: !!data?.settings?.app_search?.onboarding_complete,
+ accountId: data?.current_user?.app_search?.account?.id,
+ onBoardingComplete: !!data?.current_user?.app_search?.account?.onboarding_complete,
role: {
- id: data?.current_user?.app_search_role?.id,
- roleType: data?.current_user?.app_search_role?.role_type,
+ id: data?.current_user?.app_search?.role?.id,
+ roleType: data?.current_user?.app_search?.role?.role_type,
ability: {
- accessAllEngines: !!data?.current_user?.app_search_role?.ability?.access_all_engines,
- destroy: data?.current_user?.app_search_role?.ability?.destroy || [],
- manage: data?.current_user?.app_search_role?.ability?.manage || [],
- edit: data?.current_user?.app_search_role?.ability?.edit || [],
- view: data?.current_user?.app_search_role?.ability?.view || [],
- credentialTypes: data?.current_user?.app_search_role?.ability?.credential_types || [],
+ accessAllEngines: !!data?.current_user?.app_search?.role?.ability?.access_all_engines,
+ destroy: data?.current_user?.app_search?.role?.ability?.destroy || [],
+ manage: data?.current_user?.app_search?.role?.ability?.manage || [],
+ edit: data?.current_user?.app_search?.role?.ability?.edit || [],
+ view: data?.current_user?.app_search?.role?.ability?.view || [],
+ credentialTypes: data?.current_user?.app_search?.role?.ability?.credential_types || [],
availableRoleTypes:
- data?.current_user?.app_search_role?.ability?.available_role_types || [],
+ data?.current_user?.app_search?.role?.ability?.available_role_types || [],
},
},
},
workplaceSearch: {
- canCreateInvitations: !!data?.settings?.workplace_search?.can_create_invitations,
- isFederatedAuth: !!data?.settings?.workplace_search?.is_federated_auth,
organization: {
- name: data?.settings?.workplace_search?.organization?.name,
- defaultOrgName: data?.settings?.workplace_search?.organization?.default_org_name,
+ name: data?.current_user?.workplace_search?.organization?.name,
+ defaultOrgName: data?.current_user?.workplace_search?.organization?.default_org_name,
},
- fpAccount: {
- id: data?.settings?.workplace_search?.fp_account.id,
- groups: data?.settings?.workplace_search?.fp_account.groups || [],
- isAdmin: !!data?.settings?.workplace_search?.fp_account?.is_admin,
- canCreatePersonalSources: !!data?.settings?.workplace_search?.fp_account
+ account: {
+ id: data?.current_user?.workplace_search?.account?.id,
+ groups: data?.current_user?.workplace_search?.account?.groups || [],
+ isAdmin: !!data?.current_user?.workplace_search?.account?.is_admin,
+ canCreatePersonalSources: !!data?.current_user?.workplace_search?.account
?.can_create_personal_sources,
- isCurated: !!data?.settings?.workplace_search?.fp_account.is_curated,
- viewedOnboardingPage: !!data?.settings?.workplace_search?.fp_account
- .viewed_onboarding_page,
+ canCreateInvitations: !!data?.current_user?.workplace_search?.account
+ ?.can_create_invitations,
+ isCurated: !!data?.current_user?.workplace_search?.account?.is_curated,
+ viewedOnboardingPage: !!data?.current_user?.workplace_search?.account
+ ?.viewed_onboarding_page,
},
},
};
From 7fc4bb3799542b5813933af989cba678521f1275 Mon Sep 17 00:00:00 2001
From: Pierre Gayvallet
Date: Wed, 9 Sep 2020 19:37:08 +0200
Subject: [PATCH 10/25] IndexMigrator: fix non blocking migration wrapper
promise rejection (#77018)
* fix transformNonBlocking
* add test for indexMigrator
---
.../migrations/core/index_migrator.test.ts | 24 +++++++++++++++++++
.../migrations/core/migrate_raw_docs.test.ts | 14 +++++++++++
.../migrations/core/migrate_raw_docs.ts | 8 +++++--
3 files changed, 44 insertions(+), 2 deletions(-)
diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts
index df89137a1d798..13f771c16bc67 100644
--- a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts
+++ b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts
@@ -369,6 +369,30 @@ describe('IndexMigrator', () => {
],
});
});
+
+ test('rejects when the migration function throws an error', async () => {
+ const { client } = testOpts;
+ const migrateDoc = jest.fn((doc: SavedObjectUnsanitizedDoc) => {
+ throw new Error('error migrating document');
+ });
+
+ testOpts.documentMigrator = {
+ migrationVersion: { foo: '1.2.3' },
+ migrate: migrateDoc,
+ };
+
+ withIndex(client, {
+ numOutOfDate: 1,
+ docs: [
+ [{ _id: 'foo:1', _source: { type: 'foo', foo: { name: 'Bar' } } }],
+ [{ _id: 'foo:2', _source: { type: 'foo', foo: { name: 'Baz' } } }],
+ ],
+ });
+
+ await expect(new IndexMigrator(testOpts).migrate()).rejects.toThrowErrorMatchingInlineSnapshot(
+ `"error migrating document"`
+ );
+ });
});
function withIndex(
diff --git a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts
index 4c9d2e870a7bb..83dc042d2b96b 100644
--- a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts
+++ b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts
@@ -90,4 +90,18 @@ describe('migrateRawDocs', () => {
expect(logger.error).toBeCalledTimes(1);
});
+
+ test('rejects when the transform function throws an error', async () => {
+ const transform = jest.fn((doc: any) => {
+ throw new Error('error during transform');
+ });
+ await expect(
+ migrateRawDocs(
+ new SavedObjectsSerializer(new SavedObjectTypeRegistry()),
+ transform,
+ [{ _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }],
+ createSavedObjectsMigrationLoggerMock()
+ )
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`"error during transform"`);
+ });
});
diff --git a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts
index 2bdf59d25dc74..5a5048d8ad88f 100644
--- a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts
+++ b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts
@@ -78,10 +78,14 @@ function transformNonBlocking(
): (doc: SavedObjectUnsanitizedDoc) => Promise {
// promises aren't enough to unblock the event loop
return (doc: SavedObjectUnsanitizedDoc) =>
- new Promise((resolve) => {
+ new Promise((resolve, reject) => {
// set immediate is though
setImmediate(() => {
- resolve(transform(doc));
+ try {
+ resolve(transform(doc));
+ } catch (e) {
+ reject(e);
+ }
});
});
}
From 7e84661471f95c9e84ea3844b724f730e80d4868 Mon Sep 17 00:00:00 2001
From: Nathan Reese
Date: Wed, 9 Sep 2020 11:44:58 -0600
Subject: [PATCH 11/25] [Maps] convert MetricsEditor to TS (#76727)
* [Maps] convert MetricsEditor to TS
* fix jest test
Co-authored-by: Elastic Machine
---
.../maps/public/components/_index.scss | 2 +-
.../metrics_editor.test.tsx.snap} | 6 +-
.../{ => metrics_editor}/_metric_editors.scss | 0
.../public/components/metrics_editor/index.ts | 7 ++
.../metric_editor.tsx} | 83 +++++++++++++------
.../metric_select.tsx} | 25 +++---
.../metrics_editor.test.tsx} | 7 +-
.../metrics_editor.tsx} | 73 +++++++---------
.../resources/metrics_expression.test.js | 6 --
9 files changed, 112 insertions(+), 97 deletions(-)
rename x-pack/plugins/maps/public/components/{__snapshots__/metrics_editor.test.js.snap => metrics_editor/__snapshots__/metrics_editor.test.tsx.snap} (92%)
rename x-pack/plugins/maps/public/components/{ => metrics_editor}/_metric_editors.scss (100%)
create mode 100644 x-pack/plugins/maps/public/components/metrics_editor/index.ts
rename x-pack/plugins/maps/public/components/{metric_editor.js => metrics_editor/metric_editor.tsx} (59%)
rename x-pack/plugins/maps/public/components/{metric_select.js => metrics_editor/metric_select.tsx} (80%)
rename x-pack/plugins/maps/public/components/{metrics_editor.test.js => metrics_editor/metrics_editor.test.tsx} (84%)
rename x-pack/plugins/maps/public/components/{metrics_editor.js => metrics_editor/metrics_editor.tsx} (54%)
diff --git a/x-pack/plugins/maps/public/components/_index.scss b/x-pack/plugins/maps/public/components/_index.scss
index 76ce9f1bc79e3..726573ce4307d 100644
--- a/x-pack/plugins/maps/public/components/_index.scss
+++ b/x-pack/plugins/maps/public/components/_index.scss
@@ -1,4 +1,4 @@
@import 'action_select';
-@import 'metric_editors';
+@import 'metrics_editor/metric_editors';
@import './geometry_filter';
@import 'tooltip_selector/tooltip_selector';
diff --git a/x-pack/plugins/maps/public/components/__snapshots__/metrics_editor.test.js.snap b/x-pack/plugins/maps/public/components/metrics_editor/__snapshots__/metrics_editor.test.tsx.snap
similarity index 92%
rename from x-pack/plugins/maps/public/components/__snapshots__/metrics_editor.test.js.snap
rename to x-pack/plugins/maps/public/components/metrics_editor/__snapshots__/metrics_editor.test.tsx.snap
index 0d4f1f99e464c..bd58ded41e7f5 100644
--- a/x-pack/plugins/maps/public/components/__snapshots__/metrics_editor.test.js.snap
+++ b/x-pack/plugins/maps/public/components/metrics_editor/__snapshots__/metrics_editor.test.tsx.snap
@@ -16,8 +16,9 @@ exports[`should add default count metric when metrics is empty array 1`] = `
"type": "count",
}
}
- metricsFilter={[Function]}
onChange={[Function]}
+ onRemove={[Function]}
+ showRemoveButton={false}
/>
@@ -59,8 +60,9 @@ exports[`should render metrics editor 1`] = `
"type": "sum",
}
}
- metricsFilter={[Function]}
onChange={[Function]}
+ onRemove={[Function]}
+ showRemoveButton={false}
/>
diff --git a/x-pack/plugins/maps/public/components/_metric_editors.scss b/x-pack/plugins/maps/public/components/metrics_editor/_metric_editors.scss
similarity index 100%
rename from x-pack/plugins/maps/public/components/_metric_editors.scss
rename to x-pack/plugins/maps/public/components/metrics_editor/_metric_editors.scss
diff --git a/x-pack/plugins/maps/public/components/metrics_editor/index.ts b/x-pack/plugins/maps/public/components/metrics_editor/index.ts
new file mode 100644
index 0000000000000..3c105c2d798ff
--- /dev/null
+++ b/x-pack/plugins/maps/public/components/metrics_editor/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { MetricsEditor } from './metrics_editor';
diff --git a/x-pack/plugins/maps/public/components/metric_editor.js b/x-pack/plugins/maps/public/components/metrics_editor/metric_editor.tsx
similarity index 59%
rename from x-pack/plugins/maps/public/components/metric_editor.js
rename to x-pack/plugins/maps/public/components/metrics_editor/metric_editor.tsx
index 96b52d84653b2..543d144efdcc7 100644
--- a/x-pack/plugins/maps/public/components/metric_editor.js
+++ b/x-pack/plugins/maps/public/components/metrics_editor/metric_editor.tsx
@@ -4,18 +4,20 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { Fragment } from 'react';
-import PropTypes from 'prop-types';
+import React, { ChangeEvent, Fragment } from 'react';
import { i18n } from '@kbn/i18n';
-import { EuiFieldText, EuiFormRow } from '@elastic/eui';
+import { EuiButtonEmpty, EuiComboBoxOptionOption, EuiFieldText, EuiFormRow } from '@elastic/eui';
-import { MetricSelect, METRIC_AGGREGATION_VALUES } from './metric_select';
-import { SingleFieldSelect } from './single_field_select';
-import { AGG_TYPE } from '../../common/constants';
-import { getTermsFields } from '../index_pattern_util';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { MetricSelect } from './metric_select';
+import { SingleFieldSelect } from '../single_field_select';
+import { AggDescriptor } from '../../../common/descriptor_types';
+import { AGG_TYPE } from '../../../common/constants';
+import { getTermsFields } from '../../index_pattern_util';
+import { IFieldType } from '../../../../../../src/plugins/data/public';
-function filterFieldsForAgg(fields, aggType) {
+function filterFieldsForAgg(fields: IFieldType[], aggType: AGG_TYPE) {
if (!fields) {
return [];
}
@@ -34,8 +36,27 @@ function filterFieldsForAgg(fields, aggType) {
});
}
-export function MetricEditor({ fields, metricsFilter, metric, onChange, removeButton }) {
- const onAggChange = (metricAggregationType) => {
+interface Props {
+ metric: AggDescriptor;
+ fields: IFieldType[];
+ onChange: (metric: AggDescriptor) => void;
+ onRemove: () => void;
+ metricsFilter?: (metricOption: EuiComboBoxOptionOption) => boolean;
+ showRemoveButton: boolean;
+}
+
+export function MetricEditor({
+ fields,
+ metricsFilter,
+ metric,
+ onChange,
+ showRemoveButton,
+ onRemove,
+}: Props) {
+ const onAggChange = (metricAggregationType?: AGG_TYPE) => {
+ if (!metricAggregationType) {
+ return;
+ }
const newMetricProps = {
...metric,
type: metricAggregationType,
@@ -54,13 +75,16 @@ export function MetricEditor({ fields, metricsFilter, metric, onChange, removeBu
onChange(newMetricProps);
};
- const onFieldChange = (fieldName) => {
+ const onFieldChange = (fieldName?: string) => {
+ if (!fieldName) {
+ return;
+ }
onChange({
...metric,
field: fieldName,
});
};
- const onLabelChange = (e) => {
+ const onLabelChange = (e: ChangeEvent) => {
onChange({
...metric,
label: e.target.value,
@@ -80,7 +104,7 @@ export function MetricEditor({ fields, metricsFilter, metric, onChange, removeBu
placeholder={i18n.translate('xpack.maps.metricsEditor.selectFieldPlaceholder', {
defaultMessage: 'Select field',
})}
- value={metric.field}
+ value={metric.field ? metric.field : null}
onChange={onFieldChange}
fields={filterFieldsForAgg(fields, metric.type)}
isClearable={false}
@@ -108,6 +132,28 @@ export function MetricEditor({ fields, metricsFilter, metric, onChange, removeBu
);
}
+ let removeButton;
+ if (showRemoveButton) {
+ removeButton = (
+
+
+
+
+
+ );
+ }
+
return (
);
}
-
-MetricEditor.propTypes = {
- metric: PropTypes.shape({
- type: PropTypes.oneOf(METRIC_AGGREGATION_VALUES),
- field: PropTypes.string,
- label: PropTypes.string,
- }),
- fields: PropTypes.array,
- onChange: PropTypes.func.isRequired,
- metricsFilter: PropTypes.func,
-};
diff --git a/x-pack/plugins/maps/public/components/metric_select.js b/x-pack/plugins/maps/public/components/metrics_editor/metric_select.tsx
similarity index 80%
rename from x-pack/plugins/maps/public/components/metric_select.js
rename to x-pack/plugins/maps/public/components/metrics_editor/metric_select.tsx
index 2ebfcf99dece6..197c5466fe0fd 100644
--- a/x-pack/plugins/maps/public/components/metric_select.js
+++ b/x-pack/plugins/maps/public/components/metrics_editor/metric_select.tsx
@@ -5,10 +5,9 @@
*/
import React from 'react';
-import PropTypes from 'prop-types';
import { i18n } from '@kbn/i18n';
-import { EuiComboBox } from '@elastic/eui';
-import { AGG_TYPE } from '../../common/constants';
+import { EuiComboBox, EuiComboBoxOptionOption, EuiComboBoxProps } from '@elastic/eui';
+import { AGG_TYPE } from '../../../common/constants';
const AGG_OPTIONS = [
{
@@ -55,17 +54,19 @@ const AGG_OPTIONS = [
},
];
-export const METRIC_AGGREGATION_VALUES = AGG_OPTIONS.map(({ value }) => {
- return value;
-});
+type Props = Omit, 'onChange'> & {
+ value: AGG_TYPE;
+ onChange: (aggType: AGG_TYPE) => void;
+ metricsFilter?: (metricOption: EuiComboBoxOptionOption) => boolean;
+};
-export function MetricSelect({ value, onChange, metricsFilter, ...rest }) {
- function onAggChange(selectedOptions) {
+export function MetricSelect({ value, onChange, metricsFilter, ...rest }: Props) {
+ function onAggChange(selectedOptions: Array>) {
if (selectedOptions.length === 0) {
return;
}
- const aggType = selectedOptions[0].value;
+ const aggType = selectedOptions[0].value!;
onChange(aggType);
}
@@ -87,9 +88,3 @@ export function MetricSelect({ value, onChange, metricsFilter, ...rest }) {
/>
);
}
-
-MetricSelect.propTypes = {
- metricsFilter: PropTypes.func,
- value: PropTypes.oneOf(METRIC_AGGREGATION_VALUES),
- onChange: PropTypes.func.isRequired,
-};
diff --git a/x-pack/plugins/maps/public/components/metrics_editor.test.js b/x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.test.tsx
similarity index 84%
rename from x-pack/plugins/maps/public/components/metrics_editor.test.js
rename to x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.test.tsx
index bcbeef29875ee..7ce7fbce2b066 100644
--- a/x-pack/plugins/maps/public/components/metrics_editor.test.js
+++ b/x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.test.tsx
@@ -7,7 +7,7 @@
import React from 'react';
import { shallow } from 'enzyme';
import { MetricsEditor } from './metrics_editor';
-import { AGG_TYPE } from '../../common/constants';
+import { AGG_TYPE } from '../../../common/constants';
const defaultProps = {
metrics: [
@@ -19,15 +19,14 @@ const defaultProps = {
fields: [],
onChange: () => {},
allowMultipleMetrics: true,
- metricsFilter: () => {},
};
-test('should render metrics editor', async () => {
+test('should render metrics editor', () => {
const component = shallow();
expect(component).toMatchSnapshot();
});
-test('should add default count metric when metrics is empty array', async () => {
+test('should add default count metric when metrics is empty array', () => {
const component = shallow();
expect(component).toMatchSnapshot();
});
diff --git a/x-pack/plugins/maps/public/components/metrics_editor.js b/x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.tsx
similarity index 54%
rename from x-pack/plugins/maps/public/components/metrics_editor.js
rename to x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.tsx
index 7d4d7bf3ec7ab..17cfc5f62fee5 100644
--- a/x-pack/plugins/maps/public/components/metrics_editor.js
+++ b/x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.tsx
@@ -5,48 +5,43 @@
*/
import React, { Fragment } from 'react';
-import PropTypes from 'prop-types';
-import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiButtonEmpty, EuiSpacer, EuiTextAlign } from '@elastic/eui';
+import { EuiButtonEmpty, EuiComboBoxOptionOption, EuiSpacer, EuiTextAlign } from '@elastic/eui';
import { MetricEditor } from './metric_editor';
-import { DEFAULT_METRIC } from '../classes/sources/es_agg_source';
+// @ts-expect-error
+import { DEFAULT_METRIC } from '../../classes/sources/es_agg_source';
+import { IFieldType } from '../../../../../../src/plugins/data/public';
+import { AggDescriptor } from '../../../common/descriptor_types';
+import { AGG_TYPE } from '../../../common/constants';
-export function MetricsEditor({ fields, metrics, onChange, allowMultipleMetrics, metricsFilter }) {
+interface Props {
+ allowMultipleMetrics: boolean;
+ metrics: AggDescriptor[];
+ fields: IFieldType[];
+ onChange: (metrics: AggDescriptor[]) => void;
+ metricsFilter?: (metricOption: EuiComboBoxOptionOption) => boolean;
+}
+
+export function MetricsEditor({
+ fields,
+ metrics = [DEFAULT_METRIC],
+ onChange,
+ allowMultipleMetrics = true,
+ metricsFilter,
+}: Props) {
function renderMetrics() {
// There was a bug in 7.8 that initialized metrics to [].
// This check is needed to handle any saved objects created before the bug was patched.
const nonEmptyMetrics = metrics.length === 0 ? [DEFAULT_METRIC] : metrics;
return nonEmptyMetrics.map((metric, index) => {
- const onMetricChange = (metric) => {
- onChange([...metrics.slice(0, index), metric, ...metrics.slice(index + 1)]);
+ const onMetricChange = (updatedMetric: AggDescriptor) => {
+ onChange([...metrics.slice(0, index), updatedMetric, ...metrics.slice(index + 1)]);
};
const onRemove = () => {
onChange([...metrics.slice(0, index), ...metrics.slice(index + 1)]);
};
- let removeButton;
- if (index > 0) {
- removeButton = (
-
-
-
-
-
- );
- }
return (
0}
+ onRemove={onRemove}
/>
);
@@ -62,7 +58,7 @@ export function MetricsEditor({ fields, metrics, onChange, allowMultipleMetrics,
}
function addMetric() {
- onChange([...metrics, {}]);
+ onChange([...metrics, { type: AGG_TYPE.AVG }]);
}
function renderAddMetricButton() {
@@ -71,7 +67,7 @@ export function MetricsEditor({ fields, metrics, onChange, allowMultipleMetrics,
}
return (
- <>
+
@@ -81,7 +77,7 @@ export function MetricsEditor({ fields, metrics, onChange, allowMultipleMetrics,
/>
- >
+
);
}
@@ -93,16 +89,3 @@ export function MetricsEditor({ fields, metrics, onChange, allowMultipleMetrics,
);
}
-
-MetricsEditor.propTypes = {
- metrics: PropTypes.array,
- fields: PropTypes.array,
- onChange: PropTypes.func.isRequired,
- allowMultipleMetrics: PropTypes.bool,
- metricsFilter: PropTypes.func,
-};
-
-MetricsEditor.defaultProps = {
- metrics: [DEFAULT_METRIC],
- allowMultipleMetrics: true,
-};
diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.test.js b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.test.js
index 3cd8a3c42879a..e0e1556ecde06 100644
--- a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.test.js
+++ b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.test.js
@@ -4,12 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-jest.mock('../../../../components/metric_editor', () => ({
- MetricsEditor: () => {
- return mockMetricsEditor
;
- },
-}));
-
import React from 'react';
import { shallow } from 'enzyme';
import { MetricsExpression } from './metrics_expression';
From 42a693475dbec77cba624a6b58f492d456aefddf Mon Sep 17 00:00:00 2001
From: Marshall Main <55718608+marshallmain@users.noreply.github.com>
Date: Wed, 9 Sep 2020 14:05:01 -0400
Subject: [PATCH 12/25] [Security_solution][Detections] Refactor signal
ancestry to allow multiple parents (#76531)
* Refactors signal ancestry to allow multiple parents
* Fix depth calculation for 7.10+ signals on pre-7.10 signals
* Comment build_signal functions
* Rename buildAncestorsSignal to buildAncestors
* Update detection engine depth test scripts and docs
* Update halting test readme
* Match up rule ids in readme
* Continue populating signal.parent along with signal.parents
* pr comments
Co-authored-by: Elastic Machine
---
.../routes/index/signals_mapping.json | 25 ++
.../signals_on_signals/depth_test/README.md | 244 +++++++++--------
.../depth_test/query_single_id.json | 2 +-
.../depth_test/signal_on_signal_depth_1.json | 4 +-
.../depth_test/signal_on_signal_depth_2.json | 4 +-
.../signals_on_signals/halting_test/README.md | 182 +++++++------
.../signals/__mocks__/es_results.ts | 10 +-
.../signals/build_bulk_body.test.ts | 60 +++--
.../signals/build_bulk_body.ts | 9 +-
.../signals/build_rule.test.ts | 102 ++++++-
.../detection_engine/signals/build_rule.ts | 16 +-
.../signals/build_signal.test.ts | 250 ++++--------------
.../detection_engine/signals/build_signal.ts | 82 +++---
.../signals/signal_rule_alert_type.ts | 3 +-
.../signals/single_bulk_create.test.ts | 32 +--
.../signals/single_bulk_create.ts | 5 +-
.../lib/detection_engine/signals/types.ts | 19 +-
17 files changed, 568 insertions(+), 481 deletions(-)
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json
index 7d80a319e9e52..cfce019910071 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json
@@ -22,11 +22,33 @@
}
}
},
+ "parents": {
+ "properties": {
+ "rule": {
+ "type": "keyword"
+ },
+ "index": {
+ "type": "keyword"
+ },
+ "id": {
+ "type": "keyword"
+ },
+ "type": {
+ "type": "keyword"
+ },
+ "depth": {
+ "type": "long"
+ }
+ }
+ },
"ancestors": {
"properties": {
"rule": {
"type": "keyword"
},
+ "index": {
+ "type": "keyword"
+ },
"id": {
"type": "keyword"
},
@@ -299,6 +321,9 @@
},
"threshold_count": {
"type": "float"
+ },
+ "depth": {
+ "type": "integer"
}
}
}
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/README.md b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/README.md
index 2310ba979da20..7cf7d11e4c1f8 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/README.md
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/README.md
@@ -22,7 +22,7 @@ which will write a single signal document into the signals index by searching fo
signal_on_signal_depth_1.json
```
-which has this key part of its query: `"query": "signal.parent.depth: 1 and _id: *"` which will only create signals
+which has this key part of its query: `"query": "signal.depth: 1 and _id: *"` which will only create signals
from all signals that point directly to an event (signal -> event).
Then a second rule called
@@ -34,7 +34,7 @@ signal_on_signal_depth_2.json
which will only create signals from all signals that point directly to another signal (signal -> signal) with this query
```json
-"query": "signal.parent.depth: 2 and _id: *"
+"query": "signal.depth: 2 and _id: *"
```
## Setup
@@ -90,38 +90,43 @@ And then you can query against that:
GET .siem-signals-default/_search
```
-Check your parent section of the signal and you will see something like this:
+Check your `signal` section of the signal and you will see something like this:
```json
-"parent" : {
- "rule" : "74e0dd0c-4609-416f-b65e-90f8b2564612",
- "id" : "o8G7vm8BvLT8jmu5B1-M",
- "type" : "event",
- "index" : "filebeat-8.0.0-2019.12.18-000001",
- "depth" : 1
-},
-"ancestors" : [
+"parents" : [
{
- "rule" : "74e0dd0c-4609-416f-b65e-90f8b2564612",
"id" : "o8G7vm8BvLT8jmu5B1-M",
"type" : "event",
"index" : "filebeat-8.0.0-2019.12.18-000001",
- "depth" : 1
+ "depth" : 0
}
-]
+],
+"ancestors" : [
+ {
+ "id" : "o8G7vm8BvLT8jmu5B1-M",
+ "type" : "event",
+ "index" : "filebeat-8.0.0-2019.12.18-000001",
+ "depth" : 0
+ },
+],
+"depth": 1,
+"rule": {
+ "id": "74e0dd0c-4609-416f-b65e-90f8b2564612"
+}
```
-The parent and ancestors structure is defined as:
+The parents structure is defined as:
```
-rule -> The id of the rule. You can view the rule by ./get_rule_by_rule_id.sh ded57b36-9c4e-4ee4-805d-be4e92033e41
+rule -> The id of the rule, if the parent was generated by a rule. You can view the rule by ./get_rule_by_rule_id.sh ded57b36-9c4e-4ee4-805d-be4e92033e41
id -> The original _id of the document
type -> The type of the document, it will be either event or signal
index -> The original location of the index
-depth -> The depth of this signal. It will be at least 1 to indicate it is a signal generated from a event. Otherwise 2 or more to indicate a signal on signal and what depth we are at
-ancestors -> An array tracking all of the parents of this particular signal. As depth increases this will too.
+depth -> The depth of the parent event/signal. It will be 0 if the parent is an event, or 1+ if the parent is another signal.
```
+The ancestors structure has the same fields as parents, but is an array of all ancestors (parents, grandparents, etc) of the signal.
+
This is indicating that you have a single parent of an event from the signal (signal -> event) and this document has a single
ancestor of that event. Each 30 seconds that goes it will use de-duplication technique to ensure that this signal is not re-inserted. If after
each 30 seconds you DO SEE multiple signals then the bug is a de-duplication bug and a critical bug. If you ever see a duplicate rule in the
@@ -138,55 +143,64 @@ running in the system which are generating signals on top of signals. After 30 s
documents in the signals index. The first signal is our original (signal -> event) document with a rule id:
```json
-"parent" : {
- "rule" : "74e0dd0c-4609-416f-b65e-90f8b2564612",
- "id" : "o8G7vm8BvLT8jmu5B1-M",
- "type" : "event",
- "index" : "filebeat-8.0.0-2019.12.18-000001",
- "depth" : 1
-},
+"parents" : [
+ {
+ "id" : "o8G7vm8BvLT8jmu5B1-M",
+ "type" : "event",
+ "index" : "filebeat-8.0.0-2019.12.18-000001",
+ "depth" : 0
+ }
+],
"ancestors" : [
{
- "rule" : "74e0dd0c-4609-416f-b65e-90f8b2564612",
"id" : "o8G7vm8BvLT8jmu5B1-M",
"type" : "event",
"index" : "filebeat-8.0.0-2019.12.18-000001",
- "depth" : 1
+ "depth" : 0
}
-]
+],
+"depth": 1,
+"rule": {
+ "id": "74e0dd0c-4609-416f-b65e-90f8b2564612"
+}
```
and the second document is a signal on top of a signal like so:
```json
-"parent" : {
- "rule" : "1d3b3735-66ef-4e53-b7f5-4340026cc40c",
- "id" : "4cc69c1cbecdd2ace4075fd1d8a5c28e7d46e4bf31aecc8d2da39252c50c96b4",
- "type" : "signal",
- "index" : ".siem-signals-default-000001",
- "depth" : 2
-},
-"ancestors" : [
+"parents" : [
{
"rule" : "74e0dd0c-4609-416f-b65e-90f8b2564612",
+ "id" : "4cc69c1cbecdd2ace4075fd1d8a5c28e7d46e4bf31aecc8d2da39252c50c96b4",
+ "type" : "signal",
+ "index" : ".siem-signals-default-000001",
+ "depth" : 1
+ }
+]
+"ancestors" : [
+ {
"id" : "o8G7vm8BvLT8jmu5B1-M",
"type" : "event",
"index" : "filebeat-8.0.0-2019.12.18-000001",
- "depth" : 1
+ "depth" : 0
},
{
- "rule" : "1d3b3735-66ef-4e53-b7f5-4340026cc40c",
+ "rule" : "74e0dd0c-4609-416f-b65e-90f8b2564612",
"id" : "4cc69c1cbecdd2ace4075fd1d8a5c28e7d46e4bf31aecc8d2da39252c50c96b4",
"type" : "signal",
"index" : ".siem-signals-default-000001",
- "depth" : 2
+ "depth" : 1
}
-]
+],
+"depth": 2,
+"rule": {
+ "id": "1d3b3735-66ef-4e53-b7f5-4340026cc40c"
+}
```
Notice that the depth indicates it is at level 2 and its parent is that of a signal. Also notice that the ancestors is an array of size 2
indicating that this signal terminates at an event. Each and every signal ancestors array should terminate at an event and should ONLY contain 1
-event and NEVER 2 or more events. After 30+ seconds you should NOT see any new documents being created and you should be stable
+event and NEVER 2 or more events for KQL query based rules. EQL query based rules that use sequences may have multiple parents at the same level. After 30+ seconds you should NOT see any new documents being created and you should be stable
at 2. Otherwise we have AND/OR a de-duplication issue, signal on signal issue.
Now, post this same rule a second time as a second instance which is going to run against these two documents.
@@ -212,79 +226,93 @@ The expected behavior is that eventually you will get 3 total documents but not
The original event rule 74e0dd0c-4609-416f-b65e-90f8b2564612 (event -> signal)
```json
-"parent" : {
- "rule" : "74e0dd0c-4609-416f-b65e-90f8b2564612",
- "id" : "o8G7vm8BvLT8jmu5B1-M",
- "type" : "event",
- "index" : "filebeat-8.0.0-2019.12.18-000001",
- "depth" : 1
-},
+"parents" : [
+ {
+ "id" : "o8G7vm8BvLT8jmu5B1-M",
+ "type" : "event",
+ "index" : "filebeat-8.0.0-2019.12.18-000001",
+ "depth" : 0
+ }
+],
"ancestors" : [
{
- "rule" : "74e0dd0c-4609-416f-b65e-90f8b2564612",
"id" : "o8G7vm8BvLT8jmu5B1-M",
"type" : "event",
"index" : "filebeat-8.0.0-2019.12.18-000001",
- "depth" : 1
+ "depth" : 0
}
-]
+],
+"depth": 1,
+"rule": {
+ "id": "74e0dd0c-4609-416f-b65e-90f8b2564612"
+}
```
The first signal to signal rule 1d3b3735-66ef-4e53-b7f5-4340026cc40c (signal -> event)
```json
-"parent" : {
- "rule" : "1d3b3735-66ef-4e53-b7f5-4340026cc40c",
- "id" : "4cc69c1cbecdd2ace4075fd1d8a5c28e7d46e4bf31aecc8d2da39252c50c96b4",
- "type" : "signal",
- "index" : ".siem-signals-default-000001",
- "depth" : 2
-},
-"ancestors" : [
+"parents" : [
{
"rule" : "74e0dd0c-4609-416f-b65e-90f8b2564612",
+ "id" : "4cc69c1cbecdd2ace4075fd1d8a5c28e7d46e4bf31aecc8d2da39252c50c96b4",
+ "type" : "signal",
+ "index" : ".siem-signals-default-000001",
+ "depth" : 1
+ }
+]
+"ancestors" : [
+ {
"id" : "o8G7vm8BvLT8jmu5B1-M",
"type" : "event",
"index" : "filebeat-8.0.0-2019.12.18-000001",
- "depth" : 1
+ "depth" : 0
},
{
- "rule" : "1d3b3735-66ef-4e53-b7f5-4340026cc40c",
+ "rule" : "74e0dd0c-4609-416f-b65e-90f8b2564612",
"id" : "4cc69c1cbecdd2ace4075fd1d8a5c28e7d46e4bf31aecc8d2da39252c50c96b4",
"type" : "signal",
"index" : ".siem-signals-default-000001",
- "depth" : 2
+ "depth" : 1
}
-]
+],
+"depth": 2,
+"rule": {
+ "id": "1d3b3735-66ef-4e53-b7f5-4340026cc40c"
+}
```
Then our second signal to signal rule c93ddb57-e7e9-4973-9886-72ddefb4d22e (signal -> event) which finds the same thing as the first
signal to signal
```json
-"parent" : {
- "rule" : "c93ddb57-e7e9-4973-9886-72ddefb4d22e",
- "id" : "4cc69c1cbecdd2ace4075fd1d8a5c28e7d46e4bf31aecc8d2da39252c50c96b4",
- "type" : "signal",
- "index" : ".siem-signals-default-000001",
- "depth" : 2
-},
-"ancestors" : [
+"parents" : [
{
"rule" : "74e0dd0c-4609-416f-b65e-90f8b2564612",
+ "id" : "4cc69c1cbecdd2ace4075fd1d8a5c28e7d46e4bf31aecc8d2da39252c50c96b4",
+ "type" : "signal",
+ "index" : ".siem-signals-default-000001",
+ "depth" : 1
+ }
+],
+"ancestors" : [
+ {
"id" : "o8G7vm8BvLT8jmu5B1-M",
"type" : "event",
"index" : "filebeat-8.0.0-2019.12.18-000001",
- "depth" : 1
+ "depth" : 0
},
{
- "rule" : "c93ddb57-e7e9-4973-9886-72ddefb4d22e",
+ "rule" : "74e0dd0c-4609-416f-b65e-90f8b2564612",
"id" : "4cc69c1cbecdd2ace4075fd1d8a5c28e7d46e4bf31aecc8d2da39252c50c96b4",
"type" : "signal",
"index" : ".siem-signals-default-000001",
- "depth" : 2
+ "depth" : 1
}
-]
+],
+"depth": 2,
+"rule": {
+ "id": "c93ddb57-e7e9-4973-9886-72ddefb4d22e"
+}
```
We should be able to post this depth level as many times as we want and get only 1 new document each time. If we decide though to
@@ -298,69 +326,79 @@ The expectation is that a document for each of the previous depth 1 documents wo
depth 1 rules running then the signals at depth 2 will produce two new ones and those two will look like so:
```json
-"parent" : {
- "rule" : "a1f7b520-5bfd-451d-af59-428f60753fee",
- "id" : "365236ce5e77770508152403b4e16613f407ae4b1a135a450dcfec427f2a3231",
- "type" : "signal",
- "index" : ".siem-signals-default-000001",
- "depth" : 3
-},
+"parents" : [
+ {
+ "rule" : "1d3b3735-66ef-4e53-b7f5-4340026cc40c",
+ "id" : "365236ce5e77770508152403b4e16613f407ae4b1a135a450dcfec427f2a3231",
+ "type" : "signal",
+ "index" : ".siem-signals-default-000001",
+ "depth" : 2
+ }
+],
"ancestors" : [
{
- "rule" : "74e0dd0c-4609-416f-b65e-90f8b2564612",
"id" : "o8G7vm8BvLT8jmu5B1-M",
"type" : "event",
"index" : "filebeat-8.0.0-2019.12.18-000001",
- "depth" : 1
+ "depth" : 0
},
{
- "rule" : "1d3b3735-66ef-4e53-b7f5-4340026cc40c",
+ "rule" : "74e0dd0c-4609-416f-b65e-90f8b2564612",
"id" : "4cc69c1cbecdd2ace4075fd1d8a5c28e7d46e4bf31aecc8d2da39252c50c96b4",
"type" : "signal",
"index" : ".siem-signals-default-000001",
- "depth" : 2
+ "depth" : 1
},
{
- "rule" : "a1f7b520-5bfd-451d-af59-428f60753fee",
+ "rule" : "1d3b3735-66ef-4e53-b7f5-4340026cc40c",
"id" : "365236ce5e77770508152403b4e16613f407ae4b1a135a450dcfec427f2a3231",
"type" : "signal",
"index" : ".siem-signals-default-000001",
- "depth" : 3
+ "depth" : 2
}
-]
+],
+"depth": 3,
+"rule": {
+ "id": "a1f7b520-5bfd-451d-af59-428f60753fee"
+}
```
```json
-"parent" : {
- "rule" : "a1f7b520-5bfd-451d-af59-428f60753fee",
- "id" : "e8b1f1adb40fd642fa524dea89ef94232e67b05e99fb0b2683f1e47e90b759fb",
- "type" : "signal",
- "index" : ".siem-signals-default-000001",
- "depth" : 3
-},
+"parents" : [
+ {
+ "rule" : "c93ddb57-e7e9-4973-9886-72ddefb4d22e",
+ "id" : "e8b1f1adb40fd642fa524dea89ef94232e67b05e99fb0b2683f1e47e90b759fb",
+ "type" : "signal",
+ "index" : ".siem-signals-default-000001",
+ "depth" : 2
+ }
+],
"ancestors" : [
{
- "rule" : "74e0dd0c-4609-416f-b65e-90f8b2564612",
"id" : "o8G7vm8BvLT8jmu5B1-M",
"type" : "event",
"index" : "filebeat-8.0.0-2019.12.18-000001",
- "depth" : 1
+ "depth" : 0
},
{
- "rule" : "c93ddb57-e7e9-4973-9886-72ddefb4d22e",
+ "rule" : "74e0dd0c-4609-416f-b65e-90f8b2564612",
"id" : "4cc69c1cbecdd2ace4075fd1d8a5c28e7d46e4bf31aecc8d2da39252c50c96b4",
"type" : "signal",
"index" : ".siem-signals-default-000001",
- "depth" : 2
+ "depth" : 1
},
{
- "rule" : "a1f7b520-5bfd-451d-af59-428f60753fee",
+ "rule" : "c93ddb57-e7e9-4973-9886-72ddefb4d22e",
"id" : "e8b1f1adb40fd642fa524dea89ef94232e67b05e99fb0b2683f1e47e90b759fb",
"type" : "signal",
"index" : ".siem-signals-default-000001",
- "depth" : 3
+ "depth" : 2
}
-]
+],
+"depth": 3,
+"rule": {
+ "id": "a1f7b520-5bfd-451d-af59-428f60753fee"
+}
```
The total number of documents should be 5 at this point. If you were to post this same rule a second time to get a second instance
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/query_single_id.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/query_single_id.json
index dc05c656d7cf1..305aa34992623 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/query_single_id.json
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/query_single_id.json
@@ -7,6 +7,6 @@
"from": "now-1d",
"interval": "30s",
"to": "now",
- "query": "_id: o8G7vm8BvLT8jmu5B1-M",
+ "query": "event.id: 08cde4aa-d249-4e6b-8300-06f3d56c7fe7",
"enabled": true
}
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/signal_on_signal_depth_1.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/signal_on_signal_depth_1.json
index fb13413a02791..c9132ddb0a590 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/signal_on_signal_depth_1.json
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/signal_on_signal_depth_1.json
@@ -7,7 +7,7 @@
"from": "now-1d",
"interval": "30s",
"to": "now",
- "query": "signal.parent.depth: 1 and _id: *",
+ "query": "signal.depth: 1 and _id: *",
"enabled": true,
- "index": ".siem-signals-default"
+ "index": [".siem-signals-default"]
}
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/signal_on_signal_depth_2.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/signal_on_signal_depth_2.json
index c1b7594653ec7..d1a2749792686 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/signal_on_signal_depth_2.json
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/signal_on_signal_depth_2.json
@@ -7,7 +7,7 @@
"from": "now-1d",
"interval": "30s",
"to": "now",
- "query": "signal.parent.depth: 2 and _id: *",
+ "query": "signal.depth: 2 and _id: *",
"enabled": true,
- "index": ".siem-signals-default"
+ "index": [".siem-signals-default"]
}
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/halting_test/README.md b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/halting_test/README.md
index b1a83f5317776..01b21bf762e44 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/halting_test/README.md
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/halting_test/README.md
@@ -69,38 +69,43 @@ And then you can query against that:
GET .siem-signals-default/_search
```
-Check your parent section of the signal and you will see something like this:
+Check your `signal` section of the signal and you will see something like this:
```json
-"parent" : {
- "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41",
- "id" : "o8G7vm8BvLT8jmu5B1-M",
- "type" : "event",
- "index" : "filebeat-8.0.0-2019.12.18-000001",
- "depth" : 1
-},
-"ancestors" : [
+"parents" : [
{
- "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41",
"id" : "o8G7vm8BvLT8jmu5B1-M",
"type" : "event",
"index" : "filebeat-8.0.0-2019.12.18-000001",
- "depth" : 1
+ "depth" : 0
}
-]
+],
+"ancestors" : [
+ {
+ "id" : "o8G7vm8BvLT8jmu5B1-M",
+ "type" : "event",
+ "index" : "filebeat-8.0.0-2019.12.18-000001",
+ "depth" : 0
+ },
+],
+"depth": 1,
+"rule": {
+ "id": "ded57b36-9c4e-4ee4-805d-be4e92033e41"
+}
```
-The parent and ancestors structure is defined as:
+The parents structure is defined as:
```
-rule -> The id of the rule. You can view the rule by ./get_rule_by_rule_id.sh ded57b36-9c4e-4ee4-805d-be4e92033e41
+rule -> The id of the rule, if the parent was generated by a rule. You can view the rule by ./get_rule_by_rule_id.sh ded57b36-9c4e-4ee4-805d-be4e92033e41
id -> The original _id of the document
type -> The type of the document, it will be either event or signal
index -> The original location of the index
-depth -> The depth of this signal. It will be at least 1 to indicate it is a signal generated from a event. Otherwise 2 or more to indicate a signal on signal and what depth we are at
-ancestors -> An array tracking all of the parents of this particular signal. As depth increases this will too.
+depth -> The depth of the parent event/signal. It will be 0 if the parent is an event, or 1+ if the parent is another signal.
```
+The ancestors structure has the same fields as parents, but is an array of all ancestors (parents, grandparents, etc) of the signal.
+
This is indicating that you have a single parent of an event from the signal (signal -> event) and this document has a single
ancestor of that event. Each 30 seconds that goes it will use de-duplication technique to ensure that this signal is not re-inserted. If after
each 30 seconds you DO SEE multiple signals then the bug is a de-duplication bug and a critical bug. If you ever see a duplicate rule in the
@@ -119,22 +124,26 @@ documents in the signals index. The first signal is our original (signal -> even
(signal -> event)
```json
-"parent" : {
- "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41",
- "id" : "o8G7vm8BvLT8jmu5B1-M",
- "type" : "event",
- "index" : "filebeat-8.0.0-2019.12.18-000001",
- "depth" : 1
-},
-"ancestors" : [
+"parents" : [
{
- "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41",
"id" : "o8G7vm8BvLT8jmu5B1-M",
"type" : "event",
"index" : "filebeat-8.0.0-2019.12.18-000001",
- "depth" : 1
+ "depth" : 0
}
-]
+],
+"ancestors" : [
+ {
+ "id" : "o8G7vm8BvLT8jmu5B1-M",
+ "type" : "event",
+ "index" : "filebeat-8.0.0-2019.12.18-000001",
+ "depth" : 0
+ },
+],
+"depth": 1,
+"rule": {
+ "id": "ded57b36-9c4e-4ee4-805d-be4e92033e41"
+}
```
and the second document is a signal on top of a signal like so:
@@ -143,28 +152,31 @@ and the second document is a signal on top of a signal like so:
```json
"parent" : {
- "rule" : "161fa5b8-0b96-4985-b066-0d99b2bcb904",
+ "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41",
"id" : "9d8710925adbf1a9c469621805407e74334dd08ca2c2ea414840fe971a571938",
"type" : "signal",
"index" : ".siem-signals-default-000001",
- "depth" : 2
+ "depth" : 1
},
"ancestors" : [
{
- "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41",
"id" : "o8G7vm8BvLT8jmu5B1-M",
"type" : "event",
"index" : "filebeat-8.0.0-2019.12.18-000001",
- "depth" : 1
+ "depth" : 0
},
{
- "rule" : "161fa5b8-0b96-4985-b066-0d99b2bcb904",
+ "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41",
"id" : "9d8710925adbf1a9c469621805407e74334dd08ca2c2ea414840fe971a571938",
"type" : "signal",
"index" : ".siem-signals-default-000001",
- "depth" : 2
+ "depth" : 1
}
-]
+],
+"depth": 2,
+"rule": {
+ "id": "161fa5b8-0b96-4985-b066-0d99b2bcb904"
+}
```
Notice that the depth indicates it is at level 2 and its parent is that of a signal. Also notice that the ancestors is an array of size 2
@@ -195,50 +207,57 @@ The expected behavior is that eventually you will get 5 total documents but not
The original event rule ded57b36-9c4e-4ee4-805d-be4e92033e41 (event -> signal)
```json
-"parent" : {
- "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41",
- "id" : "o8G7vm8BvLT8jmu5B1-M",
- "type" : "event",
- "index" : "filebeat-8.0.0-2019.12.18-000001",
- "depth" : 1
-},
-"ancestors" : [
+"parents" : [
{
- "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41",
"id" : "o8G7vm8BvLT8jmu5B1-M",
"type" : "event",
"index" : "filebeat-8.0.0-2019.12.18-000001",
- "depth" : 1
+ "depth" : 0
}
-]
+],
+"ancestors" : [
+ {
+ "id" : "o8G7vm8BvLT8jmu5B1-M",
+ "type" : "event",
+ "index" : "filebeat-8.0.0-2019.12.18-000001",
+ "depth" : 0
+ },
+],
+"depth": 1,
+"rule": {
+ "id": "ded57b36-9c4e-4ee4-805d-be4e92033e41"
+}
```
The first signal to signal rule 161fa5b8-0b96-4985-b066-0d99b2bcb904 (signal -> event)
```json
"parent" : {
- "rule" : "161fa5b8-0b96-4985-b066-0d99b2bcb904",
+ "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41",
"id" : "9d8710925adbf1a9c469621805407e74334dd08ca2c2ea414840fe971a571938",
"type" : "signal",
"index" : ".siem-signals-default-000001",
- "depth" : 2
+ "depth" : 1
},
"ancestors" : [
{
- "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41",
"id" : "o8G7vm8BvLT8jmu5B1-M",
"type" : "event",
"index" : "filebeat-8.0.0-2019.12.18-000001",
- "depth" : 1
+ "depth" : 0
},
{
- "rule" : "161fa5b8-0b96-4985-b066-0d99b2bcb904",
+ "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41",
"id" : "9d8710925adbf1a9c469621805407e74334dd08ca2c2ea414840fe971a571938",
"type" : "signal",
"index" : ".siem-signals-default-000001",
- "depth" : 2
+ "depth" : 1
}
-]
+],
+"depth": 2,
+"rule": {
+ "id": "161fa5b8-0b96-4985-b066-0d99b2bcb904"
+}
```
Then our second signal to signal rule f2b70c4a-4d8f-4db5-9ed7-d3ab0630e406 (signal -> event) which finds the same thing as the first
@@ -246,28 +265,31 @@ signal to signal
```json
"parent" : {
- "rule" : "f2b70c4a-4d8f-4db5-9ed7-d3ab0630e406",
+ "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41",
"id" : "9d8710925adbf1a9c469621805407e74334dd08ca2c2ea414840fe971a571938",
"type" : "signal",
"index" : ".siem-signals-default-000001",
- "depth" : 2
+ "depth" : 1
},
"ancestors" : [
{
- "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41",
"id" : "o8G7vm8BvLT8jmu5B1-M",
"type" : "event",
"index" : "filebeat-8.0.0-2019.12.18-000001",
- "depth" : 1
+ "depth" : 0
},
{
- "rule" : "f2b70c4a-4d8f-4db5-9ed7-d3ab0630e406",
+ "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41",
"id" : "9d8710925adbf1a9c469621805407e74334dd08ca2c2ea414840fe971a571938",
"type" : "signal",
"index" : ".siem-signals-default-000001",
- "depth" : 2
+ "depth" : 1
}
-]
+],
+"depth": 2,
+"rule": {
+ "id": "f2b70c4a-4d8f-4db5-9ed7-d3ab0630e406"
+}
```
But then f2b70c4a-4d8f-4db5-9ed7-d3ab0630e406 also finds the first signal to signal rule from 161fa5b8-0b96-4985-b066-0d99b2bcb904
@@ -275,35 +297,38 @@ and writes that document out with a depth of 3. (signal -> signal -> event)
```json
"parent" : {
- "rule" : "f2b70c4a-4d8f-4db5-9ed7-d3ab0630e406",
+ "rule" : "161fa5b8-0b96-4985-b066-0d99b2bcb904",
"id" : "c627e5e2576f1b10952c6c57249947e89b6153b763a59fb9e391d0b56be8e7fe",
"type" : "signal",
"index" : ".siem-signals-default-000001",
- "depth" : 3
+ "depth" : 2
},
"ancestors" : [
{
- "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41",
"id" : "o8G7vm8BvLT8jmu5B1-M",
"type" : "event",
"index" : "filebeat-8.0.0-2019.12.18-000001",
- "depth" : 1
+ "depth" : 0
},
{
- "rule" : "161fa5b8-0b96-4985-b066-0d99b2bcb904",
+ "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41",
"id" : "9d8710925adbf1a9c469621805407e74334dd08ca2c2ea414840fe971a571938",
"type" : "signal",
"index" : ".siem-signals-default-000001",
- "depth" : 2
+ "depth" : 1
},
{
- "rule" : "f2b70c4a-4d8f-4db5-9ed7-d3ab0630e406",
+ "rule" : "161fa5b8-0b96-4985-b066-0d99b2bcb904",
"id" : "c627e5e2576f1b10952c6c57249947e89b6153b763a59fb9e391d0b56be8e7fe",
"type" : "signal",
"index" : ".siem-signals-default-000001",
- "depth" : 3
+ "depth" : 2
}
-]
+],
+"depth": 3,
+"rule": {
+ "id": "f2b70c4a-4d8f-4db5-9ed7-d3ab0630e406"
+}
```
Since it wrote that document, the first signal to signal 161fa5b8-0b96-4985-b066-0d99b2bcb904 writes out it found this newly created signal
@@ -311,35 +336,38 @@ Since it wrote that document, the first signal to signal 161fa5b8-0b96-4985-b066
```json
"parent" : {
- "rule" : "161fa5b8-0b96-4985-b066-0d99b2bcb904",
+ "rule" : "f2b70c4a-4d8f-4db5-9ed7-d3ab0630e406",
"id" : "efbe514e8d806a5ef3da7658cfa73961e25befefc84f622e963b45dcac798868",
"type" : "signal",
"index" : ".siem-signals-default-000001",
- "depth" : 3
+ "depth" : 2
},
"ancestors" : [
{
- "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41",
"id" : "o8G7vm8BvLT8jmu5B1-M",
"type" : "event",
"index" : "filebeat-8.0.0-2019.12.18-000001",
- "depth" : 1
+ "depth" : 0
},
{
- "rule" : "f2b70c4a-4d8f-4db5-9ed7-d3ab0630e406",
+ "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41",
"id" : "9d8710925adbf1a9c469621805407e74334dd08ca2c2ea414840fe971a571938",
"type" : "signal",
"index" : ".siem-signals-default-000001",
- "depth" : 2
+ "depth" : 1
},
{
- "rule" : "161fa5b8-0b96-4985-b066-0d99b2bcb904",
+ "rule" : "f2b70c4a-4d8f-4db5-9ed7-d3ab0630e406",
"id" : "efbe514e8d806a5ef3da7658cfa73961e25befefc84f622e963b45dcac798868",
"type" : "signal",
"index" : ".siem-signals-default-000001",
- "depth" : 3
+ "depth" : 2
}
-]
+],
+"depth": 3,
+"rule": {
+ "id": "161fa5b8-0b96-4985-b066-0d99b2bcb904"
+}
```
You will be "halted" at this point as the signal ancestry and de-duplication ensures that we do not report twice on signals and that we do not
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts
index 95ec753c21fd8..9d3eb29be08dd 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts
@@ -149,21 +149,23 @@ export const sampleDocWithAncestors = (): SignalSearchResponse => {
delete sampleDoc._source.source;
sampleDoc._source.signal = {
parent: {
- rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71',
type: 'event',
index: 'myFakeSignalIndex',
- depth: 1,
+ depth: 0,
},
ancestors: [
{
- rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71',
type: 'event',
index: 'myFakeSignalIndex',
- depth: 1,
+ depth: 0,
},
],
+ rule: {
+ id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
+ },
+ depth: 1,
};
return {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts
index ee83c826371bc..967dc5331e46b 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts
@@ -48,19 +48,25 @@ describe('buildBulkBody', () => {
},
signal: {
parent: {
- rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
id: sampleIdGuid,
type: 'event',
index: 'myFakeSignalIndex',
- depth: 1,
+ depth: 0,
},
+ parents: [
+ {
+ id: sampleIdGuid,
+ type: 'event',
+ index: 'myFakeSignalIndex',
+ depth: 0,
+ },
+ ],
ancestors: [
{
- rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
id: sampleIdGuid,
type: 'event',
index: 'myFakeSignalIndex',
- depth: 1,
+ depth: 0,
},
],
original_time: '2020-04-20T21:27:45+0000',
@@ -102,6 +108,7 @@ describe('buildBulkBody', () => {
updated_at: fakeSignalSourceHit.signal.rule?.updated_at,
exceptions_list: getListArrayMock(),
},
+ depth: 1,
},
};
expect(fakeSignalSourceHit).toEqual(expected);
@@ -151,19 +158,25 @@ describe('buildBulkBody', () => {
module: 'system',
},
parent: {
- rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
id: sampleIdGuid,
type: 'event',
index: 'myFakeSignalIndex',
- depth: 1,
+ depth: 0,
},
+ parents: [
+ {
+ id: sampleIdGuid,
+ type: 'event',
+ index: 'myFakeSignalIndex',
+ depth: 0,
+ },
+ ],
ancestors: [
{
- rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
id: sampleIdGuid,
type: 'event',
index: 'myFakeSignalIndex',
- depth: 1,
+ depth: 0,
},
],
original_time: '2020-04-20T21:27:45+0000',
@@ -205,6 +218,7 @@ describe('buildBulkBody', () => {
threat: [],
exceptions_list: getListArrayMock(),
},
+ depth: 1,
},
};
expect(fakeSignalSourceHit).toEqual(expected);
@@ -252,19 +266,25 @@ describe('buildBulkBody', () => {
module: 'system',
},
parent: {
- rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
id: sampleIdGuid,
type: 'event',
index: 'myFakeSignalIndex',
- depth: 1,
+ depth: 0,
},
+ parents: [
+ {
+ id: sampleIdGuid,
+ type: 'event',
+ index: 'myFakeSignalIndex',
+ depth: 0,
+ },
+ ],
ancestors: [
{
- rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
id: sampleIdGuid,
type: 'event',
index: 'myFakeSignalIndex',
- depth: 1,
+ depth: 0,
},
],
original_time: '2020-04-20T21:27:45+0000',
@@ -306,6 +326,7 @@ describe('buildBulkBody', () => {
throttle: 'no_actions',
exceptions_list: getListArrayMock(),
},
+ depth: 1,
},
};
expect(fakeSignalSourceHit).toEqual(expected);
@@ -346,19 +367,25 @@ describe('buildBulkBody', () => {
kind: 'event',
},
parent: {
- rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
id: sampleIdGuid,
type: 'event',
index: 'myFakeSignalIndex',
- depth: 1,
+ depth: 0,
},
+ parents: [
+ {
+ id: sampleIdGuid,
+ type: 'event',
+ index: 'myFakeSignalIndex',
+ depth: 0,
+ },
+ ],
ancestors: [
{
- rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
id: sampleIdGuid,
type: 'event',
index: 'myFakeSignalIndex',
- depth: 1,
+ depth: 0,
},
],
original_time: '2020-04-20T21:27:45+0000',
@@ -400,6 +427,7 @@ describe('buildBulkBody', () => {
throttle: 'no_actions',
exceptions_list: getListArrayMock(),
},
+ depth: 1,
},
};
expect(fakeSignalSourceHit).toEqual(expected);
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts
index 218750ac30a2a..7be97e46f91f2 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts
@@ -4,9 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { SignalSourceHit, SignalHit } from './types';
+import { SignalSourceHit, SignalHit, Signal } from './types';
import { buildRule } from './build_rule';
-import { buildSignal } from './build_signal';
+import { additionalSignalFields, buildSignal } from './build_signal';
import { buildEventTypeSignal } from './build_event_type_signal';
import { RuleAlertAction } from '../../../../common/detection_engine/types';
import { RuleTypeParams } from '../types';
@@ -58,7 +58,10 @@ export const buildBulkBody = ({
tags,
throttle,
});
- const signal = buildSignal(doc, rule);
+ const signal: Signal = {
+ ...buildSignal([doc], rule),
+ ...additionalSignalFields(doc),
+ };
const event = buildEventTypeSignal(doc);
const signalHit: SignalHit = {
...doc._source,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts
index 7257e5952ff05..ba815a0b62f0d 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts
@@ -4,10 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { buildRule } from './build_rule';
+import { buildRule, removeInternalTagsFromRule } from './build_rule';
import { sampleDocNoSortId, sampleRuleAlertParams, sampleRuleGuid } from './__mocks__/es_results';
import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema';
import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock';
+import { INTERNAL_RULE_ID_KEY, INTERNAL_IMMUTABLE_KEY } from '../../../../common/constants';
+import { getPartialRulesSchemaMock } from '../../../../common/detection_engine/schemas/response/rules_schema.mocks';
describe('buildRule', () => {
beforeEach(() => {
@@ -208,4 +210,102 @@ describe('buildRule', () => {
};
expect(rule).toEqual(expected);
});
+
+ test('it builds a rule and removes internal tags', () => {
+ const ruleParams = sampleRuleAlertParams();
+ const rule = buildRule({
+ actions: [],
+ doc: sampleDocNoSortId(),
+ ruleParams,
+ name: 'some-name',
+ id: sampleRuleGuid,
+ enabled: false,
+ createdAt: '2020-01-28T15:58:34.810Z',
+ updatedAt: '2020-01-28T15:59:14.004Z',
+ createdBy: 'elastic',
+ updatedBy: 'elastic',
+ interval: 'some interval',
+ tags: [
+ 'some fake tag 1',
+ 'some fake tag 2',
+ `${INTERNAL_RULE_ID_KEY}:rule-1`,
+ `${INTERNAL_IMMUTABLE_KEY}:true`,
+ ],
+ throttle: 'no_actions',
+ });
+ const expected: Partial = {
+ actions: [],
+ author: ['Elastic'],
+ building_block_type: 'default',
+ created_by: 'elastic',
+ description: 'Detecting root and admin users',
+ enabled: false,
+ false_positives: [],
+ from: 'now-6m',
+ id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
+ immutable: false,
+ index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
+ interval: 'some interval',
+ language: 'kuery',
+ license: 'Elastic License',
+ max_signals: 10000,
+ name: 'some-name',
+ output_index: '.siem-signals',
+ query: 'user.name: root or user.name: admin',
+ references: ['http://google.com'],
+ risk_score: 50,
+ risk_score_mapping: [],
+ rule_id: 'rule-1',
+ severity: 'high',
+ severity_mapping: [],
+ tags: ['some fake tag 1', 'some fake tag 2'],
+ threat: [],
+ to: 'now',
+ type: 'query',
+ note: '',
+ updated_by: 'elastic',
+ updated_at: rule.updated_at,
+ created_at: rule.created_at,
+ throttle: 'no_actions',
+ exceptions_list: getListArrayMock(),
+ version: 1,
+ };
+ expect(rule).toEqual(expected);
+ });
+
+ test('it removes internal tags from a typical rule', () => {
+ const rule = getPartialRulesSchemaMock();
+ rule.tags = [
+ 'some fake tag 1',
+ 'some fake tag 2',
+ `${INTERNAL_RULE_ID_KEY}:rule-1`,
+ `${INTERNAL_IMMUTABLE_KEY}:true`,
+ ];
+ const noInternals = removeInternalTagsFromRule(rule);
+ expect(noInternals).toEqual(getPartialRulesSchemaMock());
+ });
+
+ test('it works with an empty array', () => {
+ const rule = getPartialRulesSchemaMock();
+ rule.tags = [];
+ const noInternals = removeInternalTagsFromRule(rule);
+ const expected = getPartialRulesSchemaMock();
+ expected.tags = [];
+ expect(noInternals).toEqual(expected);
+ });
+
+ test('it works if tags does not exist', () => {
+ const rule = getPartialRulesSchemaMock();
+ delete rule.tags;
+ const noInternals = removeInternalTagsFromRule(rule);
+ const expected = getPartialRulesSchemaMock();
+ delete expected.tags;
+ expect(noInternals).toEqual(expected);
+ });
+
+ test('it works if tags contains normal values and no internal values', () => {
+ const rule = getPartialRulesSchemaMock();
+ const noInternals = removeInternalTagsFromRule(rule);
+ expect(noInternals).toEqual(rule);
+ });
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts
index e02a0154d63c9..aacf9b8be31b4 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts
@@ -12,6 +12,7 @@ import { buildRiskScoreFromMapping } from './mappings/build_risk_score_from_mapp
import { SignalSourceHit } from './types';
import { buildSeverityFromMapping } from './mappings/build_severity_from_mapping';
import { buildRuleNameFromMapping } from './mappings/build_rule_name_from_mapping';
+import { INTERNAL_IDENTIFIER } from '../../../../common/constants';
interface BuildRuleParams {
ruleParams: RuleTypeParams;
@@ -64,7 +65,7 @@ export const buildRule = ({
const meta = { ...ruleParams.meta, ...riskScoreMeta, ...severityMeta, ...ruleNameMeta };
- return pickBy((value: unknown) => value != null, {
+ const rule = pickBy((value: unknown) => value != null, {
id,
rule_id: ruleParams.ruleId ?? '(unknown rule_id)',
actions,
@@ -111,4 +112,17 @@ export const buildRule = ({
anomaly_threshold: ruleParams.anomalyThreshold,
threshold: ruleParams.threshold,
});
+ return removeInternalTagsFromRule(rule);
+};
+
+export const removeInternalTagsFromRule = (rule: Partial): Partial => {
+ if (rule.tags == null) {
+ return rule;
+ } else {
+ const ruleWithoutInternalTags: Partial = {
+ ...rule,
+ tags: rule.tags.filter((tag) => !tag.startsWith(INTERNAL_IDENTIFIER)),
+ };
+ return ruleWithoutInternalTags;
+ }
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts
index 6aebf8815659a..d684807a09126 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts
@@ -5,14 +5,8 @@
*/
import { sampleDocNoSortId } from './__mocks__/es_results';
-import {
- buildSignal,
- buildAncestor,
- buildAncestorsSignal,
- removeInternalTagsFromRule,
-} from './build_signal';
+import { buildSignal, buildParent, buildAncestors, additionalSignalFields } from './build_signal';
import { Signal, Ancestor } from './types';
-import { INTERNAL_RULE_ID_KEY, INTERNAL_IMMUTABLE_KEY } from '../../../../common/constants';
import { getPartialRulesSchemaMock } from '../../../../common/detection_engine/schemas/response/rules_schema.mocks';
describe('buildSignal', () => {
@@ -24,22 +18,31 @@ describe('buildSignal', () => {
const doc = sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71');
delete doc._source.event;
const rule = getPartialRulesSchemaMock();
- const signal = buildSignal(doc, rule);
+ const signal = {
+ ...buildSignal([doc], rule),
+ ...additionalSignalFields(doc),
+ };
const expected: Signal = {
parent: {
- rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71',
type: 'event',
index: 'myFakeSignalIndex',
- depth: 1,
+ depth: 0,
},
+ parents: [
+ {
+ id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71',
+ type: 'event',
+ index: 'myFakeSignalIndex',
+ depth: 0,
+ },
+ ],
ancestors: [
{
- rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71',
type: 'event',
index: 'myFakeSignalIndex',
- depth: 1,
+ depth: 0,
},
],
original_time: '2020-04-20T21:27:45+0000',
@@ -71,6 +74,7 @@ describe('buildSignal', () => {
updated_at: signal.rule.updated_at,
created_at: signal.rule.created_at,
},
+ depth: 1,
};
expect(signal).toEqual(expected);
});
@@ -84,94 +88,31 @@ describe('buildSignal', () => {
module: 'system',
};
const rule = getPartialRulesSchemaMock();
- const signal = buildSignal(doc, rule);
+ const signal = {
+ ...buildSignal([doc], rule),
+ ...additionalSignalFields(doc),
+ };
const expected: Signal = {
parent: {
- rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71',
type: 'event',
index: 'myFakeSignalIndex',
- depth: 1,
+ depth: 0,
},
- ancestors: [
+ parents: [
{
- rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71',
type: 'event',
index: 'myFakeSignalIndex',
- depth: 1,
+ depth: 0,
},
],
- original_time: '2020-04-20T21:27:45+0000',
- original_event: {
- action: 'socket_opened',
- dataset: 'socket',
- kind: 'event',
- module: 'system',
- },
- status: 'open',
- rule: {
- created_by: 'elastic',
- description: 'Detecting root and admin users',
- enabled: true,
- false_positives: [],
- from: 'now-6m',
- id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
- immutable: false,
- index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
- interval: '5m',
- risk_score: 50,
- rule_id: 'rule-1',
- language: 'kuery',
- max_signals: 100,
- name: 'Detect Root/Admin Users',
- output_index: '.siem-signals',
- query: 'user.name: root or user.name: admin',
- references: ['http://www.example.com', 'https://ww.example.com'],
- severity: 'high',
- updated_by: 'elastic',
- tags: ['some fake tag 1', 'some fake tag 2'],
- to: 'now',
- type: 'query',
- note: '',
- updated_at: signal.rule.updated_at,
- created_at: signal.rule.created_at,
- },
- };
- expect(signal).toEqual(expected);
- });
-
- test('it builds a signal as expected with original_event if is present and without internal tags in them', () => {
- const doc = sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71');
- doc._source.event = {
- action: 'socket_opened',
- dataset: 'socket',
- kind: 'event',
- module: 'system',
- };
- const rule = getPartialRulesSchemaMock();
- rule.tags = [
- 'some fake tag 1',
- 'some fake tag 2',
- `${INTERNAL_RULE_ID_KEY}:rule-1`,
- `${INTERNAL_IMMUTABLE_KEY}:true`,
- ];
- const signal = buildSignal(doc, rule);
- const expected: Signal = {
- parent: {
- rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
- id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71',
- type: 'event',
- index: 'myFakeSignalIndex',
- depth: 1,
- },
ancestors: [
{
- rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71',
type: 'event',
index: 'myFakeSignalIndex',
- depth: 1,
+ depth: 0,
},
],
original_time: '2020-04-20T21:27:45+0000',
@@ -209,6 +150,7 @@ describe('buildSignal', () => {
updated_at: signal.rule.updated_at,
created_at: signal.rule.created_at,
},
+ depth: 1,
};
expect(signal).toEqual(expected);
});
@@ -221,14 +163,12 @@ describe('buildSignal', () => {
kind: 'event',
module: 'system',
};
- const rule = getPartialRulesSchemaMock();
- const signal = buildAncestor(doc, rule);
+ const signal = buildParent(doc);
const expected: Ancestor = {
- rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71',
type: 'event',
index: 'myFakeSignalIndex',
- depth: 1,
+ depth: 0,
};
expect(signal).toEqual(expected);
});
@@ -242,76 +182,34 @@ describe('buildSignal', () => {
module: 'system',
};
doc._source.signal = {
- parent: {
- rule: '98c0bf9e-4d38-46f4-9a6a-8a820426256b',
- id: '730ddf9e-5a00-4f85-9ddf-5878ca511a87',
- type: 'event',
- index: 'myFakeSignalIndex',
- depth: 1,
- },
- ancestors: [
+ parents: [
{
- rule: '98c0bf9e-4d38-46f4-9a6a-8a820426256b',
id: '730ddf9e-5a00-4f85-9ddf-5878ca511a87',
type: 'event',
index: 'myFakeSignalIndex',
- depth: 1,
+ depth: 0,
},
],
- };
- const rule = getPartialRulesSchemaMock();
- const signal = buildAncestor(doc, rule);
- const expected: Ancestor = {
- rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
- id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71',
- type: 'signal',
- index: 'myFakeSignalIndex',
- depth: 2,
- };
- expect(signal).toEqual(expected);
- });
-
- test('it builds a ancestor correctly if the parent does exist without internal tags in them', () => {
- const doc = sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71');
- doc._source.event = {
- action: 'socket_opened',
- dataset: 'socket',
- kind: 'event',
- module: 'system',
- };
- doc._source.signal = {
- parent: {
- rule: '98c0bf9e-4d38-46f4-9a6a-8a820426256b',
- id: '730ddf9e-5a00-4f85-9ddf-5878ca511a87',
- type: 'event',
- index: 'myFakeSignalIndex',
- depth: 1,
- },
ancestors: [
{
- rule: '98c0bf9e-4d38-46f4-9a6a-8a820426256b',
id: '730ddf9e-5a00-4f85-9ddf-5878ca511a87',
type: 'event',
index: 'myFakeSignalIndex',
- depth: 1,
+ depth: 0,
},
],
+ depth: 1,
+ rule: {
+ id: '98c0bf9e-4d38-46f4-9a6a-8a820426256b',
+ },
};
- const rule = getPartialRulesSchemaMock();
- rule.tags = [
- 'some fake tag 1',
- 'some fake tag 2',
- `${INTERNAL_RULE_ID_KEY}:rule-1`,
- `${INTERNAL_IMMUTABLE_KEY}:true`,
- ];
-
- const signal = buildAncestor(doc, rule);
+ const signal = buildParent(doc);
const expected: Ancestor = {
- rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
+ rule: '98c0bf9e-4d38-46f4-9a6a-8a820426256b',
id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71',
type: 'signal',
index: 'myFakeSignalIndex',
- depth: 2,
+ depth: 1,
};
expect(signal).toEqual(expected);
});
@@ -324,15 +222,13 @@ describe('buildSignal', () => {
kind: 'event',
module: 'system',
};
- const rule = getPartialRulesSchemaMock();
- const signal = buildAncestorsSignal(doc, rule);
+ const signal = buildAncestors(doc);
const expected: Ancestor[] = [
{
- rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71',
type: 'event',
index: 'myFakeSignalIndex',
- depth: 1,
+ depth: 0,
},
];
expect(signal).toEqual(expected);
@@ -347,77 +243,43 @@ describe('buildSignal', () => {
module: 'system',
};
doc._source.signal = {
- parent: {
- rule: '98c0bf9e-4d38-46f4-9a6a-8a820426256b',
- id: '730ddf9e-5a00-4f85-9ddf-5878ca511a87',
- type: 'event',
- index: 'myFakeSignalIndex',
- depth: 1,
- },
+ parents: [
+ {
+ id: '730ddf9e-5a00-4f85-9ddf-5878ca511a87',
+ type: 'event',
+ index: 'myFakeSignalIndex',
+ depth: 0,
+ },
+ ],
ancestors: [
{
- rule: '98c0bf9e-4d38-46f4-9a6a-8a820426256b',
id: '730ddf9e-5a00-4f85-9ddf-5878ca511a87',
type: 'event',
index: 'myFakeSignalIndex',
- depth: 1,
+ depth: 0,
},
],
+ rule: {
+ id: '98c0bf9e-4d38-46f4-9a6a-8a820426256b',
+ },
+ depth: 1,
};
- const rule = getPartialRulesSchemaMock();
- const signal = buildAncestorsSignal(doc, rule);
+ const signal = buildAncestors(doc);
const expected: Ancestor[] = [
{
- rule: '98c0bf9e-4d38-46f4-9a6a-8a820426256b',
id: '730ddf9e-5a00-4f85-9ddf-5878ca511a87',
type: 'event',
index: 'myFakeSignalIndex',
- depth: 1,
+ depth: 0,
},
{
- rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
+ rule: '98c0bf9e-4d38-46f4-9a6a-8a820426256b',
id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71',
type: 'signal',
index: 'myFakeSignalIndex',
- depth: 2,
+ depth: 1,
},
];
expect(signal).toEqual(expected);
});
-
- test('it removes internal tags from a typical rule', () => {
- const rule = getPartialRulesSchemaMock();
- rule.tags = [
- 'some fake tag 1',
- 'some fake tag 2',
- `${INTERNAL_RULE_ID_KEY}:rule-1`,
- `${INTERNAL_IMMUTABLE_KEY}:true`,
- ];
- const noInternals = removeInternalTagsFromRule(rule);
- expect(noInternals).toEqual(getPartialRulesSchemaMock());
- });
-
- test('it works with an empty array', () => {
- const rule = getPartialRulesSchemaMock();
- rule.tags = [];
- const noInternals = removeInternalTagsFromRule(rule);
- const expected = getPartialRulesSchemaMock();
- expected.tags = [];
- expect(noInternals).toEqual(expected);
- });
-
- test('it works if tags does not exist', () => {
- const rule = getPartialRulesSchemaMock();
- delete rule.tags;
- const noInternals = removeInternalTagsFromRule(rule);
- const expected = getPartialRulesSchemaMock();
- delete expected.tags;
- expect(noInternals).toEqual(expected);
- });
-
- test('it works if tags contains normal values and no internal values', () => {
- const rule = getPartialRulesSchemaMock();
- const noInternals = removeInternalTagsFromRule(rule);
- expect(noInternals).toEqual(rule);
- });
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts
index e7098c015c165..78818779dd661 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts
@@ -5,35 +5,41 @@
*/
import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema';
-import { INTERNAL_IDENTIFIER } from '../../../../common/constants';
import { SignalSourceHit, Signal, Ancestor } from './types';
-export const buildAncestor = (doc: SignalSourceHit, rule: Partial): Ancestor => {
- const existingSignal = doc._source.signal?.parent;
- if (existingSignal != null) {
+/**
+ * Takes a parent signal or event document and extracts the information needed for the corresponding entry in the child
+ * signal's `signal.parents` array.
+ * @param doc The parent signal or event
+ */
+export const buildParent = (doc: SignalSourceHit): Ancestor => {
+ if (doc._source.signal != null) {
return {
- rule: rule.id != null ? rule.id : '',
+ rule: doc._source.signal.rule.id,
id: doc._id,
type: 'signal',
index: doc._index,
- depth: existingSignal.depth + 1,
+ // We first look for signal.depth and use that if it exists. If it doesn't exist, this should be a pre-7.10 signal
+ // and should have signal.parent.depth instead. signal.parent.depth in this case is treated as equivalent to signal.depth.
+ depth: doc._source.signal.depth ?? doc._source.signal.parent?.depth ?? 1,
};
} else {
return {
- rule: rule.id != null ? rule.id : '',
id: doc._id,
type: 'event',
index: doc._index,
- depth: 1,
+ depth: 0,
};
}
};
-export const buildAncestorsSignal = (
- doc: SignalSourceHit,
- rule: Partial
-): Signal['ancestors'] => {
- const newAncestor = buildAncestor(doc, rule);
+/**
+ * Takes a parent signal or event document with N ancestors and adds the parent document to the ancestry array,
+ * creating an array of N+1 ancestors.
+ * @param doc The parent signal/event for which to extend the ancestry.
+ */
+export const buildAncestors = (doc: SignalSourceHit): Ancestor[] => {
+ const newAncestor = buildParent(doc);
const existingAncestors = doc._source.signal?.ancestors;
if (existingAncestors != null) {
return [...existingAncestors, newAncestor];
@@ -42,35 +48,33 @@ export const buildAncestorsSignal = (
}
};
-export const buildSignal = (doc: SignalSourceHit, rule: Partial): Signal => {
- const ruleWithoutInternalTags = removeInternalTagsFromRule(rule);
- const parent = buildAncestor(doc, rule);
- const ancestors = buildAncestorsSignal(doc, rule);
- let signal: Signal = {
- parent,
+/**
+ * Builds the `signal.*` fields that are common across all signals.
+ * @param docs The parent signals/events of the new signal to be built.
+ * @param rule The rule that is generating the new signal.
+ */
+export const buildSignal = (docs: SignalSourceHit[], rule: Partial): Signal => {
+ const parents = docs.map(buildParent);
+ const depth = parents.reduce((acc, parent) => Math.max(parent.depth, acc), 0) + 1;
+ const ancestors = docs.reduce((acc: Ancestor[], doc) => acc.concat(buildAncestors(doc)), []);
+ return {
+ parents,
ancestors,
- original_time: doc._source['@timestamp'],
status: 'open',
- rule: ruleWithoutInternalTags,
+ rule,
+ depth,
};
- if (doc._source.event != null) {
- signal = { ...signal, original_event: doc._source.event };
- }
- if (doc._source.threshold_count != null) {
- signal = { ...signal, threshold_count: doc._source.threshold_count };
- delete doc._source.threshold_count;
- }
- return signal;
};
-export const removeInternalTagsFromRule = (rule: Partial): Partial => {
- if (rule.tags == null) {
- return rule;
- } else {
- const ruleWithoutInternalTags: Partial = {
- ...rule,
- tags: rule.tags.filter((tag) => !tag.startsWith(INTERNAL_IDENTIFIER)),
- };
- return ruleWithoutInternalTags;
- }
+/**
+ * Creates signal fields that are only available in the special case where a signal has only 1 parent signal/event.
+ * @param doc The parent signal/event of the new signal to be built.
+ */
+export const additionalSignalFields = (doc: SignalSourceHit) => {
+ return {
+ parent: buildParent(doc),
+ original_time: doc._source['@timestamp'],
+ original_event: doc._source.event ?? undefined,
+ threshold_count: doc._source.threshold_count ?? undefined,
+ };
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts
index da17d4a1f123a..7ee157beec789 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts
@@ -120,7 +120,6 @@ export const signalRulesAlertType = ({
enabled,
schedule: { interval },
throttle,
- params: ruleParams,
} = savedObject.attributes;
const updatedAt = savedObject.updated_at ?? '';
const refresh = actions.length ? 'wait_for' : false;
@@ -343,7 +342,7 @@ export const signalRulesAlertType = ({
if (result.success) {
if (actions.length) {
const notificationRuleParams: NotificationRuleTypeParams = {
- ...ruleParams,
+ ...params,
name,
id: savedObject.id,
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts
index 8b9fb0574efe9..41c825ea4d978 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts
@@ -291,37 +291,7 @@ describe('singleBulkCreate', () => {
test('filter duplicate rules will return nothing filtered when the two rule ids do not match with each other', () => {
const filtered = filterDuplicateRules('some id', sampleDocWithAncestors());
- expect(filtered).toEqual([
- {
- _index: 'myFakeSignalIndex',
- _type: 'doc',
- _score: 100,
- _version: 1,
- _id: 'e1e08ddc-5e37-49ff-a258-5393aa44435a',
- _source: {
- someKey: 'someValue',
- '@timestamp': '2020-04-20T21:27:45+0000',
- signal: {
- parent: {
- rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
- id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71',
- type: 'event',
- index: 'myFakeSignalIndex',
- depth: 1,
- },
- ancestors: [
- {
- rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
- id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71',
- type: 'event',
- index: 'myFakeSignalIndex',
- depth: 1,
- },
- ],
- },
- },
- },
- ]);
+ expect(filtered).toEqual(sampleDocWithAncestors().hits.hits);
});
test('filters duplicate rules will return empty array when the two rule ids match each other', () => {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts
index 74709f31563ee..be71c67615a4c 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts
@@ -51,7 +51,10 @@ export const filterDuplicateRules = (
if (doc._source.signal == null) {
return true;
} else {
- return !doc._source.signal.ancestors.some((ancestor) => ancestor.rule === ruleId);
+ return !(
+ doc._source.signal.ancestors.some((ancestor) => ancestor.rule === ruleId) ||
+ doc._source.signal.rule.id === ruleId
+ );
}
});
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts
index aecdbe10695d2..700a8fb5022d7 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts
@@ -44,8 +44,16 @@ export interface SignalSource {
[key: string]: SearchTypes;
'@timestamp': string;
signal?: {
- parent: Ancestor;
+ // parent is deprecated: new signals should populate parents instead
+ // both are optional until all signals with parent are gone and we can safely remove it
+ parent?: Ancestor;
+ parents?: Ancestor[];
ancestors: Ancestor[];
+ rule: {
+ id: string;
+ };
+ // signal.depth doesn't exist on pre-7.10 signals
+ depth?: number;
};
}
@@ -113,7 +121,7 @@ export type SignalRuleAlertTypeDefinition = Omit & {
};
export interface Ancestor {
- rule: string;
+ rule?: string;
id: string;
type: string;
index: string;
@@ -122,12 +130,15 @@ export interface Ancestor {
export interface Signal {
rule: Partial;
- parent: Ancestor;
+ // DEPRECATED: use parents instead of parent
+ parent?: Ancestor;
+ parents: Ancestor[];
ancestors: Ancestor[];
- original_time: string;
+ original_time?: string;
original_event?: SearchTypes;
status: Status;
threshold_count?: SearchTypes;
+ depth: number;
}
export interface SignalHit {
From 92ab49c1003a2b83f98f6be731f0386c1266ffdb Mon Sep 17 00:00:00 2001
From: Brian Seeders
Date: Wed, 9 Sep 2020 14:42:47 -0400
Subject: [PATCH 13/25] [CI] Balance xpack ci groups a bit (#77068)
---
x-pack/test/functional/apps/maps/index.js | 2 +-
.../security_and_spaces/apis/index.ts | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/x-pack/test/functional/apps/maps/index.js b/x-pack/test/functional/apps/maps/index.js
index ef8b4ad4c0f19..03b75601ec2a8 100644
--- a/x-pack/test/functional/apps/maps/index.js
+++ b/x-pack/test/functional/apps/maps/index.js
@@ -28,7 +28,7 @@ export default function ({ loadTestFile, getService }) {
});
describe('', function () {
- this.tags('ciGroup7');
+ this.tags('ciGroup9');
loadTestFile(require.resolve('./documents_source'));
loadTestFile(require.resolve('./blended_vector_layer'));
loadTestFile(require.resolve('./vector_styling'));
diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts
index 81ffc5eea9220..ed501b235a457 100644
--- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts
+++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts
@@ -12,7 +12,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
const supertest = getService('supertest');
describe('saved objects security and spaces enabled', function () {
- this.tags('ciGroup5');
+ this.tags('ciGroup8');
before(async () => {
await createUsersAndRoles(es, supertest);
From 87ca6ff70c90144f010c1757f309aac8f53608ce Mon Sep 17 00:00:00 2001
From: John Schulz
Date: Wed, 9 Sep 2020 14:55:21 -0400
Subject: [PATCH 14/25] First pass. Change TS type. Update OpenAPI (#76434)
Co-authored-by: Elastic Machine
---
.../common/openapi/spec_oas3.json | 53 +++++++++++++------
.../ingest_manager/common/types/models/epm.ts | 4 +-
.../hooks/use_package_icon_type.ts | 2 +-
.../epm/screens/detail/screenshots.tsx | 2 +-
.../common/endpoint/generate_data.ts | 3 +-
5 files changed, 43 insertions(+), 21 deletions(-)
diff --git a/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json b/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json
index d75a914e080d7..b7856e6d57402 100644
--- a/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json
+++ b/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json
@@ -1425,11 +1425,13 @@
},
"icons": [
{
- "src": "/package/coredns-1.0.1/img/icon.png",
+ "path": "/package/coredns-1.0.1/img/icon.png",
+ "src": "/img/icon.png",
"size": "1800x1800"
},
{
- "src": "/package/coredns-1.0.1/img/icon.svg",
+ "path": "/package/coredns-1.0.1/img/icon.svg",
+ "src": "/img/icon.svg",
"size": "255x144",
"type": "image/svg+xml"
}
@@ -1704,7 +1706,8 @@
},
"icons": [
{
- "src": "/package/endpoint/0.3.0/img/logo-endpoint-64-color.svg",
+ "path": "/package/endpoint/0.3.0/img/logo-endpoint-64-color.svg",
+ "src": "/img/logo-endpoint-64-color.svg",
"size": "16x16",
"type": "image/svg+xml"
}
@@ -2001,7 +2004,8 @@
"download": "/epr/aws/aws-0.0.3.tar.gz",
"icons": [
{
- "src": "/package/aws/0.0.3/img/logo_aws.svg",
+ "path": "/package/aws/0.0.3/img/logo_aws.svg",
+ "src": "/img/logo_aws.svg",
"title": "logo aws",
"size": "32x32",
"type": "image/svg+xml"
@@ -2019,7 +2023,8 @@
"download": "/epr/endpoint/endpoint-0.1.0.tar.gz",
"icons": [
{
- "src": "/package/endpoint/0.1.0/img/logo-endpoint-64-color.svg",
+ "path": "/package/endpoint/0.1.0/img/logo-endpoint-64-color.svg",
+ "src": "/img/logo-endpoint-64-color.svg",
"size": "16x16",
"type": "image/svg+xml"
}
@@ -2087,7 +2092,8 @@
"download": "/epr/log/log-0.9.0.tar.gz",
"icons": [
{
- "src": "/package/log/0.9.0/img/icon.svg",
+ "path": "/package/log/0.9.0/img/icon.svg",
+ "src": "/img/icon.svg",
"type": "image/svg+xml"
}
],
@@ -2103,7 +2109,8 @@
"download": "/epr/longdocs/longdocs-1.0.4.tar.gz",
"icons": [
{
- "src": "/package/longdocs/1.0.4/img/icon.svg",
+ "path": "/package/longdocs/1.0.4/img/icon.svg",
+ "src": "/img/icon.svg",
"type": "image/svg+xml"
}
],
@@ -2119,7 +2126,8 @@
"download": "/epr/metricsonly/metricsonly-2.0.1.tar.gz",
"icons": [
{
- "src": "/package/metricsonly/2.0.1/img/icon.svg",
+ "path": "/package/metricsonly/2.0.1/img/icon.svg",
+ "src": "/img/icon.svg",
"type": "image/svg+xml"
}
],
@@ -2135,7 +2143,8 @@
"download": "/epr/multiversion/multiversion-1.1.0.tar.gz",
"icons": [
{
- "src": "/package/multiversion/1.1.0/img/icon.svg",
+ "path": "/package/multiversion/1.1.0/img/icon.svg",
+ "src": "/img/icon.svg",
"type": "image/svg+xml"
}
],
@@ -2151,7 +2160,8 @@
"download": "/epr/mysql/mysql-0.1.0.tar.gz",
"icons": [
{
- "src": "/package/mysql/0.1.0/img/logo_mysql.svg",
+ "path": "/package/mysql/0.1.0/img/logo_mysql.svg",
+ "src": "/img/logo_mysql.svg",
"title": "logo mysql",
"size": "32x32",
"type": "image/svg+xml"
@@ -2169,7 +2179,8 @@
"download": "/epr/nginx/nginx-0.1.0.tar.gz",
"icons": [
{
- "src": "/package/nginx/0.1.0/img/logo_nginx.svg",
+ "path": "/package/nginx/0.1.0/img/logo_nginx.svg",
+ "src": "/img/logo_nginx.svg",
"title": "logo nginx",
"size": "32x32",
"type": "image/svg+xml"
@@ -2187,7 +2198,8 @@
"download": "/epr/redis/redis-0.1.0.tar.gz",
"icons": [
{
- "src": "/package/redis/0.1.0/img/logo_redis.svg",
+ "path": "/package/redis/0.1.0/img/logo_redis.svg",
+ "src": "/img/logo_redis.svg",
"title": "logo redis",
"size": "32x32",
"type": "image/svg+xml"
@@ -2205,7 +2217,8 @@
"download": "/epr/reference/reference-1.0.0.tar.gz",
"icons": [
{
- "src": "/package/reference/1.0.0/img/icon.svg",
+ "path": "/package/reference/1.0.0/img/icon.svg",
+ "src": "/img/icon.svg",
"size": "32x32",
"type": "image/svg+xml"
}
@@ -2222,7 +2235,8 @@
"download": "/epr/system/system-0.1.0.tar.gz",
"icons": [
{
- "src": "/package/system/0.1.0/img/system.svg",
+ "path": "/package/system/0.1.0/img/system.svg",
+ "src": "/img/system.svg",
"title": "system",
"size": "1000x1000",
"type": "image/svg+xml"
@@ -3913,11 +3927,20 @@
"src": {
"type": "string"
},
+ "path": {
+ "type": "string"
+ },
"title": {
"type": "string"
+ },
+ "size": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
}
},
- "required": ["src"]
+ "required": ["src", "path"]
}
},
"icons": {
diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts
index f083400997870..8bc5d9f7210b2 100644
--- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts
+++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts
@@ -74,10 +74,8 @@ export interface RegistryPackage {
}
interface RegistryImage {
- // https://github.com/elastic/package-registry/blob/master/util/package.go#L74
- // says src is potentially missing but I couldn't find any examples
- // it seems like src should be required. How can you have an image with no reference to the content?
src: string;
+ path: string;
title?: string;
size?: string;
type?: string;
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_package_icon_type.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_package_icon_type.ts
index e5a7191372e9c..690ffdf46f704 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_package_icon_type.ts
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_package_icon_type.ts
@@ -42,7 +42,7 @@ export const usePackageIconType = ({
const svgIcons = (paramIcons || iconList)?.filter(
(iconDef) => iconDef.type === 'image/svg+xml'
);
- const localIconSrc = Array.isArray(svgIcons) && svgIcons[0]?.src;
+ const localIconSrc = Array.isArray(svgIcons) && (svgIcons[0].path || svgIcons[0].src);
if (localIconSrc) {
CACHED_ICONS.set(pkgKey, toImage(localIconSrc));
setIconType(CACHED_ICONS.get(pkgKey) || '');
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/screenshots.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/screenshots.tsx
index d8388a71556d6..6326e9072be8e 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/screenshots.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/screenshots.tsx
@@ -75,7 +75,7 @@ export function Screenshots(props: ScreenshotProps) {
set image to same width. Will need to update if size changes.
*/}
Date: Wed, 9 Sep 2020 12:53:51 -0700
Subject: [PATCH 15/25] [Enterprise Search] Add Overview landing page/plugin
(#76734)
* [public] Register Enterprise search plugin
+ move new Home strings to constants
* [server] Register plugin access/visibility
* Set up Enterprise Search Kibana Chrome
- Add SetEnterpriseSearchChrome
- Update Enterprise Search breadcrumbs to link to new overview plugin (+ update overview plugin URL per team discussion)
- Add ability to break out of React Router basename by not using history.createhref
- Update createHref mock to more closely match Kibana urls (adding /app prefix)
- Minor documentation fix
* Set up Enterprise Search plugin telemetry
- client-side: SendEnterpriseSearchTelemetry
- server-side: register saved objects, usage collector, etc.
* Enterprise search overview views (#23)
* Add formatTestSubj util
This allows us to correctly format strings into our casing for data-test-subj attrs
* Add images and stylesheet
* Add product card component
* Add index component
* Remove unused styles
* Fix inter-plugin links
- by add shouldNotCreateHref prop to RR helpers
- similiar to breadcrumb change
* Fix/clean up CSS
- Prefer EUI components over bespoke CSS (e.g. EuiCard)
- Remove unused or unspecific CSS
- Pull out product card CSS to its component
- Fix kebab-cased CSS classes to camelCased
* Clean up ProductCard props
- Prefer passing in our plugin consts instead of separate props
- Move productCardDescription to constants
- Update tests
* Add telemetry clicked actions to product buttons
+ revert data-test-subj strings to previous implementation
+ prune format_test_subj helper by using lodash util directly
* [PR feedback] Add new plugin to applicationUsageSchema per telemetry team request
* Fix failing functional navLinks test
* Fix telemetry schema test
* [Perf] Optimize assets size by switching from 300kb SVG to 25kb PNG
* Only show product cards if the user has access to that product
- adds access checks
- fixes flex/CSS to show one card at a time
Co-authored-by: Scotty Bollinger
---
.../collectors/application_usage/schema.ts | 1 +
src/plugins/telemetry/schema/oss_plugins.json | 28 ++++++
.../enterprise_search/common/constants.ts | 30 +++++-
.../enterprise_search/common/types/index.ts | 4 +
.../__mocks__/react_router_history.mock.ts | 2 +-
.../enterprise_search/assets/app_search.png | Bin 0 -> 25568 bytes
.../assets/bg_enterprise_search.png | Bin 0 -> 24757 bytes
.../assets/workplace_search.png | Bin 0 -> 30483 bytes
.../components/product_card/index.ts | 7 ++
.../components/product_card/product_card.scss | 58 ++++++++++++
.../product_card/product_card.test.tsx | 57 ++++++++++++
.../components/product_card/product_card.tsx | 71 ++++++++++++++
.../applications/enterprise_search/index.scss | 54 +++++++++++
.../enterprise_search/index.test.tsx | 50 ++++++++++
.../applications/enterprise_search/index.tsx | 78 ++++++++++++++++
.../generate_breadcrumbs.test.ts | 54 +++++++----
.../kibana_chrome/generate_breadcrumbs.ts | 16 +++-
.../shared/kibana_chrome/generate_title.ts | 2 +-
.../shared/kibana_chrome/index.ts | 6 +-
.../shared/kibana_chrome/set_chrome.test.tsx | 33 ++++++-
.../shared/kibana_chrome/set_chrome.tsx | 26 +++++-
.../react_router_helpers/eui_link.test.tsx | 10 +-
.../shared/react_router_helpers/eui_link.tsx | 18 +++-
.../applications/shared/telemetry/index.ts | 7 +-
.../shared/telemetry/send_telemetry.test.tsx | 22 ++++-
.../shared/telemetry/send_telemetry.tsx | 14 ++-
.../enterprise_search/public/plugin.ts | 38 ++++----
.../enterprise_search/telemetry.test.ts | 85 +++++++++++++++++
.../collectors/enterprise_search/telemetry.ts | 87 ++++++++++++++++++
.../server/collectors/lib/telemetry.test.ts | 2 +-
.../enterprise_search/server/plugin.ts | 18 +++-
.../routes/enterprise_search/telemetry.ts | 3 +-
.../enterprise_search/telemetry.ts | 19 ++++
.../schema/xpack_plugins.json | 21 +++++
.../security_only/tests/nav_links.ts | 8 +-
35 files changed, 868 insertions(+), 61 deletions(-)
create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search/assets/app_search.png
create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search/assets/bg_enterprise_search.png
create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search/assets/workplace_search.png
create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/index.ts
create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.scss
create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.scss
create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx
create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx
create mode 100644 x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.test.ts
create mode 100644 x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.ts
create mode 100644 x-pack/plugins/enterprise_search/server/saved_objects/enterprise_search/telemetry.ts
diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts
index 6efe872553583..2e79cdaa7fc6b 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts
@@ -66,6 +66,7 @@ export const applicationUsageSchema = {
csm: commonSchema,
canvas: commonSchema,
dashboard_mode: commonSchema, // It's a forward app so we'll likely never report it
+ enterpriseSearch: commonSchema,
appSearch: commonSchema,
workplaceSearch: commonSchema,
graph: commonSchema,
diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json
index acd575badbe5b..5bce03a292760 100644
--- a/src/plugins/telemetry/schema/oss_plugins.json
+++ b/src/plugins/telemetry/schema/oss_plugins.json
@@ -414,6 +414,34 @@
}
}
},
+ "enterpriseSearch": {
+ "properties": {
+ "clicks_total": {
+ "type": "long"
+ },
+ "clicks_7_days": {
+ "type": "long"
+ },
+ "clicks_30_days": {
+ "type": "long"
+ },
+ "clicks_90_days": {
+ "type": "long"
+ },
+ "minutes_on_screen_total": {
+ "type": "float"
+ },
+ "minutes_on_screen_7_days": {
+ "type": "float"
+ },
+ "minutes_on_screen_30_days": {
+ "type": "float"
+ },
+ "minutes_on_screen_90_days": {
+ "type": "float"
+ }
+ }
+ },
"appSearch": {
"properties": {
"clicks_total": {
diff --git a/x-pack/plugins/enterprise_search/common/constants.ts b/x-pack/plugins/enterprise_search/common/constants.ts
index 05d27d7337a6e..6e2f0c0f24b7a 100644
--- a/x-pack/plugins/enterprise_search/common/constants.ts
+++ b/x-pack/plugins/enterprise_search/common/constants.ts
@@ -11,7 +11,24 @@ export const ENTERPRISE_SEARCH_PLUGIN = {
NAME: i18n.translate('xpack.enterpriseSearch.productName', {
defaultMessage: 'Enterprise Search',
}),
- URL: '/app/enterprise_search',
+ NAV_TITLE: i18n.translate('xpack.enterpriseSearch.navTitle', {
+ defaultMessage: 'Overview',
+ }),
+ SUBTITLE: i18n.translate('xpack.enterpriseSearch.featureCatalogue.subtitle', {
+ defaultMessage: 'Search everything',
+ }),
+ DESCRIPTIONS: [
+ i18n.translate('xpack.enterpriseSearch.featureCatalogueDescription1', {
+ defaultMessage: 'Build a powerful search experience.',
+ }),
+ i18n.translate('xpack.enterpriseSearch.featureCatalogueDescription2', {
+ defaultMessage: 'Connect your users to relevant data.',
+ }),
+ i18n.translate('xpack.enterpriseSearch.featureCatalogueDescription3', {
+ defaultMessage: 'Unify your team content.',
+ }),
+ ],
+ URL: '/app/enterprise_search/overview',
};
export const APP_SEARCH_PLUGIN = {
@@ -23,6 +40,10 @@ export const APP_SEARCH_PLUGIN = {
defaultMessage:
'Leverage dashboards, analytics, and APIs for advanced application search made simple.',
}),
+ CARD_DESCRIPTION: i18n.translate('xpack.enterpriseSearch.appSearch.productCardDescription', {
+ defaultMessage:
+ 'Elastic App Search provides user-friendly tools to design and deploy a powerful search to your websites or web/mobile applications.',
+ }),
URL: '/app/enterprise_search/app_search',
SUPPORT_URL: 'https://discuss.elastic.co/c/enterprise-search/app-search/',
};
@@ -36,6 +57,13 @@ export const WORKPLACE_SEARCH_PLUGIN = {
defaultMessage:
'Search all documents, files, and sources available across your virtual workplace.',
}),
+ CARD_DESCRIPTION: i18n.translate(
+ 'xpack.enterpriseSearch.workplaceSearch.productCardDescription',
+ {
+ defaultMessage:
+ "Unify all your team's content in one place, with instant connectivity to popular productivity and collaboration tools.",
+ }
+ ),
URL: '/app/enterprise_search/workplace_search',
SUPPORT_URL: 'https://discuss.elastic.co/c/enterprise-search/workplace-search/',
};
diff --git a/x-pack/plugins/enterprise_search/common/types/index.ts b/x-pack/plugins/enterprise_search/common/types/index.ts
index a41a42da477ee..d5774adc0d516 100644
--- a/x-pack/plugins/enterprise_search/common/types/index.ts
+++ b/x-pack/plugins/enterprise_search/common/types/index.ts
@@ -18,6 +18,10 @@ export interface IInitialAppData {
ilmEnabled?: boolean;
isFederatedAuth?: boolean;
configuredLimits?: IConfiguredLimits;
+ access?: {
+ hasAppSearchAccess: boolean;
+ hasWorkplaceSearchAccess: boolean;
+ };
appSearch?: IAppSearchAccount;
workplaceSearch?: IWorkplaceSearchInitialData;
}
diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts
index 779eb1a043e8c..842dcefd3aef8 100644
--- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts
@@ -9,7 +9,7 @@
* Jest to accept its use within a jest.mock()
*/
export const mockHistory = {
- createHref: jest.fn(({ pathname }) => `/enterprise_search${pathname}`),
+ createHref: jest.fn(({ pathname }) => `/app/enterprise_search${pathname}`),
push: jest.fn(),
location: {
pathname: '/current-path',
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/assets/app_search.png b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/assets/app_search.png
new file mode 100644
index 0000000000000000000000000000000000000000..6cf0639167e2fe45a58adfebe78c4864ac336e3a
GIT binary patch
literal 25568
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
literal 0
HcmV?d00001
diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/assets/bg_enterprise_search.png b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/assets/bg_enterprise_search.png
new file mode 100644
index 0000000000000000000000000000000000000000..1b5e1e489fd96c8a764a2e2802f42a1d77272ee7
GIT binary patch
literal 24757
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