diff --git a/.ci/Dockerfile b/.ci/Dockerfile
index 445cc0e51073f7..1c59d6d9aaaf81 100644
--- a/.ci/Dockerfile
+++ b/.ci/Dockerfile
@@ -1,7 +1,7 @@
# NOTE: This Dockerfile is ONLY used to run certain tasks in CI. It is not used to run Kibana or as a distributable.
# If you're looking for the Kibana Docker image distributable, please see: src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts
-ARG NODE_VERSION=14.16.0
+ARG NODE_VERSION=14.16.1
FROM node:${NODE_VERSION} AS base
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index a8dcafeb7753c0..92e39c2e634e50 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -79,6 +79,7 @@
# Uptime
/x-pack/plugins/uptime @elastic/uptime
+/x-pack/plugins/observability/public/components/shared/exploratory_view @elastic/uptime
/x-pack/test/functional_with_es_ssl/apps/uptime @elastic/uptime
/x-pack/test/functional/apps/uptime @elastic/uptime
/x-pack/test/api_integration/apis/uptime @elastic/uptime
diff --git a/.node-version b/.node-version
index 2a0dc9a810cf34..6b17d228d33517 100644
--- a/.node-version
+++ b/.node-version
@@ -1 +1 @@
-14.16.0
+14.16.1
diff --git a/.nvmrc b/.nvmrc
index 2a0dc9a810cf34..6b17d228d33517 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-14.16.0
+14.16.1
diff --git a/NOTICE.txt b/NOTICE.txt
index 2341a478cbda90..4eec329b7a6033 100644
--- a/NOTICE.txt
+++ b/NOTICE.txt
@@ -261,33 +261,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
----
-This product bundles childnode-remove which is available under a
-"MIT" license.
-
-The MIT License (MIT)
-
-Copyright (c) 2016-present, jszhou
-https://github.com/jserz/js_piece
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
-
---
This product bundles code based on probot-metadata@1.0.0 which is
available under a "MIT" license.
diff --git a/WORKSPACE.bazel b/WORKSPACE.bazel
index 4639414b4564e2..e74c646eedeaf8 100644
--- a/WORKSPACE.bazel
+++ b/WORKSPACE.bazel
@@ -27,13 +27,13 @@ check_rules_nodejs_version(minimum_version_string = "3.2.3")
# we can update that rule.
node_repositories(
node_repositories = {
- "14.16.0-darwin_amd64": ("node-v14.16.0-darwin-x64.tar.gz", "node-v14.16.0-darwin-x64", "14ec767e376d1e2e668f997065926c5c0086ec46516d1d45918af8ae05bd4583"),
- "14.16.0-linux_arm64": ("node-v14.16.0-linux-arm64.tar.xz", "node-v14.16.0-linux-arm64", "440489a08bfd020e814c9e65017f58d692299ac3f150c8e78d01abb1104c878a"),
- "14.16.0-linux_s390x": ("node-v14.16.0-linux-s390x.tar.xz", "node-v14.16.0-linux-s390x", "335348e46f45284b6356416ef58f85602d2dee99094588b65900f6c8839df77e"),
- "14.16.0-linux_amd64": ("node-v14.16.0-linux-x64.tar.xz", "node-v14.16.0-linux-x64", "2e079cf638766fedd720d30ec8ffef5d6ceada4e8b441fc2a093cb9a865f4087"),
- "14.16.0-windows_amd64": ("node-v14.16.0-win-x64.zip", "node-v14.16.0-win-x64", "716045c2f16ea10ca97bd04cf2e5ef865f9c4d6d677a9bc25e2ea522b594af4f"),
+ "14.16.1-darwin_amd64": ("node-v14.16.1-darwin-x64.tar.gz", "node-v14.16.1-darwin-x64", "b762b72fc149629b7e394ea9b75a093cad709a9f2f71480942945d8da0fc1218"),
+ "14.16.1-linux_arm64": ("node-v14.16.1-linux-arm64.tar.xz", "node-v14.16.1-linux-arm64", "b4d474e79f7d33b3b4430fad25c3f836b82ce2d5bb30d4a2c9fa20df027e40da"),
+ "14.16.1-linux_s390x": ("node-v14.16.1-linux-s390x.tar.xz", "node-v14.16.1-linux-s390x", "af9982fef32e4a3e4a5d66741dcf30ac9c27613bd73582fa1dae1fb25003047a"),
+ "14.16.1-linux_amd64": ("node-v14.16.1-linux-x64.tar.xz", "node-v14.16.1-linux-x64", "85a89d2f68855282c87851c882d4c4bbea4cd7f888f603722f0240a6e53d89df"),
+ "14.16.1-windows_amd64": ("node-v14.16.1-win-x64.zip", "node-v14.16.1-win-x64", "e469db37b4df74627842d809566c651042d86f0e6006688f0f5fe3532c6dfa41"),
},
- node_version = "14.16.0",
+ node_version = "14.16.1",
node_urls = [
"https://nodejs.org/dist/v{version}/{filename}",
],
diff --git a/docs/developer/advanced/upgrading-nodejs.asciidoc b/docs/developer/advanced/upgrading-nodejs.asciidoc
index c1e727b1eac659..3827cb6e9aa7de 100644
--- a/docs/developer/advanced/upgrading-nodejs.asciidoc
+++ b/docs/developer/advanced/upgrading-nodejs.asciidoc
@@ -14,10 +14,14 @@ Theses files must be updated when upgrading Node.js:
- {kib-repo}blob/{branch}/.node-version[`.node-version`]
- {kib-repo}blob/{branch}/.nvmrc[`.nvmrc`]
- {kib-repo}blob/{branch}/package.json[`package.json`] - The version is specified in the `engines.node` field.
+ - {kib-repo}blob/{branch}/WORKSPACE.bazel[`WORKSPACE.bazel`] - The version is specified in the `node_version` property.
+ Besides this property, the list of files under `node_repositories` must be updated along with their respective SHA256 hashes.
+ These can be found on the https://nodejs.org[nodejs.org] website.
+ Example for Node.js v14.16.1: https://nodejs.org/dist/v14.16.1/SHASUMS256.txt.asc
-See PR {kib-repo}pull/86593[#86593] for an example of how the Node.js version has been upgraded previously.
+See PR {kib-repo}pull/96382[#96382] for an example of how the Node.js version has been upgraded previously.
-In the 6.8 branch, the `.ci/Dockerfile` file does not exist, so when upgrading Node.js in that branch, just skip that file.
+In the 6.8 branch, neither the `.ci/Dockerfile` file nor the `WORKSPACE.bazel` file exists, so when upgrading Node.js in that branch, just skip those files.
=== Backporting
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.autorefreshdonefn.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.autorefreshdonefn.md
new file mode 100644
index 00000000000000..a5694ea2d1af93
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.autorefreshdonefn.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AutoRefreshDoneFn](./kibana-plugin-plugins-data-public.autorefreshdonefn.md)
+
+## AutoRefreshDoneFn type
+
+Signature:
+
+```typescript
+export declare type AutoRefreshDoneFn = () => void;
+```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.kbn_field_types.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.kbn_field_types.md
index 4d75dda61d5c9a..521ceeb1e37f22 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.kbn_field_types.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.kbn_field_types.md
@@ -27,6 +27,7 @@ export declare enum KBN_FIELD_TYPES
| HISTOGRAM | "histogram"
| |
| IP | "ip"
| |
| IP\_RANGE | "ip_range"
| |
+| MISSING | "missing"
| |
| MURMUR3 | "murmur3"
| |
| NESTED | "nested"
| |
| NUMBER | "number"
| |
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md
index d2e7ef9db05e8e..4429f45f556450 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md
@@ -47,6 +47,7 @@
| [getSearchParamsFromRequest(searchRequest, dependencies)](./kibana-plugin-plugins-data-public.getsearchparamsfromrequest.md) | |
| [getTime(indexPattern, timeRange, options)](./kibana-plugin-plugins-data-public.gettime.md) | |
| [plugin(initializerContext)](./kibana-plugin-plugins-data-public.plugin.md) | |
+| [waitUntilNextSessionCompletes$(sessionService, { waitForIdle })](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md) | Creates an observable that emits when next search session completes. This utility is helpful to use in the application to delay some tasks until next session completes. |
## Interfaces
@@ -92,6 +93,7 @@
| [SearchInterceptorDeps](./kibana-plugin-plugins-data-public.searchinterceptordeps.md) | |
| [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.md) | Provide info about current search session to be stored in the Search Session saved object |
| [SearchSourceFields](./kibana-plugin-plugins-data-public.searchsourcefields.md) | search source fields |
+| [WaitUntilNextSessionCompletesOptions](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.md) | Options for [waitUntilNextSessionCompletes$()](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md) |
## Variables
@@ -141,6 +143,7 @@
| [AggParam](./kibana-plugin-plugins-data-public.aggparam.md) | |
| [AggsStart](./kibana-plugin-plugins-data-public.aggsstart.md) | AggsStart represents the actual external contract as AggsCommonStart is only used internally. The difference is that AggsStart includes the typings for the registry with initialized agg types. |
| [AutocompleteStart](./kibana-plugin-plugins-data-public.autocompletestart.md) | \* |
+| [AutoRefreshDoneFn](./kibana-plugin-plugins-data-public.autorefreshdonefn.md) | |
| [CustomFilter](./kibana-plugin-plugins-data-public.customfilter.md) | |
| [EsaggsExpressionFunctionDefinition](./kibana-plugin-plugins-data-public.esaggsexpressionfunctiondefinition.md) | |
| [EsdslExpressionFunctionDefinition](./kibana-plugin-plugins-data-public.esdslexpressionfunctiondefinition.md) | |
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md
new file mode 100644
index 00000000000000..a4b294fb1decd0
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md
@@ -0,0 +1,25 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [waitUntilNextSessionCompletes$](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md)
+
+## waitUntilNextSessionCompletes$() function
+
+Creates an observable that emits when next search session completes. This utility is helpful to use in the application to delay some tasks until next session completes.
+
+Signature:
+
+```typescript
+export declare function waitUntilNextSessionCompletes$(sessionService: ISessionService, { waitForIdle }?: WaitUntilNextSessionCompletesOptions): import("rxjs").Observable;
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| sessionService | ISessionService
| [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) |
+| { waitForIdle } | WaitUntilNextSessionCompletesOptions
| |
+
+Returns:
+
+`import("rxjs").Observable`
+
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.md
new file mode 100644
index 00000000000000..d575722a22453a
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.md
@@ -0,0 +1,20 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [WaitUntilNextSessionCompletesOptions](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.md)
+
+## WaitUntilNextSessionCompletesOptions interface
+
+Options for [waitUntilNextSessionCompletes$()](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md)
+
+Signature:
+
+```typescript
+export interface WaitUntilNextSessionCompletesOptions
+```
+
+## Properties
+
+| Property | Type | Description |
+| --- | --- | --- |
+| [waitForIdle](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.waitforidle.md) | number
| For how long to wait between session state transitions before considering that session completed |
+
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.waitforidle.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.waitforidle.md
new file mode 100644
index 00000000000000..60d3df77838522
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.waitforidle.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [WaitUntilNextSessionCompletesOptions](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.md) > [waitForIdle](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.waitforidle.md)
+
+## WaitUntilNextSessionCompletesOptions.waitForIdle property
+
+For how long to wait between session state transitions before considering that session completed
+
+Signature:
+
+```typescript
+waitForIdle?: number;
+```
diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.kbn_field_types.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.kbn_field_types.md
index be4c3705bd8deb..40fa872ff0fc61 100644
--- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.kbn_field_types.md
+++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.kbn_field_types.md
@@ -27,6 +27,7 @@ export declare enum KBN_FIELD_TYPES
| HISTOGRAM | "histogram"
| |
| IP | "ip"
| |
| IP\_RANGE | "ip_range"
| |
+| MISSING | "missing"
| |
| MURMUR3 | "murmur3"
| |
| NESTED | "nested"
| |
| NUMBER | "number"
| |
diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.getupdated_.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.getupdated_.md
index 5201444e69867e..290dc106625696 100644
--- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.getupdated_.md
+++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.getupdated_.md
@@ -9,9 +9,9 @@ Merges input$ and output$ streams and debounces emit till next macro-task. Could
Signature:
```typescript
-getUpdated$(): Readonly>;
+getUpdated$(): Readonly>;
```
Returns:
-`Readonly>`
+`Readonly>`
diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc
index 3645499d5f9ffe..20bbbcf874c051 100644
--- a/docs/settings/alert-action-settings.asciidoc
+++ b/docs/settings/alert-action-settings.asciidoc
@@ -59,6 +59,12 @@ You can configure the following settings in the `kibana.yml` file.
| `xpack.actions.proxyUrl` {ess-icon}
| Specifies the proxy URL to use, if using a proxy for actions. By default, no proxy is used.
+| `xpack.actions.proxyBypassHosts` {ess-icon}
+ | Specifies hostnames which should not use the proxy, if using a proxy for actions. The value is an array of hostnames as strings. By default, all hosts will use the proxy, but if an action's hostname is in this list, the proxy will not be used. The settings `xpack.actions.proxyBypassHosts` and `xpack.actions.proxyOnlyHosts` cannot be used at the same time.
+
+| `xpack.actions.proxyOnlyHosts` {ess-icon}
+ | Specifies hostnames which should only use the proxy, if using a proxy for actions. The value is an array of hostnames as strings. By default, no hosts will use the proxy, but if an action's hostname is in this list, the proxy will be used. The settings `xpack.actions.proxyBypassHosts` and `xpack.actions.proxyOnlyHosts` cannot be used at the same time.
+
| `xpack.actions.proxyHeaders` {ess-icon}
| Specifies HTTP headers for the proxy, if using a proxy for actions. Defaults to {}.
@@ -71,6 +77,13 @@ a|`xpack.actions.`
+
As an alternative to setting both `xpack.actions.proxyRejectUnauthorizedCertificates` and `xpack.actions.rejectUnauthorized`, you can point the OS level environment variable `NODE_EXTRA_CA_CERTS` to a file that contains the root CAs needed to trust certificates.
+| `xpack.actions.maxResponseContentLength` {ess-icon}
+ | Specifies the max number of bytes of the http response for requests to external resources. Defaults to 1000000 (1MB).
+
+| `xpack.actions.responseTimeout` {ess-icon}
+ | Specifies the time allowed for requests to external resources. Requests that take longer are aborted. The time is formatted as [ms|s|m|h|d|w|M|Y], for example, '20m', '24h', '7d', '1w'. Defaults to 60s.
+
+
|===
[float]
diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc
index 643718b961650c..90e813afad6f44 100644
--- a/docs/setup/settings.asciidoc
+++ b/docs/setup/settings.asciidoc
@@ -429,6 +429,15 @@ to display map tiles in tilemap visualizations. By default,
override this parameter to use their own Tile Map Service. For example:
`"https://tiles.elastic.co/v2/default/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana"`
+| `migrations.batchSize:`
+ | Defines the number of documents migrated at a time. The higher the value, the faster the Saved Objects migration process performs at the cost of higher memory consumption. If the migration fails due to a `circuit_breaking_exception`, set a smaller `batchSize` value. *Default: `1000`*
+
+| `migrations.enableV2:`
+ | experimental[]. Enables the new Saved Objects migration algorithm. For information about the migration algorithm, refer to <>. When `migrations v2` is stable, the setting will be removed in an upcoming release without any further notice. Setting the value to `false` causes {kib} to use the legacy migration algorithm, which shipped in 7.11 and earlier versions. *Default: `true`*
+
+| `migrations.retryAttempts:`
+ | The number of times migrations retry temporary failures, such as a network timeout, 503 status code, or `snapshot_in_progress_exception`. When upgrade migrations frequently fail after exhausting all retry attempts with a message such as `Unable to complete the [...] step after 15 attempts, terminating.`, increase the setting value. *Default: `15`*
+
| `newsfeed.enabled:`
| Controls whether to enable the newsfeed
system for the {kib} UI notification center. Set to `false` to disable the
diff --git a/docs/user/dashboard/timelion.asciidoc b/docs/user/dashboard/timelion.asciidoc
index 80ce77f30c75e5..ff71cd7b383bdc 100644
--- a/docs/user/dashboard/timelion.asciidoc
+++ b/docs/user/dashboard/timelion.asciidoc
@@ -33,6 +33,40 @@ If the value of your parameter contains spaces or commas you have to put the val
.es(q='some query', index=logstash-*)
+[float]
+[[customize-data-series-y-axis]]
+===== .yaxis() function
+
+{kib} supports many y-axis scales and ranges for your data series.
+
+The `.yaxis()` function supports the following parameters:
+
+* *yaxis* — The numbered y-axis to plot the series on. For example, use `.yaxis(2)` to display a second y-axis.
+* *min* — The minimum value for the y-axis range.
+* *max* — The maximum value for the y-axis range.
+* *position* — The location of the units. Values include `left` or `right`.
+* *label* — The label for the axis.
+* *color* — The color of the axis label.
+* *units* — The function to use for formatting the y-axis labels. Values include `bits`, `bits/s`, `bytes`, `bytes/s`, `currency(:ISO 4217 currency code)`, `percent`, and `custom(:prefix:suffix)`.
+* *tickDecimals* — The tick decimal precision.
+
+Example:
+
+[source,text]
+----------------------------------
+.es(index= kibana_sample_data_logs,
+ timefield='@timestamp',
+ metric='avg:bytes')
+ .label('Average Bytes for request')
+ .title('Memory consumption over time in bytes').yaxis(1,units=bytes,position=left), <1>
+.es(index= kibana_sample_data_logs,
+ timefield='@timestamp',
+ metric=avg:machine.ram)
+ .label('Average Machine RAM amount').yaxis(2,units=bytes,position=right) <2>
+----------------------------------
+
+<1> `.yaxis(1,units=bytes,position=left)` — Specifies the first y-axis for the first data series, and changes the units on the left.
+<2> `.yaxis(2,units=bytes,position=left)` — Specifies the second y-axis for the second data series, and changes the units on the right.
[float]
==== Tutorial: Create visualizations with Timelion
diff --git a/package.json b/package.json
index bb383e986e7218..9bddca46654674 100644
--- a/package.json
+++ b/package.json
@@ -92,7 +92,7 @@
"**/typescript": "4.1.3"
},
"engines": {
- "node": "14.16.0",
+ "node": "14.16.1",
"yarn": "^1.21.1"
},
"dependencies": {
@@ -131,10 +131,12 @@
"@kbn/crypto": "link:packages/kbn-crypto",
"@kbn/i18n": "link:packages/kbn-i18n",
"@kbn/interpreter": "link:packages/kbn-interpreter",
+ "@kbn/io-ts-utils": "link:packages/kbn-io-ts-utils",
"@kbn/legacy-logging": "link:packages/kbn-legacy-logging",
"@kbn/logging": "link:packages/kbn-logging",
"@kbn/monaco": "link:packages/kbn-monaco",
"@kbn/server-http-tools": "link:packages/kbn-server-http-tools",
+ "@kbn/server-route-repository": "link:packages/kbn-server-route-repository",
"@kbn/std": "link:packages/kbn-std",
"@kbn/tinymath": "link:packages/kbn-tinymath",
"@kbn/ui-framework": "link:packages/kbn-ui-framework",
@@ -206,8 +208,6 @@
"content-disposition": "0.5.3",
"copy-to-clipboard": "^3.0.8",
"core-js": "^3.6.5",
- "css-minimizer-webpack-plugin": "^1.3.0",
- "custom-event-polyfill": "^0.3.0",
"cytoscape": "^3.10.0",
"cytoscape-dagre": "^2.2.2",
"d3": "3.5.17",
@@ -682,6 +682,7 @@
"copy-webpack-plugin": "^6.0.2",
"cpy": "^8.1.1",
"css-loader": "^3.4.2",
+ "css-minimizer-webpack-plugin": "^1.3.0",
"cypress": "^6.8.0",
"cypress-cucumber-preprocessor": "^2.5.2",
"cypress-multi-reporters": "^1.4.0",
diff --git a/packages/kbn-analytics/tsconfig.json b/packages/kbn-analytics/tsconfig.json
index c2e579e7fdbead..80a2255d718051 100644
--- a/packages/kbn-analytics/tsconfig.json
+++ b/packages/kbn-analytics/tsconfig.json
@@ -7,6 +7,7 @@
"emitDeclarationOnly": true,
"declaration": true,
"declarationMap": true,
+ "isolatedModules": true,
"sourceMap": true,
"sourceRoot": "../../../../../packages/kbn-analytics/src",
"types": [
diff --git a/packages/kbn-dev-utils/src/plugins/simple_kibana_platform_plugin_discovery.ts b/packages/kbn-dev-utils/src/plugins/simple_kibana_platform_plugin_discovery.ts
index 26b1a6fa2e8049..2381faefbff29f 100644
--- a/packages/kbn-dev-utils/src/plugins/simple_kibana_platform_plugin_discovery.ts
+++ b/packages/kbn-dev-utils/src/plugins/simple_kibana_platform_plugin_discovery.ts
@@ -9,6 +9,7 @@
import Path from 'path';
import globby from 'globby';
+import normalize from 'normalize-path';
import { parseKibanaPlatformPlugin } from './parse_kibana_platform_plugin';
@@ -32,7 +33,7 @@ export function simpleKibanaPlatformPluginDiscovery(scanDirs: string[], pluginPa
),
...pluginPaths.map((path) => Path.resolve(path, `kibana.json`)),
])
- );
+ ).map((path) => normalize(path));
const manifestPaths = globby.sync(patterns, { absolute: true }).map((path) =>
// absolute paths returned from globby are using normalize or
diff --git a/packages/kbn-io-ts-utils/jest.config.js b/packages/kbn-io-ts-utils/jest.config.js
new file mode 100644
index 00000000000000..1a71166fae843b
--- /dev/null
+++ b/packages/kbn-io-ts-utils/jest.config.js
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+module.exports = {
+ preset: '@kbn/test',
+ rootDir: '../..',
+ roots: ['/packages/kbn-io-ts-utils'],
+};
diff --git a/packages/kbn-io-ts-utils/package.json b/packages/kbn-io-ts-utils/package.json
new file mode 100644
index 00000000000000..4d6f02d3f85a62
--- /dev/null
+++ b/packages/kbn-io-ts-utils/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "@kbn/io-ts-utils",
+ "main": "./target/index.js",
+ "types": "./target/index.d.ts",
+ "version": "1.0.0",
+ "license": "SSPL-1.0 OR Elastic License 2.0",
+ "private": true,
+ "scripts": {
+ "build": "../../node_modules/.bin/tsc",
+ "kbn:bootstrap": "yarn build",
+ "kbn:watch": "yarn build --watch"
+ }
+}
diff --git a/packages/kbn-io-ts-utils/src/index.ts b/packages/kbn-io-ts-utils/src/index.ts
new file mode 100644
index 00000000000000..2032127b1eb917
--- /dev/null
+++ b/packages/kbn-io-ts-utils/src/index.ts
@@ -0,0 +1,11 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export { jsonRt } from './json_rt';
+export { mergeRt } from './merge_rt';
+export { strictKeysRt } from './strict_keys_rt';
diff --git a/x-pack/plugins/apm/common/runtime_types/json_rt/index.test.ts b/packages/kbn-io-ts-utils/src/json_rt/index.test.ts
similarity index 85%
rename from x-pack/plugins/apm/common/runtime_types/json_rt/index.test.ts
rename to packages/kbn-io-ts-utils/src/json_rt/index.test.ts
index d6c286c672d906..1220639fc7befd 100644
--- a/x-pack/plugins/apm/common/runtime_types/json_rt/index.test.ts
+++ b/packages/kbn-io-ts-utils/src/json_rt/index.test.ts
@@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
import * as t from 'io-ts';
@@ -12,9 +13,7 @@ import { Right } from 'fp-ts/lib/Either';
import { pipe } from 'fp-ts/lib/pipeable';
import { identity } from 'fp-ts/lib/function';
-function getValueOrThrow>(
- either: TEither
-): Right {
+function getValueOrThrow>(either: TEither): Right {
const value = pipe(
either,
fold(() => {
diff --git a/x-pack/plugins/apm/common/runtime_types/json_rt/index.ts b/packages/kbn-io-ts-utils/src/json_rt/index.ts
similarity index 74%
rename from x-pack/plugins/apm/common/runtime_types/json_rt/index.ts
rename to packages/kbn-io-ts-utils/src/json_rt/index.ts
index 0207145a17be76..bc596d53db54c9 100644
--- a/x-pack/plugins/apm/common/runtime_types/json_rt/index.ts
+++ b/packages/kbn-io-ts-utils/src/json_rt/index.ts
@@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
import * as t from 'io-ts';
diff --git a/x-pack/plugins/apm/common/runtime_types/merge/index.test.ts b/packages/kbn-io-ts-utils/src/merge_rt/index.test.ts
similarity index 66%
rename from x-pack/plugins/apm/common/runtime_types/merge/index.test.ts
rename to packages/kbn-io-ts-utils/src/merge_rt/index.test.ts
index af5a0221662d51..b25d4451895f2f 100644
--- a/x-pack/plugins/apm/common/runtime_types/merge/index.test.ts
+++ b/packages/kbn-io-ts-utils/src/merge_rt/index.test.ts
@@ -1,18 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
import * as t from 'io-ts';
import { isLeft } from 'fp-ts/lib/Either';
-import { merge } from './';
+import { mergeRt } from '.';
import { jsonRt } from '../json_rt';
describe('merge', () => {
it('fails on one or more errors', () => {
- const type = merge([t.type({ foo: t.string }), t.type({ bar: t.number })]);
+ const type = mergeRt(t.type({ foo: t.string }), t.type({ bar: t.number }));
const result = type.decode({ foo: '' });
@@ -20,10 +21,7 @@ describe('merge', () => {
});
it('merges left to right', () => {
- const typeBoolean = merge([
- t.type({ foo: t.string }),
- t.type({ foo: jsonRt.pipe(t.boolean) }),
- ]);
+ const typeBoolean = mergeRt(t.type({ foo: t.string }), t.type({ foo: jsonRt.pipe(t.boolean) }));
const resultBoolean = typeBoolean.decode({
foo: 'true',
@@ -34,10 +32,7 @@ describe('merge', () => {
foo: true,
});
- const typeString = merge([
- t.type({ foo: jsonRt.pipe(t.boolean) }),
- t.type({ foo: t.string }),
- ]);
+ const typeString = mergeRt(t.type({ foo: jsonRt.pipe(t.boolean) }), t.type({ foo: t.string }));
const resultString = typeString.decode({
foo: 'true',
@@ -50,10 +45,10 @@ describe('merge', () => {
});
it('deeply merges values', () => {
- const type = merge([
+ const type = mergeRt(
t.type({ foo: t.type({ baz: t.string }) }),
- t.type({ foo: t.type({ bar: t.string }) }),
- ]);
+ t.type({ foo: t.type({ bar: t.string }) })
+ );
const result = type.decode({
foo: {
diff --git a/x-pack/plugins/apm/common/runtime_types/merge/index.ts b/packages/kbn-io-ts-utils/src/merge_rt/index.ts
similarity index 62%
rename from x-pack/plugins/apm/common/runtime_types/merge/index.ts
rename to packages/kbn-io-ts-utils/src/merge_rt/index.ts
index 451edf678aabe5..c582767fb51018 100644
--- a/x-pack/plugins/apm/common/runtime_types/merge/index.ts
+++ b/packages/kbn-io-ts-utils/src/merge_rt/index.ts
@@ -1,31 +1,40 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
import * as t from 'io-ts';
import { merge as lodashMerge } from 'lodash';
import { isLeft } from 'fp-ts/lib/Either';
-import { ValuesType } from 'utility-types';
-export type MergeType<
- T extends t.Any[],
- U extends ValuesType = ValuesType
-> = t.Type & {
- _tag: 'MergeType';
- types: T;
-};
+type PlainObject = Record;
+
+type DeepMerge = U extends PlainObject
+ ? T extends PlainObject
+ ? Omit &
+ {
+ [key in keyof U]: T extends { [k in key]: any } ? DeepMerge : U[key];
+ }
+ : U
+ : U;
// this is similar to t.intersection, but does a deep merge
// instead of a shallow merge
-export function merge(
- types: [A, B]
-): MergeType<[A, B]>;
+export type MergeType = t.Type<
+ DeepMerge, t.TypeOf>,
+ DeepMerge, t.OutputOf>
+> & {
+ _tag: 'MergeType';
+ types: [T1, T2];
+};
+
+export function mergeRt(a: T1, b: T2): MergeType;
-export function merge(types: t.Any[]) {
+export function mergeRt(...types: t.Any[]) {
const mergeType = new t.Type(
'merge',
(u): u is unknown => {
diff --git a/x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.test.ts b/packages/kbn-io-ts-utils/src/strict_keys_rt/index.test.ts
similarity index 77%
rename from x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.test.ts
rename to packages/kbn-io-ts-utils/src/strict_keys_rt/index.test.ts
index 4212e0430ff5f3..ab20ca42a283e5 100644
--- a/x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.test.ts
+++ b/packages/kbn-io-ts-utils/src/strict_keys_rt/index.test.ts
@@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
import * as t from 'io-ts';
@@ -14,10 +15,7 @@ describe('strictKeysRt', () => {
it('correctly and deeply validates object keys', () => {
const checks: Array<{ type: t.Type; passes: any[]; fails: any[] }> = [
{
- type: t.intersection([
- t.type({ foo: t.string }),
- t.partial({ bar: t.string }),
- ]),
+ type: t.intersection([t.type({ foo: t.string }), t.partial({ bar: t.string })]),
passes: [{ foo: '' }, { foo: '', bar: '' }],
fails: [
{ foo: '', unknownKey: '' },
@@ -26,15 +24,9 @@ describe('strictKeysRt', () => {
},
{
type: t.type({
- path: t.union([
- t.type({ serviceName: t.string }),
- t.type({ transactionType: t.string }),
- ]),
+ path: t.union([t.type({ serviceName: t.string }), t.type({ transactionType: t.string })]),
}),
- passes: [
- { path: { serviceName: '' } },
- { path: { transactionType: '' } },
- ],
+ passes: [{ path: { serviceName: '' } }, { path: { transactionType: '' } }],
fails: [
{ path: { serviceName: '', unknownKey: '' } },
{ path: { transactionType: '', unknownKey: '' } },
@@ -62,9 +54,7 @@ describe('strictKeysRt', () => {
if (!isRight(result)) {
throw new Error(
- `Expected ${JSON.stringify(
- value
- )} to be allowed, but validation failed with ${
+ `Expected ${JSON.stringify(value)} to be allowed, but validation failed with ${
result.left[0].message
}`
);
@@ -76,9 +66,7 @@ describe('strictKeysRt', () => {
if (!isLeft(result)) {
throw new Error(
- `Expected ${JSON.stringify(
- value
- )} to be disallowed, but validation succeeded`
+ `Expected ${JSON.stringify(value)} to be disallowed, but validation succeeded`
);
}
});
diff --git a/x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.ts b/packages/kbn-io-ts-utils/src/strict_keys_rt/index.ts
similarity index 66%
rename from x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.ts
rename to packages/kbn-io-ts-utils/src/strict_keys_rt/index.ts
index e90ccf7eb8d318..56afdf54463f73 100644
--- a/x-pack/plugins/apm/common/runtime_types/strict_keys_rt/index.ts
+++ b/packages/kbn-io-ts-utils/src/strict_keys_rt/index.ts
@@ -1,14 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
*/
import * as t from 'io-ts';
import { either, isRight } from 'fp-ts/lib/Either';
import { mapValues, difference, isPlainObject, forEach } from 'lodash';
-import { MergeType, merge } from '../merge';
+import { MergeType, mergeRt } from '../merge_rt';
/*
Type that tracks validated keys, and fails when the input value
@@ -21,7 +22,7 @@ type ParsableType =
| t.PartialType
| t.ExactType
| t.InterfaceType
- | MergeType;
+ | MergeType;
function getKeysInObject>(
object: T,
@@ -32,17 +33,16 @@ function getKeysInObject>(
const ownPrefix = prefix ? `${prefix}.${key}` : key;
keys.push(ownPrefix);
if (isPlainObject(object[key])) {
- keys.push(
- ...getKeysInObject(object[key] as Record, ownPrefix)
- );
+ keys.push(...getKeysInObject(object[key] as Record, ownPrefix));
}
});
return keys;
}
-function addToContextWhenValidated<
- T extends t.InterfaceType | t.PartialType
->(type: T, prefix: string): T {
+function addToContextWhenValidated | t.PartialType>(
+ type: T,
+ prefix: string
+): T {
const validate = (input: unknown, context: t.Context) => {
const result = type.validate(input, context);
const keysType = context[0].type as StrictKeysType;
@@ -50,36 +50,19 @@ function addToContextWhenValidated<
throw new Error('Expected a top-level StrictKeysType');
}
if (isRight(result)) {
- keysType.trackedKeys.push(
- ...Object.keys(type.props).map((propKey) => `${prefix}${propKey}`)
- );
+ keysType.trackedKeys.push(...Object.keys(type.props).map((propKey) => `${prefix}${propKey}`));
}
return result;
};
if (type._tag === 'InterfaceType') {
- return new t.InterfaceType(
- type.name,
- type.is,
- validate,
- type.encode,
- type.props
- ) as T;
+ return new t.InterfaceType(type.name, type.is, validate, type.encode, type.props) as T;
}
- return new t.PartialType(
- type.name,
- type.is,
- validate,
- type.encode,
- type.props
- ) as T;
+ return new t.PartialType(type.name, type.is, validate, type.encode, type.props) as T;
}
-function trackKeysOfValidatedTypes(
- type: ParsableType | t.Any,
- prefix: string = ''
-): t.Any {
+function trackKeysOfValidatedTypes(type: ParsableType | t.Any, prefix: string = ''): t.Any {
if (!('_tag' in type)) {
return type;
}
@@ -89,27 +72,24 @@ function trackKeysOfValidatedTypes(
case 'IntersectionType': {
const collectionType = type as t.IntersectionType;
return t.intersection(
- collectionType.types.map((rt) =>
- trackKeysOfValidatedTypes(rt, prefix)
- ) as [t.Any, t.Any]
+ collectionType.types.map((rt) => trackKeysOfValidatedTypes(rt, prefix)) as [t.Any, t.Any]
);
}
case 'UnionType': {
const collectionType = type as t.UnionType;
return t.union(
- collectionType.types.map((rt) =>
- trackKeysOfValidatedTypes(rt, prefix)
- ) as [t.Any, t.Any]
+ collectionType.types.map((rt) => trackKeysOfValidatedTypes(rt, prefix)) as [t.Any, t.Any]
);
}
case 'MergeType': {
- const collectionType = type as MergeType;
- return merge(
- collectionType.types.map((rt) =>
- trackKeysOfValidatedTypes(rt, prefix)
- ) as [t.Any, t.Any]
+ const collectionType = type as MergeType;
+ return mergeRt(
+ ...(collectionType.types.map((rt) => trackKeysOfValidatedTypes(rt, prefix)) as [
+ t.Any,
+ t.Any
+ ])
);
}
@@ -142,9 +122,7 @@ function trackKeysOfValidatedTypes(
case 'ExactType': {
const exactType = type as t.ExactType;
- return t.exact(
- trackKeysOfValidatedTypes(exactType.type, prefix) as t.HasProps
- );
+ return t.exact(trackKeysOfValidatedTypes(exactType.type, prefix) as t.HasProps);
}
default:
@@ -169,17 +147,11 @@ class StrictKeysType<
(input, context) => {
this.trackedKeys.length = 0;
return either.chain(trackedType.validate(input, context), (i) => {
- const originalKeys = getKeysInObject(
- input as Record
- );
+ const originalKeys = getKeysInObject(input as Record);
const excessKeys = difference(originalKeys, this.trackedKeys);
if (excessKeys.length) {
- return t.failure(
- i,
- context,
- `Excess keys are not allowed: \n${excessKeys.join('\n')}`
- );
+ return t.failure(i, context, `Excess keys are not allowed: \n${excessKeys.join('\n')}`);
}
return t.success(i);
diff --git a/packages/kbn-io-ts-utils/tsconfig.json b/packages/kbn-io-ts-utils/tsconfig.json
new file mode 100644
index 00000000000000..6c67518e210734
--- /dev/null
+++ b/packages/kbn-io-ts-utils/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "incremental": false,
+ "outDir": "./target",
+ "stripInternal": false,
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "sourceRoot": "../../../../packages/kbn-io-ts-utils/src",
+ "types": [
+ "jest",
+ "node"
+ ]
+ },
+ "include": [
+ "./src/**/*.ts"
+ ]
+}
diff --git a/packages/kbn-server-route-repository/README.md b/packages/kbn-server-route-repository/README.md
new file mode 100644
index 00000000000000..e22205540ef317
--- /dev/null
+++ b/packages/kbn-server-route-repository/README.md
@@ -0,0 +1,7 @@
+# @kbn/server-route-repository
+
+Utility functions for creating a typed server route repository, and a typed client, generating runtime validation and type validation from the same route definition.
+
+## Usage
+
+TBD
diff --git a/packages/kbn-server-route-repository/jest.config.js b/packages/kbn-server-route-repository/jest.config.js
new file mode 100644
index 00000000000000..7449bb7cd3860b
--- /dev/null
+++ b/packages/kbn-server-route-repository/jest.config.js
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+module.exports = {
+ preset: '@kbn/test',
+ rootDir: '../..',
+ roots: ['/packages/kbn-server-route-repository'],
+};
diff --git a/packages/kbn-server-route-repository/package.json b/packages/kbn-server-route-repository/package.json
new file mode 100644
index 00000000000000..ce1ca02d0c4f6b
--- /dev/null
+++ b/packages/kbn-server-route-repository/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "@kbn/server-route-repository",
+ "main": "./target/index.js",
+ "types": "./target/index.d.ts",
+ "version": "1.0.0",
+ "license": "SSPL-1.0 OR Elastic License 2.0",
+ "private": true,
+ "scripts": {
+ "build": "../../node_modules/.bin/tsc",
+ "kbn:bootstrap": "yarn build",
+ "kbn:watch": "yarn build --watch"
+ },
+ "dependencies": {
+ "@kbn/io-ts-utils": "link:../kbn-io-ts-utils"
+ }
+}
diff --git a/packages/kbn-server-route-repository/src/create_server_route_factory.ts b/packages/kbn-server-route-repository/src/create_server_route_factory.ts
new file mode 100644
index 00000000000000..edf9bd657f995f
--- /dev/null
+++ b/packages/kbn-server-route-repository/src/create_server_route_factory.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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+import {
+ ServerRouteCreateOptions,
+ ServerRouteHandlerResources,
+ RouteParamsRT,
+ ServerRoute,
+} from './typings';
+
+export function createServerRouteFactory<
+ TRouteHandlerResources extends ServerRouteHandlerResources,
+ TRouteCreateOptions extends ServerRouteCreateOptions
+>(): <
+ TEndpoint extends string,
+ TReturnType,
+ TRouteParamsRT extends RouteParamsRT | undefined = undefined
+>(
+ route: ServerRoute<
+ TEndpoint,
+ TRouteParamsRT,
+ TRouteHandlerResources,
+ TReturnType,
+ TRouteCreateOptions
+ >
+) => ServerRoute<
+ TEndpoint,
+ TRouteParamsRT,
+ TRouteHandlerResources,
+ TReturnType,
+ TRouteCreateOptions
+> {
+ return (route) => route;
+}
diff --git a/packages/kbn-server-route-repository/src/create_server_route_repository.ts b/packages/kbn-server-route-repository/src/create_server_route_repository.ts
new file mode 100644
index 00000000000000..5ac89ebcac77f9
--- /dev/null
+++ b/packages/kbn-server-route-repository/src/create_server_route_repository.ts
@@ -0,0 +1,39 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+import {
+ ServerRouteHandlerResources,
+ ServerRouteRepository,
+ ServerRouteCreateOptions,
+} from './typings';
+
+export function createServerRouteRepository<
+ TRouteHandlerResources extends ServerRouteHandlerResources = never,
+ TRouteCreateOptions extends ServerRouteCreateOptions = never
+>(): ServerRouteRepository {
+ let routes: Record = {};
+
+ return {
+ add(route) {
+ routes = {
+ ...routes,
+ [route.endpoint]: route,
+ };
+
+ return this as any;
+ },
+ merge(repository) {
+ routes = {
+ ...routes,
+ ...Object.fromEntries(repository.getRoutes().map((route) => [route.endpoint, route])),
+ };
+
+ return this as any;
+ },
+ getRoutes: () => Object.values(routes),
+ };
+}
diff --git a/packages/kbn-server-route-repository/src/decode_request_params.test.ts b/packages/kbn-server-route-repository/src/decode_request_params.test.ts
new file mode 100644
index 00000000000000..08ef303ad0b3af
--- /dev/null
+++ b/packages/kbn-server-route-repository/src/decode_request_params.test.ts
@@ -0,0 +1,122 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+import { jsonRt } from '@kbn/io-ts-utils';
+import * as t from 'io-ts';
+import { decodeRequestParams } from './decode_request_params';
+
+describe('decodeRequestParams', () => {
+ it('decodes request params', () => {
+ const decode = () => {
+ return decodeRequestParams(
+ {
+ params: {
+ serviceName: 'opbeans-java',
+ },
+ body: null,
+ query: {
+ start: '',
+ },
+ },
+ t.type({
+ path: t.type({
+ serviceName: t.string,
+ }),
+ query: t.type({
+ start: t.string,
+ }),
+ })
+ );
+ };
+ expect(decode).not.toThrow();
+
+ expect(decode()).toEqual({
+ path: {
+ serviceName: 'opbeans-java',
+ },
+ query: {
+ start: '',
+ },
+ });
+ });
+
+ it('fails on excess keys', () => {
+ const decode = () => {
+ return decodeRequestParams(
+ {
+ params: {
+ serviceName: 'opbeans-java',
+ extraKey: '',
+ },
+ body: null,
+ query: {
+ start: '',
+ },
+ },
+ t.type({
+ path: t.type({
+ serviceName: t.string,
+ }),
+ query: t.type({
+ start: t.string,
+ }),
+ })
+ );
+ };
+
+ expect(decode).toThrowErrorMatchingInlineSnapshot(`
+ "Excess keys are not allowed:
+ path.extraKey"
+ `);
+ });
+
+ it('returns the decoded output', () => {
+ const decode = () => {
+ return decodeRequestParams(
+ {
+ params: {},
+ query: {
+ _inspect: 'true',
+ },
+ body: null,
+ },
+ t.type({
+ query: t.type({
+ _inspect: jsonRt.pipe(t.boolean),
+ }),
+ })
+ );
+ };
+
+ expect(decode).not.toThrow();
+
+ expect(decode()).toEqual({
+ query: {
+ _inspect: true,
+ },
+ });
+ });
+
+ it('strips empty params', () => {
+ const decode = () => {
+ return decodeRequestParams(
+ {
+ params: {},
+ query: {},
+ body: {},
+ },
+ t.type({
+ body: t.any,
+ })
+ );
+ };
+
+ expect(decode).not.toThrow();
+
+ expect(decode()).toEqual({});
+ });
+});
diff --git a/packages/kbn-server-route-repository/src/decode_request_params.ts b/packages/kbn-server-route-repository/src/decode_request_params.ts
new file mode 100644
index 00000000000000..00492d69b8ac5e
--- /dev/null
+++ b/packages/kbn-server-route-repository/src/decode_request_params.ts
@@ -0,0 +1,43 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+import * as t from 'io-ts';
+import { omitBy, isPlainObject, isEmpty } from 'lodash';
+import { isLeft } from 'fp-ts/lib/Either';
+import { PathReporter } from 'io-ts/lib/PathReporter';
+import Boom from '@hapi/boom';
+import { strictKeysRt } from '@kbn/io-ts-utils';
+import { RouteParamsRT } from './typings';
+
+interface KibanaRequestParams {
+ body: unknown;
+ query: unknown;
+ params: unknown;
+}
+
+export function decodeRequestParams(
+ params: KibanaRequestParams,
+ paramsRt: T
+): t.OutputOf {
+ const paramMap = omitBy(
+ {
+ path: params.params,
+ body: params.body,
+ query: params.query,
+ },
+ (val) => val === null || val === undefined || (isPlainObject(val) && isEmpty(val))
+ );
+
+ // decode = validate
+ const result = strictKeysRt(paramsRt).decode(paramMap);
+
+ if (isLeft(result)) {
+ throw Boom.badRequest(PathReporter.report(result)[0]);
+ }
+
+ return result.right;
+}
diff --git a/packages/kbn-server-route-repository/src/format_request.ts b/packages/kbn-server-route-repository/src/format_request.ts
new file mode 100644
index 00000000000000..49004a78ce0e03
--- /dev/null
+++ b/packages/kbn-server-route-repository/src/format_request.ts
@@ -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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { parseEndpoint } from './parse_endpoint';
+
+export function formatRequest(endpoint: string, pathParams: Record = {}) {
+ const { method, pathname: rawPathname } = parseEndpoint(endpoint);
+
+ // replace template variables with path params
+ const pathname = Object.keys(pathParams).reduce((acc, paramName) => {
+ return acc.replace(`{${paramName}}`, pathParams[paramName]);
+ }, rawPathname);
+
+ return { method, pathname };
+}
diff --git a/packages/kbn-server-route-repository/src/index.ts b/packages/kbn-server-route-repository/src/index.ts
new file mode 100644
index 00000000000000..23621c5b213bcf
--- /dev/null
+++ b/packages/kbn-server-route-repository/src/index.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export { createServerRouteRepository } from './create_server_route_repository';
+export { createServerRouteFactory } from './create_server_route_factory';
+export { formatRequest } from './format_request';
+export { parseEndpoint } from './parse_endpoint';
+export { decodeRequestParams } from './decode_request_params';
+export { routeValidationObject } from './route_validation_object';
+export {
+ RouteRepositoryClient,
+ ReturnOf,
+ EndpointOf,
+ ClientRequestParamsOf,
+ DecodedRequestParamsOf,
+ ServerRouteRepository,
+ ServerRoute,
+ RouteParamsRT,
+} from './typings';
diff --git a/packages/kbn-server-route-repository/src/parse_endpoint.ts b/packages/kbn-server-route-repository/src/parse_endpoint.ts
new file mode 100644
index 00000000000000..fd40489b0f4a5d
--- /dev/null
+++ b/packages/kbn-server-route-repository/src/parse_endpoint.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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+type Method = 'get' | 'post' | 'put' | 'delete';
+
+export function parseEndpoint(endpoint: string) {
+ const parts = endpoint.split(' ');
+
+ const method = parts[0].trim().toLowerCase() as Method;
+ const pathname = parts[1].trim();
+
+ if (!['get', 'post', 'put', 'delete'].includes(method)) {
+ throw new Error('Endpoint was not prefixed with a valid HTTP method');
+ }
+
+ return { method, pathname };
+}
diff --git a/packages/kbn-server-route-repository/src/route_validation_object.ts b/packages/kbn-server-route-repository/src/route_validation_object.ts
new file mode 100644
index 00000000000000..550be8d20d4465
--- /dev/null
+++ b/packages/kbn-server-route-repository/src/route_validation_object.ts
@@ -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
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+import { schema } from '@kbn/config-schema';
+
+const anyObject = schema.object({}, { unknowns: 'allow' });
+
+export const routeValidationObject = {
+ // `body` can be null, but `validate` expects non-nullable types
+ // if any validation is defined. Not having validation currently
+ // means we don't get the payload. See
+ // https://github.com/elastic/kibana/issues/50179
+ body: schema.nullable(anyObject),
+ params: anyObject,
+ query: anyObject,
+};
diff --git a/packages/kbn-server-route-repository/src/test_types.ts b/packages/kbn-server-route-repository/src/test_types.ts
new file mode 100644
index 00000000000000..c9015e19b82f8e
--- /dev/null
+++ b/packages/kbn-server-route-repository/src/test_types.ts
@@ -0,0 +1,238 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+import * as t from 'io-ts';
+import { createServerRouteRepository } from './create_server_route_repository';
+import { decodeRequestParams } from './decode_request_params';
+import { EndpointOf, ReturnOf, RouteRepositoryClient } from './typings';
+
+function assertType(value: TShape) {
+ return value;
+}
+
+// Generic arguments for createServerRouteRepository should be set,
+// if not, registering routes should not be allowed
+createServerRouteRepository().add({
+ // @ts-expect-error
+ endpoint: 'any_endpoint',
+ // @ts-expect-error
+ handler: async ({ params }) => {},
+});
+
+// If a params codec is not set, its type should not be available in
+// the request handler.
+createServerRouteRepository<{}, {}>().add({
+ endpoint: 'endpoint_without_params',
+ handler: async (resources) => {
+ // @ts-expect-error Argument of type '{}' is not assignable to parameter of type '{ params: any; }'.
+ assertType<{ params: any }>(resources);
+ },
+});
+
+// If a params codec is set, its type _should_ be available in the
+// request handler.
+createServerRouteRepository<{}, {}>().add({
+ endpoint: 'endpoint_with_params',
+ params: t.type({
+ path: t.type({
+ serviceName: t.string,
+ }),
+ }),
+ handler: async (resources) => {
+ assertType<{ params: { path: { serviceName: string } } }>(resources);
+ },
+});
+
+// Resources should be passed to the request handler.
+createServerRouteRepository<{ context: { getSpaceId: () => string } }, {}>().add({
+ endpoint: 'endpoint_with_params',
+ params: t.type({
+ path: t.type({
+ serviceName: t.string,
+ }),
+ }),
+ handler: async ({ context }) => {
+ const spaceId = context.getSpaceId();
+ assertType(spaceId);
+ },
+});
+
+// Create options are available when registering a route.
+createServerRouteRepository<{}, { options: { tags: string[] } }>().add({
+ endpoint: 'endpoint_with_params',
+ params: t.type({
+ path: t.type({
+ serviceName: t.string,
+ }),
+ }),
+ options: {
+ tags: [],
+ },
+ handler: async (resources) => {
+ assertType<{ params: { path: { serviceName: string } } }>(resources);
+ },
+});
+
+const repository = createServerRouteRepository<{}, {}>()
+ .add({
+ endpoint: 'endpoint_without_params',
+ handler: async () => {
+ return {
+ noParamsForMe: true,
+ };
+ },
+ })
+ .add({
+ endpoint: 'endpoint_with_params',
+ params: t.type({
+ path: t.type({
+ serviceName: t.string,
+ }),
+ }),
+ handler: async () => {
+ return {
+ yesParamsForMe: true,
+ };
+ },
+ })
+ .add({
+ endpoint: 'endpoint_with_optional_params',
+ params: t.partial({
+ query: t.partial({
+ serviceName: t.string,
+ }),
+ }),
+ handler: async () => {
+ return {
+ someParamsForMe: true,
+ };
+ },
+ });
+
+type TestRepository = typeof repository;
+
+// EndpointOf should return all valid endpoints of a repository
+
+assertType>>([
+ 'endpoint_with_params',
+ 'endpoint_without_params',
+ 'endpoint_with_optional_params',
+]);
+
+// @ts-expect-error Type '"this_endpoint_does_not_exist"' is not assignable to type '"endpoint_without_params" | "endpoint_with_params" | "endpoint_with_optional_params"'
+assertType>>(['this_endpoint_does_not_exist']);
+
+// ReturnOf should return the return type of a request handler.
+
+assertType>({
+ noParamsForMe: true,
+});
+
+const noParamsInvalid: ReturnOf = {
+ // @ts-expect-error type '{ paramsForMe: boolean; }' is not assignable to type '{ noParamsForMe: boolean; }'.
+ paramsForMe: true,
+};
+
+// RouteRepositoryClient
+
+type TestClient = RouteRepositoryClient;
+
+const client: TestClient = {} as any;
+
+// It should respect any additional create options.
+
+// @ts-expect-error Property 'timeout' is missing
+client({
+ endpoint: 'endpoint_without_params',
+});
+
+client({
+ endpoint: 'endpoint_without_params',
+ timeout: 1,
+});
+
+// It does not allow params for routes without a params codec
+client({
+ endpoint: 'endpoint_without_params',
+ // @ts-expect-error Object literal may only specify known properties, and 'params' does not exist in type
+ params: {},
+ timeout: 1,
+});
+
+// It requires params for routes with a params codec
+client({
+ endpoint: 'endpoint_with_params',
+ params: {
+ // @ts-expect-error property 'serviceName' is missing in type '{}'
+ path: {},
+ },
+ timeout: 1,
+});
+
+// Params are optional if the codec has no required keys
+client({
+ endpoint: 'endpoint_with_optional_params',
+ timeout: 1,
+});
+
+// If optional, an error will still occur if the params do not match
+client({
+ endpoint: 'endpoint_with_optional_params',
+ timeout: 1,
+ params: {
+ // @ts-expect-error Object literal may only specify known properties, and 'path' does not exist in type
+ path: '',
+ },
+});
+
+// The return type is correctly inferred
+client({
+ endpoint: 'endpoint_with_params',
+ params: {
+ path: {
+ serviceName: '',
+ },
+ },
+ timeout: 1,
+}).then((res) => {
+ assertType<{
+ noParamsForMe: boolean;
+ // @ts-expect-error Property 'noParamsForMe' is missing in type
+ }>(res);
+
+ assertType<{
+ yesParamsForMe: boolean;
+ }>(res);
+});
+
+// decodeRequestParams should return the type of the codec that is passed
+assertType<{ path: { serviceName: string } }>(
+ decodeRequestParams(
+ {
+ params: {
+ serviceName: 'serviceName',
+ },
+ body: undefined,
+ query: undefined,
+ },
+ t.type({ path: t.type({ serviceName: t.string }) })
+ )
+);
+
+assertType<{ path: { serviceName: boolean } }>(
+ // @ts-expect-error The types of 'path.serviceName' are incompatible between these types.
+ decodeRequestParams(
+ {
+ params: {
+ serviceName: 'serviceName',
+ },
+ body: undefined,
+ query: undefined,
+ },
+ t.type({ path: t.type({ serviceName: t.string }) })
+ )
+);
diff --git a/packages/kbn-server-route-repository/src/typings.ts b/packages/kbn-server-route-repository/src/typings.ts
new file mode 100644
index 00000000000000..c27f67c71e88bc
--- /dev/null
+++ b/packages/kbn-server-route-repository/src/typings.ts
@@ -0,0 +1,192 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import * as t from 'io-ts';
+import { RequiredKeys } from 'utility-types';
+
+type MaybeOptional }> = RequiredKeys<
+ T['params']
+> extends never
+ ? { params?: T['params'] }
+ : { params: T['params'] };
+
+type WithoutIncompatibleMethods = Omit & {
+ encode: t.Encode;
+ asEncoder: () => t.Encoder;
+};
+
+export type RouteParamsRT = WithoutIncompatibleMethods<
+ t.Type<{
+ path?: any;
+ query?: any;
+ body?: any;
+ }>
+>;
+
+export interface RouteState {
+ [endpoint: string]: ServerRoute;
+}
+
+export type ServerRouteHandlerResources = Record;
+export type ServerRouteCreateOptions = Record;
+
+export type ServerRoute<
+ TEndpoint extends string,
+ TRouteParamsRT extends RouteParamsRT | undefined,
+ TRouteHandlerResources extends ServerRouteHandlerResources,
+ TReturnType,
+ TRouteCreateOptions extends ServerRouteCreateOptions
+> = {
+ endpoint: TEndpoint;
+ params?: TRouteParamsRT;
+ handler: ({}: TRouteHandlerResources &
+ (TRouteParamsRT extends RouteParamsRT
+ ? DecodedRequestParamsOfType
+ : {})) => Promise;
+} & TRouteCreateOptions;
+
+export interface ServerRouteRepository<
+ TRouteHandlerResources extends ServerRouteHandlerResources = ServerRouteHandlerResources,
+ TRouteCreateOptions extends ServerRouteCreateOptions = ServerRouteCreateOptions,
+ TRouteState extends RouteState = RouteState
+> {
+ add<
+ TEndpoint extends string,
+ TReturnType,
+ TRouteParamsRT extends RouteParamsRT | undefined = undefined
+ >(
+ route: ServerRoute<
+ TEndpoint,
+ TRouteParamsRT,
+ TRouteHandlerResources,
+ TReturnType,
+ TRouteCreateOptions
+ >
+ ): ServerRouteRepository<
+ TRouteHandlerResources,
+ TRouteCreateOptions,
+ TRouteState &
+ {
+ [key in TEndpoint]: ServerRoute<
+ TEndpoint,
+ TRouteParamsRT,
+ TRouteHandlerResources,
+ TReturnType,
+ TRouteCreateOptions
+ >;
+ }
+ >;
+ merge<
+ TServerRouteRepository extends ServerRouteRepository<
+ TRouteHandlerResources,
+ TRouteCreateOptions
+ >
+ >(
+ repository: TServerRouteRepository
+ ): TServerRouteRepository extends ServerRouteRepository<
+ TRouteHandlerResources,
+ TRouteCreateOptions,
+ infer TRouteStateToMerge
+ >
+ ? ServerRouteRepository<
+ TRouteHandlerResources,
+ TRouteCreateOptions,
+ TRouteState & TRouteStateToMerge
+ >
+ : never;
+ getRoutes: () => Array<
+ ServerRoute
+ >;
+}
+
+type ClientRequestParamsOfType<
+ TRouteParamsRT extends RouteParamsRT
+> = TRouteParamsRT extends t.Mixed
+ ? MaybeOptional<{
+ params: t.OutputOf;
+ }>
+ : {};
+
+type DecodedRequestParamsOfType<
+ TRouteParamsRT extends RouteParamsRT
+> = TRouteParamsRT extends t.Mixed
+ ? MaybeOptional<{
+ params: t.TypeOf;
+ }>
+ : {};
+
+export type EndpointOf<
+ TServerRouteRepository extends ServerRouteRepository
+> = TServerRouteRepository extends ServerRouteRepository
+ ? keyof TRouteState
+ : never;
+
+export type ReturnOf<
+ TServerRouteRepository extends ServerRouteRepository,
+ TEndpoint extends EndpointOf
+> = TServerRouteRepository extends ServerRouteRepository
+ ? TEndpoint extends keyof TRouteState
+ ? TRouteState[TEndpoint] extends ServerRoute<
+ any,
+ any,
+ any,
+ infer TReturnType,
+ ServerRouteCreateOptions
+ >
+ ? TReturnType
+ : never
+ : never
+ : never;
+
+export type DecodedRequestParamsOf<
+ TServerRouteRepository extends ServerRouteRepository,
+ TEndpoint extends EndpointOf
+> = TServerRouteRepository extends ServerRouteRepository
+ ? TEndpoint extends keyof TRouteState
+ ? TRouteState[TEndpoint] extends ServerRoute<
+ any,
+ infer TRouteParamsRT,
+ any,
+ any,
+ ServerRouteCreateOptions
+ >
+ ? TRouteParamsRT extends RouteParamsRT
+ ? DecodedRequestParamsOfType
+ : {}
+ : never
+ : never
+ : never;
+
+export type ClientRequestParamsOf<
+ TServerRouteRepository extends ServerRouteRepository,
+ TEndpoint extends EndpointOf
+> = TServerRouteRepository extends ServerRouteRepository
+ ? TEndpoint extends keyof TRouteState
+ ? TRouteState[TEndpoint] extends ServerRoute<
+ any,
+ infer TRouteParamsRT,
+ any,
+ any,
+ ServerRouteCreateOptions
+ >
+ ? TRouteParamsRT extends RouteParamsRT
+ ? ClientRequestParamsOfType
+ : {}
+ : never
+ : never
+ : never;
+
+export type RouteRepositoryClient<
+ TServerRouteRepository extends ServerRouteRepository,
+ TAdditionalClientOptions extends Record
+> = >(
+ options: {
+ endpoint: TEndpoint;
+ } & ClientRequestParamsOf &
+ TAdditionalClientOptions
+) => Promise>;
diff --git a/packages/kbn-server-route-repository/tsconfig.json b/packages/kbn-server-route-repository/tsconfig.json
new file mode 100644
index 00000000000000..8f1e72172c6759
--- /dev/null
+++ b/packages/kbn-server-route-repository/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "incremental": false,
+ "outDir": "./target",
+ "stripInternal": false,
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "sourceRoot": "../../../../packages/kbn-server-route-repository/src",
+ "types": [
+ "jest",
+ "node"
+ ],
+ "noUnusedLocals": false
+ },
+ "include": [
+ "./src/**/*.ts"
+ ]
+}
diff --git a/packages/kbn-telemetry-tools/src/tools/tasks/index.ts b/packages/kbn-telemetry-tools/src/tools/tasks/index.ts
index 5d946b73d9759f..f55a9aa80d40d6 100644
--- a/packages/kbn-telemetry-tools/src/tools/tasks/index.ts
+++ b/packages/kbn-telemetry-tools/src/tools/tasks/index.ts
@@ -7,7 +7,9 @@
*/
export { ErrorReporter } from './error_reporter';
-export { TaskContext, createTaskContext } from './task_context';
+
+export type { TaskContext } from './task_context';
+export { createTaskContext } from './task_context';
export { parseConfigsTask } from './parse_configs_task';
export { extractCollectorsTask } from './extract_collectors_task';
diff --git a/packages/kbn-telemetry-tools/tsconfig.json b/packages/kbn-telemetry-tools/tsconfig.json
index 39946fe9907e55..419af1d02f83b5 100644
--- a/packages/kbn-telemetry-tools/tsconfig.json
+++ b/packages/kbn-telemetry-tools/tsconfig.json
@@ -6,7 +6,8 @@
"declaration": true,
"declarationMap": true,
"sourceMap": true,
- "sourceRoot": "../../../../packages/kbn-telemetry-tools/src"
+ "sourceRoot": "../../../../packages/kbn-telemetry-tools/src",
+ "isolatedModules": true
},
"include": [
"src/**/*",
diff --git a/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts b/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts
index 8ef11e2dba462d..63eca93def64df 100644
--- a/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts
+++ b/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts
@@ -11,6 +11,7 @@ import Path from 'path';
import { REPO_ROOT } from '@kbn/utils';
import { run, createFailError, createFlagError } from '@kbn/dev-utils';
import globby from 'globby';
+import normalize from 'normalize-path';
import { getFailures, TestFailure } from './get_failures';
import { GithubApi, GithubIssueMini } from './github_api';
@@ -61,7 +62,9 @@ export function runFailedTestsReporterCli() {
throw createFlagError('Missing --build-url or process.env.BUILD_URL');
}
- const patterns = flags._.length ? flags._ : DEFAULT_PATTERNS;
+ const patterns = (flags._.length ? flags._ : DEFAULT_PATTERNS).map((p) =>
+ normalize(Path.resolve(p))
+ );
log.info('Searching for reports at', patterns);
const reportPaths = await globby(patterns, {
absolute: true,
diff --git a/packages/kbn-ui-shared-deps/polyfills.js b/packages/kbn-ui-shared-deps/polyfills.js
index abbf911cfc8fc5..a9ec32023f2bfd 100644
--- a/packages/kbn-ui-shared-deps/polyfills.js
+++ b/packages/kbn-ui-shared-deps/polyfills.js
@@ -8,7 +8,6 @@
require('core-js/stable');
require('regenerator-runtime/runtime');
-require('custom-event-polyfill');
if (typeof window.Event === 'object') {
// IE11 doesn't support unknown event types, required by react-use
@@ -17,6 +16,4 @@ if (typeof window.Event === 'object') {
}
require('whatwg-fetch');
-require('abortcontroller-polyfill/dist/polyfill-patch-fetch');
-require('./vendor/childnode_remove_polyfill');
require('symbol-observable');
diff --git a/packages/kbn-ui-shared-deps/vendor/childnode_remove_polyfill.js b/packages/kbn-ui-shared-deps/vendor/childnode_remove_polyfill.js
deleted file mode 100644
index d8818fe809ccbb..00000000000000
--- a/packages/kbn-ui-shared-deps/vendor/childnode_remove_polyfill.js
+++ /dev/null
@@ -1,48 +0,0 @@
-/* eslint-disable @kbn/eslint/require-license-header */
-
-/* @notice
- * This product bundles childnode-remove which is available under a
- * "MIT" license.
- *
- * The MIT License (MIT)
- *
- * Copyright (c) 2016-present, jszhou
- * https://github.com/jserz/js_piece
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
-
-/* eslint-disable */
-
-(function (arr) {
- arr.forEach(function (item) {
- if (item.hasOwnProperty('remove')) {
- return;
- }
- Object.defineProperty(item, 'remove', {
- configurable: true,
- enumerable: true,
- writable: true,
- value: function remove() {
- if (this.parentNode !== null)
- this.parentNode.removeChild(this);
- }
- });
- });
-})([Element.prototype, CharacterData.prototype, DocumentType.prototype]);
diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker
index 9617a556e2cdd6..6cc94208fbcce7 100755
--- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker
+++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker
@@ -163,7 +163,11 @@ kibana_vars=(
xpack.actions.proxyHeaders
xpack.actions.proxyRejectUnauthorizedCertificates
xpack.actions.proxyUrl
+ xpack.actions.proxyBypassHosts
+ xpack.actions.proxyOnlyHosts
xpack.actions.rejectUnauthorized
+ xpack.actions.maxResponseContentLength
+ xpack.actions.responseTimeout
xpack.alerts.healthCheck.interval
xpack.alerts.invalidateApiKeysTask.interval
xpack.alerts.invalidateApiKeysTask.removalDelay
diff --git a/src/dev/build/tasks/package_json/find_used_dependencies.ts b/src/dev/build/tasks/package_json/find_used_dependencies.ts
index 3a296ec76f3e63..004e17b87ac8ba 100644
--- a/src/dev/build/tasks/package_json/find_used_dependencies.ts
+++ b/src/dev/build/tasks/package_json/find_used_dependencies.ts
@@ -6,7 +6,9 @@
* Side Public License, v 1.
*/
+import Path from 'path';
import globby from 'globby';
+import normalize from 'normalize-path';
// @ts-ignore
import { parseEntries, dependenciesParseStrategy } from '@kbn/babel-code-parser';
@@ -21,16 +23,16 @@ export async function findUsedDependencies(listedPkgDependencies: any, baseDir:
// Define the entry points for the server code in order to
// start here later looking for the server side dependencies
const mainCodeEntries = [
- `${baseDir}/src/cli/dist.js`,
- `${baseDir}/src/cli_keystore/dist.js`,
- `${baseDir}/src/cli_plugin/dist.js`,
+ Path.resolve(baseDir, `src/cli/dist.js`),
+ Path.resolve(baseDir, `src/cli_keystore/dist.js`),
+ Path.resolve(baseDir, `src/cli_plugin/dist.js`),
];
const discoveredPluginEntries = await globby([
- `${baseDir}/src/plugins/*/server/index.js`,
- `!${baseDir}/src/plugins/**/public`,
- `${baseDir}/x-pack/plugins/*/server/index.js`,
- `!${baseDir}/x-pack/plugins/**/public`,
+ normalize(Path.resolve(baseDir, `src/plugins/*/server/index.js`)),
+ `!${normalize(Path.resolve(baseDir, `/src/plugins/**/public`))}`,
+ normalize(Path.resolve(baseDir, `x-pack/plugins/*/server/index.js`)),
+ `!${normalize(Path.resolve(baseDir, `/x-pack/plugins/**/public`))}`,
]);
// It will include entries that cannot be discovered
@@ -40,7 +42,7 @@ export async function findUsedDependencies(listedPkgDependencies: any, baseDir:
// Another way would be to include an index file and import all the functions
// using named imports
const dynamicRequiredEntries = await globby([
- `${baseDir}/src/plugins/vis_type_timelion/server/**/*.js`,
+ normalize(Path.resolve(baseDir, 'src/plugins/vis_type_timelion/server/**/*.js')),
]);
// Compose all the needed entries
diff --git a/src/dev/typescript/ref_output_cache/integration_tests/ref_output_cache.test.ts b/src/dev/typescript/ref_output_cache/integration_tests/ref_output_cache.test.ts
index 2bc75785ee6a73..7347529239176e 100644
--- a/src/dev/typescript/ref_output_cache/integration_tests/ref_output_cache.test.ts
+++ b/src/dev/typescript/ref_output_cache/integration_tests/ref_output_cache.test.ts
@@ -12,6 +12,7 @@ import Fs from 'fs';
import del from 'del';
import cpy from 'cpy';
import globby from 'globby';
+import normalize from 'normalize-path';
import {
ToolingLog,
createAbsolutePathSerializer,
@@ -98,7 +99,10 @@ it('creates and extracts caches, ingoring dirs with matching merge-base file and
const files = Object.fromEntries(
globby
- .sync(outDirs, { dot: true })
+ .sync(
+ outDirs.map((p) => normalize(p)),
+ { dot: true }
+ )
.map((path) => [Path.relative(TMP, path), Fs.readFileSync(path, 'utf-8')])
);
diff --git a/src/plugins/dashboard/public/application/dashboard_app.tsx b/src/plugins/dashboard/public/application/dashboard_app.tsx
index 3d6f08f3219779..e7e2ccfd46b9cb 100644
--- a/src/plugins/dashboard/public/application/dashboard_app.tsx
+++ b/src/plugins/dashboard/public/application/dashboard_app.tsx
@@ -10,7 +10,7 @@ import { History } from 'history';
import { merge, Subject, Subscription } from 'rxjs';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
-import { debounceTime, tap } from 'rxjs/operators';
+import { debounceTime, finalize, switchMap, tap } from 'rxjs/operators';
import { useKibana } from '../../../kibana_react/public';
import { DashboardConstants } from '../dashboard_constants';
import { DashboardTopNav } from './top_nav/dashboard_top_nav';
@@ -30,7 +30,7 @@ import {
useSavedDashboard,
} from './hooks';
-import { IndexPattern } from '../services/data';
+import { IndexPattern, waitUntilNextSessionCompletes$ } from '../services/data';
import { EmbeddableRenderer } from '../services/embeddable';
import { DashboardContainerInput } from '.';
import { leaveConfirmStrings } from '../dashboard_strings';
@@ -209,14 +209,26 @@ export function DashboardApp({
);
subscriptions.add(
- merge(
- data.query.timefilter.timefilter.getAutoRefreshFetch$(),
- searchSessionIdQuery$
- ).subscribe(() => {
+ searchSessionIdQuery$.subscribe(() => {
triggerRefresh$.next({ force: true });
})
);
+ subscriptions.add(
+ data.query.timefilter.timefilter
+ .getAutoRefreshFetch$()
+ .pipe(
+ tap(() => {
+ triggerRefresh$.next({ force: true });
+ }),
+ switchMap((done) =>
+ // best way on a dashboard to estimate that panels are updated is to rely on search session service state
+ waitUntilNextSessionCompletes$(data.search.session).pipe(finalize(done))
+ )
+ )
+ .subscribe()
+ );
+
dashboardStateManager.registerChangeListener(() => {
setUnsavedChanges(dashboardStateManager.getIsDirty(data.query.timefilter.timefilter));
// we aren't checking dirty state because there are changes the container needs to know about
diff --git a/src/plugins/data/README.mdx b/src/plugins/data/README.mdx
index 60e74a3fa126cc..30006e2b497bda 100644
--- a/src/plugins/data/README.mdx
+++ b/src/plugins/data/README.mdx
@@ -5,7 +5,7 @@ title: Data services
image: https://source.unsplash.com/400x175/?Search
summary: The data plugin contains services for searching, querying and filtering.
date: 2020-12-02
-tags: ['kibana','dev', 'contributor', 'api docs']
+tags: ['kibana', 'dev', 'contributor', 'api docs']
---
# data
@@ -149,7 +149,6 @@ Index patterns provide Rest-like HTTP CRUD+ API with the following endpoints:
- Remove a scripted field — `DELETE /api/index_patterns/index_pattern/{id}/scripted_field/{name}`
- Update a scripted field — `POST /api/index_patterns/index_pattern/{id}/scripted_field/{name}`
-
### Index Patterns API
Index Patterns REST API allows you to create, retrieve and delete index patterns. I also
@@ -212,11 +211,10 @@ The endpoint returns the created index pattern object.
```json
{
- "index_pattern": {}
+ "index_pattern": {}
}
```
-
#### Fetch an index pattern by ID
Retrieve an index pattern by its ID.
@@ -229,23 +227,22 @@ Returns an index pattern object.
```json
{
- "index_pattern": {
- "id": "...",
- "version": "...",
- "title": "...",
- "type": "...",
- "intervalName": "...",
- "timeFieldName": "...",
- "sourceFilters": [],
- "fields": {},
- "typeMeta": {},
- "fieldFormats": {},
- "fieldAttrs": {}
- }
+ "index_pattern": {
+ "id": "...",
+ "version": "...",
+ "title": "...",
+ "type": "...",
+ "intervalName": "...",
+ "timeFieldName": "...",
+ "sourceFilters": [],
+ "fields": {},
+ "typeMeta": {},
+ "fieldFormats": {},
+ "fieldAttrs": {}
+ }
}
```
-
#### Delete an index pattern by ID
Delete and index pattern by its ID.
@@ -256,21 +253,21 @@ DELETE /api/index_patterns/index_pattern/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Returns an '200 OK` response with empty body on success.
-
#### Partially update an index pattern by ID
Update part of an index pattern. Only provided fields will be updated on the
index pattern, missing fields will stay as they are persisted.
These fields can be update partially:
- - `title`
- - `timeFieldName`
- - `intervalName`
- - `fields` (optionally refresh fields)
- - `sourceFilters`
- - `fieldFormatMap`
- - `type`
- - `typeMeta`
+
+- `title`
+- `timeFieldName`
+- `intervalName`
+- `fields` (optionally refresh fields)
+- `sourceFilters`
+- `fieldFormatMap`
+- `type`
+- `typeMeta`
Update a title of an index pattern.
@@ -318,18 +315,14 @@ This endpoint returns the updated index pattern object.
```json
{
- "index_pattern": {
-
- }
+ "index_pattern": {}
}
```
-
### Fields API
Fields API allows to change field metadata, such as `count`, `customLabel`, and `format`.
-
#### Update fields
Update endpoint allows you to update fields presentation metadata, such as `count`,
@@ -383,13 +376,10 @@ This endpoint returns the updated index pattern object.
```json
{
- "index_pattern": {
-
- }
+ "index_pattern": {}
}
```
-
### Scripted Fields API
Scripted Fields API provides CRUD API for scripted fields of an index pattern.
@@ -487,7 +477,7 @@ Returns the field object.
```json
{
- "field": {}
+ "field": {}
}
```
@@ -529,47 +519,86 @@ POST /api/index_patterns/index_pattern/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/scri
}
```
-
## Query
The query service is responsible for managing the configuration of a search query (`QueryState`): filters, time range, query string, and settings such as the auto refresh behavior and saved queries.
It contains sub-services for each of those configurations:
- - `data.query.filterManager` - Manages the `filters` component of a `QueryState`. The global filter state (filters that are persisted between applications) are owned by this service.
- - `data.query.timefilter` - Responsible for the time range filter and the auto refresh behavior settings.
- - `data.query.queryString` - Responsible for the query string and query language settings.
- - `data.query.savedQueries` - Responsible for persisting a `QueryState` into a `SavedObject`, so it can be restored and used by other applications.
- Any changes to the `QueryState` are published on the `data.query.state$`, which is useful when wanting to persist global state or run a search upon data changes.
+- `data.query.filterManager` - Manages the `filters` component of a `QueryState`. The global filter state (filters that are persisted between applications) are owned by this service.
+- `data.query.timefilter` - Responsible for the time range filter and the auto refresh behavior settings.
+- `data.query.queryString` - Responsible for the query string and query language settings.
+- `data.query.savedQueries` - Responsible for persisting a `QueryState` into a `SavedObject`, so it can be restored and used by other applications.
- A simple use case is:
+Any changes to the `QueryState` are published on the `data.query.state$`, which is useful when wanting to persist global state or run a search upon data changes.
- ```.ts
- function searchOnChange(indexPattern: IndexPattern, aggConfigs: AggConfigs) {
- data.query.state$.subscribe(() => {
+A simple use case is:
- // Constuct the query portion of the search request
- const query = data.query.getEsQuery(indexPattern);
+```.ts
+function searchOnChange(indexPattern: IndexPattern, aggConfigs: AggConfigs) {
+ data.query.state$.subscribe(() => {
+
+ // Constuct the query portion of the search request
+ const query = data.query.getEsQuery(indexPattern);
+
+ // Construct a request
+ const request = {
+ params: {
+ index: indexPattern.title,
+ body: {
+ aggs: aggConfigs.toDsl(),
+ query,
+ },
+ },
+ };
+
+ // Search with the `data.query` config
+ const search$ = data.search.search(request);
+
+ ...
+ });
+}
- // Construct a request
- const request = {
- params: {
- index: indexPattern.title,
- body: {
- aggs: aggConfigs.toDsl(),
- query,
- },
- },
- };
+```
- // Search with the `data.query` config
- const search$ = data.search.search(request);
+### Timefilter
- ...
- });
- }
+`data.query.timefilter` is responsible for the time range filter and the auto refresh behavior settings.
+
+#### Autorefresh
- ```
+Timefilter provides an API for setting and getting current auto refresh state:
+
+```ts
+const { pause, value } = data.query.timefilter.timefilter.getRefreshInterval();
+
+data.query.timefilter.timefilter.setRefreshInterval({ pause: false, value: 5000 }); // start auto refresh with 5 seconds interval
+```
+
+Timefilter API also provides an `autoRefreshFetch$` observables that apps should use to get notified
+when it is time to refresh data because of auto refresh.
+This API expects apps to confirm when they are done with reloading the data.
+The confirmation mechanism is needed to prevent excessive queue of fetches.
+
+```
+import { refetchData } from '../my-app'
+
+const autoRefreshFetch$ = data.query.timefilter.timefilter.getAutoRefreshFetch$()
+autoRefreshFetch$.subscribe((done) => {
+ try {
+ await refetchData();
+ } finally {
+ // confirm that data fetching was finished
+ done();
+ }
+})
+
+function unmount() {
+ // don't forget to unsubscribe when leaving the app
+ autoRefreshFetch$.unsubscribe()
+}
+
+```
## Search
diff --git a/src/plugins/data/common/kbn_field_types/types.ts b/src/plugins/data/common/kbn_field_types/types.ts
index c46e5c5266f559..e6f815e058ce36 100644
--- a/src/plugins/data/common/kbn_field_types/types.ts
+++ b/src/plugins/data/common/kbn_field_types/types.ts
@@ -80,4 +80,5 @@ export enum KBN_FIELD_TYPES {
OBJECT = 'object',
NESTED = 'nested',
HISTOGRAM = 'histogram',
+ MISSING = 'missing',
}
diff --git a/src/plugins/data/common/search/aggs/agg_configs.test.ts b/src/plugins/data/common/search/aggs/agg_configs.test.ts
index 297af560081b11..3ce528e6ed8932 100644
--- a/src/plugins/data/common/search/aggs/agg_configs.test.ts
+++ b/src/plugins/data/common/search/aggs/agg_configs.test.ts
@@ -230,7 +230,7 @@ describe('AggConfigs', () => {
describe('#toDsl', () => {
beforeEach(() => {
indexPattern = stubIndexPattern as IndexPattern;
- indexPattern.fields.getByName = (name) => (name as unknown) as IndexPatternField;
+ indexPattern.fields.getByName = (name) => (({ name } as unknown) as IndexPatternField);
});
it('uses the sorted aggs', () => {
diff --git a/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts b/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts
index 4e278d5872a3ec..56e720d237c455 100644
--- a/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts
+++ b/src/plugins/data/common/search/aggs/buckets/_terms_other_bucket_helper.test.ts
@@ -16,16 +16,33 @@ import { AggConfigs, CreateAggConfigParams } from '../agg_configs';
import { BUCKET_TYPES } from './bucket_agg_types';
import { IBucketAggConfig } from './bucket_agg_type';
import { mockAggTypesRegistry } from '../test_helpers';
+import type { IndexPatternField } from '../../../index_patterns';
+import { IndexPattern } from '../../../index_patterns/index_patterns/index_pattern';
const indexPattern = {
id: '1234',
title: 'logstash-*',
fields: [
{
- name: 'field',
+ name: 'machine.os.raw',
+ type: 'string',
+ esTypes: ['string'],
+ aggregatable: true,
+ filterable: true,
+ searchable: true,
+ },
+ {
+ name: 'geo.src',
+ type: 'string',
+ esTypes: ['string'],
+ aggregatable: true,
+ filterable: true,
+ searchable: true,
},
],
-} as any;
+} as IndexPattern;
+
+indexPattern.fields.getByName = (name) => (({ name } as unknown) as IndexPatternField);
const singleTerm = {
aggs: [
diff --git a/src/plugins/data/common/search/aggs/buckets/terms.test.ts b/src/plugins/data/common/search/aggs/buckets/terms.test.ts
index bb34d7ede453cc..09dfbb28a4e531 100644
--- a/src/plugins/data/common/search/aggs/buckets/terms.test.ts
+++ b/src/plugins/data/common/search/aggs/buckets/terms.test.ts
@@ -10,6 +10,8 @@ import { AggConfigs } from '../agg_configs';
import { METRIC_TYPES } from '../metrics';
import { mockAggTypesRegistry } from '../test_helpers';
import { BUCKET_TYPES } from './bucket_agg_types';
+import type { IndexPatternField } from '../../../index_patterns';
+import { IndexPattern } from '../../../index_patterns/index_patterns/index_pattern';
describe('Terms Agg', () => {
describe('order agg editor UI', () => {
@@ -17,16 +19,44 @@ describe('Terms Agg', () => {
const indexPattern = {
id: '1234',
title: 'logstash-*',
- fields: {
- getByName: () => field,
- filter: () => [field],
- },
- } as any;
+ fields: [
+ {
+ name: 'field',
+ type: 'string',
+ esTypes: ['string'],
+ aggregatable: true,
+ filterable: true,
+ searchable: true,
+ },
+ {
+ name: 'string_field',
+ type: 'string',
+ esTypes: ['string'],
+ aggregatable: true,
+ filterable: true,
+ searchable: true,
+ },
+ {
+ name: 'empty_number_field',
+ type: 'number',
+ esTypes: ['number'],
+ aggregatable: true,
+ filterable: true,
+ searchable: true,
+ },
+ {
+ name: 'number_field',
+ type: 'number',
+ esTypes: ['number'],
+ aggregatable: true,
+ filterable: true,
+ searchable: true,
+ },
+ ],
+ } as IndexPattern;
- const field = {
- name: 'field',
- indexPattern,
- };
+ indexPattern.fields.getByName = (name) => (({ name } as unknown) as IndexPatternField);
+ indexPattern.fields.filter = () => indexPattern.fields;
return new AggConfigs(
indexPattern,
@@ -207,16 +237,28 @@ describe('Terms Agg', () => {
const indexPattern = {
id: '1234',
title: 'logstash-*',
- fields: {
- getByName: () => field,
- filter: () => [field],
- },
- } as any;
+ fields: [
+ {
+ name: 'string_field',
+ type: 'string',
+ esTypes: ['string'],
+ aggregatable: true,
+ filterable: true,
+ searchable: true,
+ },
+ {
+ name: 'number_field',
+ type: 'number',
+ esTypes: ['number'],
+ aggregatable: true,
+ filterable: true,
+ searchable: true,
+ },
+ ],
+ } as IndexPattern;
- const field = {
- name: 'field',
- indexPattern,
- };
+ indexPattern.fields.getByName = (name) => (({ name } as unknown) as IndexPatternField);
+ indexPattern.fields.filter = () => indexPattern.fields;
const aggConfigs = new AggConfigs(
indexPattern,
diff --git a/src/plugins/data/common/search/aggs/param_types/field.ts b/src/plugins/data/common/search/aggs/param_types/field.ts
index 2d3ff8f5fdba8b..62dac9831211a9 100644
--- a/src/plugins/data/common/search/aggs/param_types/field.ts
+++ b/src/plugins/data/common/search/aggs/param_types/field.ts
@@ -8,7 +8,10 @@
import { i18n } from '@kbn/i18n';
import { IAggConfig } from '../agg_config';
-import { SavedObjectNotFound } from '../../../../../../plugins/kibana_utils/common';
+import {
+ SavedFieldNotFound,
+ SavedFieldTypeInvalidForAgg,
+} from '../../../../../../plugins/kibana_utils/common';
import { BaseParamType } from './base';
import { propFilter } from '../utils';
import { KBN_FIELD_TYPES } from '../../../kbn_field_types/types';
@@ -47,13 +50,49 @@ export class FieldParamType extends BaseParamType {
);
}
- if (field.scripted) {
+ if (field.type === KBN_FIELD_TYPES.MISSING) {
+ throw new SavedFieldNotFound(
+ i18n.translate(
+ 'data.search.aggs.paramTypes.field.notFoundSavedFieldParameterErrorMessage',
+ {
+ defaultMessage:
+ 'The field "{fieldParameter}" associated with this object no longer exists in the index pattern. Please use another field.',
+ values: {
+ fieldParameter: field.name,
+ },
+ }
+ )
+ );
+ }
+
+ const validField = this.getAvailableFields(aggConfig).find(
+ (f: any) => f.name === field.name
+ );
+
+ if (!validField) {
+ throw new SavedFieldTypeInvalidForAgg(
+ i18n.translate(
+ 'data.search.aggs.paramTypes.field.invalidSavedFieldParameterErrorMessage',
+ {
+ defaultMessage:
+ 'Saved field "{fieldParameter}" of index pattern "{indexPatternTitle}" is invalid for use with the "{aggType}" aggregation. Please select a new field.',
+ values: {
+ fieldParameter: field.name,
+ aggType: aggConfig?.type?.title,
+ indexPatternTitle: aggConfig.getIndexPattern().title,
+ },
+ }
+ )
+ );
+ }
+
+ if (validField.scripted) {
output.params.script = {
- source: field.script,
- lang: field.lang,
+ source: validField.script,
+ lang: validField.lang,
};
} else {
- output.params.field = field.name;
+ output.params.field = validField.name;
}
};
}
@@ -69,28 +108,15 @@ export class FieldParamType extends BaseParamType {
const field = aggConfig.getIndexPattern().fields.getByName(fieldName);
if (!field) {
- throw new SavedObjectNotFound('index-pattern-field', fieldName);
- }
-
- const validField = this.getAvailableFields(aggConfig).find((f: any) => f.name === fieldName);
- if (!validField) {
- throw new Error(
- i18n.translate(
- 'data.search.aggs.paramTypes.field.invalidSavedFieldParameterErrorMessage',
- {
- defaultMessage:
- 'Saved field "{fieldParameter}" of index pattern "{indexPatternTitle}" is invalid for use with the "{aggType}" aggregation. Please select a new field.',
- values: {
- fieldParameter: fieldName,
- aggType: aggConfig?.type?.title,
- indexPatternTitle: aggConfig.getIndexPattern().title,
- },
- }
- )
- );
+ return new IndexPatternField({
+ type: KBN_FIELD_TYPES.MISSING,
+ name: fieldName,
+ searchable: false,
+ aggregatable: false,
+ });
}
- return validField;
+ return field;
};
}
diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts
index c47cd6cd9740db..d2683e248b7bf5 100644
--- a/src/plugins/data/public/index.ts
+++ b/src/plugins/data/public/index.ts
@@ -388,6 +388,8 @@ export {
PainlessError,
noSearchSessionStorageCapabilityMessage,
SEARCH_SESSIONS_MANAGEMENT_ID,
+ waitUntilNextSessionCompletes$,
+ WaitUntilNextSessionCompletesOptions,
} from './search';
export type {
@@ -467,6 +469,7 @@ export {
TimeHistoryContract,
QueryStateChange,
QueryStart,
+ AutoRefreshDoneFn,
} from './query';
export { AggsStart } from './search/aggs';
diff --git a/src/plugins/data/public/index_patterns/index_pattern.stub.ts b/src/plugins/data/public/index_patterns/index_pattern.stub.ts
index fa33f00a498792..36569cafd6611f 100644
--- a/src/plugins/data/public/index_patterns/index_pattern.stub.ts
+++ b/src/plugins/data/public/index_patterns/index_pattern.stub.ts
@@ -9,6 +9,7 @@
import sinon from 'sinon';
import { CoreSetup } from 'src/core/public';
+import { SerializedFieldFormat } from 'src/plugins/expressions/public';
import { IFieldType, FieldSpec } from '../../common/index_patterns';
import { IndexPattern, indexPatterns, KBN_FIELD_TYPES, fieldList } from '../';
import { getFieldFormatsRegistry } from '../test_utils';
@@ -51,6 +52,7 @@ export class StubIndexPattern {
_reindexFields: Function;
stubSetFieldFormat: Function;
fields?: FieldSpec[];
+ setFieldFormat: (fieldName: string, format: SerializedFieldFormat) => void;
constructor(
pattern: string,
@@ -74,6 +76,10 @@ export class StubIndexPattern {
this.metaFields = ['_id', '_type', '_source'];
this.fieldFormatMap = {};
+ this.setFieldFormat = (fieldName: string, format: SerializedFieldFormat) => {
+ this.fieldFormatMap[fieldName] = format;
+ };
+
this.getComputedFields = IndexPattern.prototype.getComputedFields.bind(this);
this.flattenHit = indexPatterns.flattenHitWrapper(
(this as unknown) as IndexPattern,
diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md
index ec24a9296674de..05925f097de240 100644
--- a/src/plugins/data/public/public.api.md
+++ b/src/plugins/data/public/public.api.md
@@ -504,6 +504,11 @@ export interface ApplyGlobalFilterActionContext {
// @public (undocumented)
export type AutocompleteStart = ReturnType;
+// Warning: (ae-missing-release-tag) "AutoRefreshDoneFn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+//
+// @public (undocumented)
+export type AutoRefreshDoneFn = () => void;
+
// Warning: (ae-forgotten-export) The symbol "DateFormat" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "DateNanosFormat" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "baseFormattersPublic" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -1781,6 +1786,8 @@ export enum KBN_FIELD_TYPES {
// (undocumented)
IP_RANGE = "ip_range",
// (undocumented)
+ MISSING = "missing",
+ // (undocumented)
MURMUR3 = "murmur3",
// (undocumented)
NESTED = "nested",
@@ -2647,6 +2654,18 @@ export const UI_SETTINGS: {
readonly AUTOCOMPLETE_USE_TIMERANGE: "autocomplete:useTimeRange";
};
+// Warning: (ae-missing-release-tag) "waitUntilNextSessionCompletes$" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+//
+// @public
+export function waitUntilNextSessionCompletes$(sessionService: ISessionService, { waitForIdle }?: WaitUntilNextSessionCompletesOptions): import("rxjs").Observable;
+
+// Warning: (ae-missing-release-tag) "WaitUntilNextSessionCompletesOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+//
+// @public
+export interface WaitUntilNextSessionCompletesOptions {
+ waitForIdle?: number;
+}
+
// Warnings were encountered during analysis:
//
@@ -2694,21 +2713,21 @@ export const UI_SETTINGS: {
// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:404:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:404:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:404:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:404:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:406:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:407:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:416:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:417:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:419:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:424:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:427:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:428:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:431:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:406:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:406:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:406:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:406:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:408:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:409:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:419:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:420:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:421:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:425:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:426:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:429:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:430:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:433:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/query/state_sync/connect_to_query_state.ts:34:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/search/session/session_service.ts:56:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts
diff --git a/src/plugins/data/public/query/timefilter/index.ts b/src/plugins/data/public/query/timefilter/index.ts
index 83e897824d86c8..3dfd4e0fe514f5 100644
--- a/src/plugins/data/public/query/timefilter/index.ts
+++ b/src/plugins/data/public/query/timefilter/index.ts
@@ -9,7 +9,7 @@
export { TimefilterService, TimefilterSetup } from './timefilter_service';
export * from './types';
-export { Timefilter, TimefilterContract } from './timefilter';
+export { Timefilter, TimefilterContract, AutoRefreshDoneFn } from './timefilter';
export { TimeHistory, TimeHistoryContract } from './time_history';
export { changeTimeFilter, convertRangeFilterToTimeRangeString } from './lib/change_time_filter';
export { extractTimeFilter, extractTimeRange } from './lib/extract_time_filter';
diff --git a/src/plugins/data/public/query/timefilter/lib/auto_refresh_loop.test.ts b/src/plugins/data/public/query/timefilter/lib/auto_refresh_loop.test.ts
new file mode 100644
index 00000000000000..3c8b316c3b878b
--- /dev/null
+++ b/src/plugins/data/public/query/timefilter/lib/auto_refresh_loop.test.ts
@@ -0,0 +1,205 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { createAutoRefreshLoop, AutoRefreshDoneFn } from './auto_refresh_loop';
+
+jest.useFakeTimers();
+
+test('triggers refresh with interval', () => {
+ const { loop$, start, stop } = createAutoRefreshLoop();
+
+ const fn = jest.fn((done) => done());
+ loop$.subscribe(fn);
+
+ jest.advanceTimersByTime(5000);
+ expect(fn).not.toBeCalled();
+
+ start(1000);
+
+ jest.advanceTimersByTime(1001);
+ expect(fn).toHaveBeenCalledTimes(1);
+
+ jest.advanceTimersByTime(1001);
+ expect(fn).toHaveBeenCalledTimes(2);
+
+ stop();
+
+ jest.advanceTimersByTime(5000);
+ expect(fn).toHaveBeenCalledTimes(2);
+});
+
+test('waits for done() to be called', () => {
+ const { loop$, start } = createAutoRefreshLoop();
+
+ let done!: AutoRefreshDoneFn;
+ const fn = jest.fn((_done) => {
+ done = _done;
+ });
+ loop$.subscribe(fn);
+ start(1000);
+
+ jest.advanceTimersByTime(1001);
+ expect(fn).toHaveBeenCalledTimes(1);
+ expect(done).toBeInstanceOf(Function);
+
+ jest.advanceTimersByTime(1001);
+ expect(fn).toHaveBeenCalledTimes(1);
+
+ done();
+
+ jest.advanceTimersByTime(500);
+ expect(fn).toHaveBeenCalledTimes(1);
+ jest.advanceTimersByTime(501);
+ expect(fn).toHaveBeenCalledTimes(2);
+});
+
+test('waits for done() from multiple subscribers to be called', () => {
+ const { loop$, start } = createAutoRefreshLoop();
+
+ let done1!: AutoRefreshDoneFn;
+ const fn1 = jest.fn((_done) => {
+ done1 = _done;
+ });
+ loop$.subscribe(fn1);
+
+ let done2!: AutoRefreshDoneFn;
+ const fn2 = jest.fn((_done) => {
+ done2 = _done;
+ });
+ loop$.subscribe(fn2);
+
+ start(1000);
+
+ jest.advanceTimersByTime(1001);
+ expect(fn1).toHaveBeenCalledTimes(1);
+ expect(done1).toBeInstanceOf(Function);
+
+ jest.advanceTimersByTime(1001);
+ expect(fn1).toHaveBeenCalledTimes(1);
+
+ done1();
+
+ jest.advanceTimersByTime(500);
+ expect(fn1).toHaveBeenCalledTimes(1);
+ jest.advanceTimersByTime(501);
+ expect(fn1).toHaveBeenCalledTimes(1);
+
+ done2();
+
+ jest.advanceTimersByTime(500);
+ expect(fn1).toHaveBeenCalledTimes(1);
+ jest.advanceTimersByTime(501);
+ expect(fn1).toHaveBeenCalledTimes(2);
+});
+
+test('unsubscribe() resets the state', () => {
+ const { loop$, start } = createAutoRefreshLoop();
+
+ let done1!: AutoRefreshDoneFn;
+ const fn1 = jest.fn((_done) => {
+ done1 = _done;
+ });
+ loop$.subscribe(fn1);
+
+ const fn2 = jest.fn();
+ const sub2 = loop$.subscribe(fn2);
+
+ start(1000);
+
+ jest.advanceTimersByTime(1001);
+ expect(fn1).toHaveBeenCalledTimes(1);
+ expect(done1).toBeInstanceOf(Function);
+
+ jest.advanceTimersByTime(1001);
+ expect(fn1).toHaveBeenCalledTimes(1);
+
+ done1();
+
+ jest.advanceTimersByTime(500);
+ expect(fn1).toHaveBeenCalledTimes(1);
+ jest.advanceTimersByTime(501);
+ expect(fn1).toHaveBeenCalledTimes(1);
+
+ sub2.unsubscribe();
+
+ jest.advanceTimersByTime(500);
+ expect(fn1).toHaveBeenCalledTimes(1);
+ jest.advanceTimersByTime(501);
+ expect(fn1).toHaveBeenCalledTimes(2);
+});
+
+test('calling done() twice is ignored', () => {
+ const { loop$, start } = createAutoRefreshLoop();
+
+ let done1!: AutoRefreshDoneFn;
+ const fn1 = jest.fn((_done) => {
+ done1 = _done;
+ });
+ loop$.subscribe(fn1);
+
+ const fn2 = jest.fn();
+ loop$.subscribe(fn2);
+
+ start(1000);
+
+ jest.advanceTimersByTime(1001);
+ expect(fn1).toHaveBeenCalledTimes(1);
+ expect(done1).toBeInstanceOf(Function);
+
+ jest.advanceTimersByTime(1001);
+ expect(fn1).toHaveBeenCalledTimes(1);
+
+ done1();
+
+ jest.advanceTimersByTime(500);
+ expect(fn1).toHaveBeenCalledTimes(1);
+ jest.advanceTimersByTime(501);
+ expect(fn1).toHaveBeenCalledTimes(1);
+
+ done1();
+
+ jest.advanceTimersByTime(500);
+ expect(fn1).toHaveBeenCalledTimes(1);
+ jest.advanceTimersByTime(501);
+ expect(fn1).toHaveBeenCalledTimes(1);
+});
+
+test('calling older done() is ignored', () => {
+ const { loop$, start } = createAutoRefreshLoop();
+
+ let done1!: AutoRefreshDoneFn;
+ const fn1 = jest.fn((_done) => {
+ // @ts-ignore
+ if (done1) return;
+ done1 = _done;
+ });
+ loop$.subscribe(fn1);
+
+ start(1000);
+
+ jest.advanceTimersByTime(1001);
+ expect(fn1).toHaveBeenCalledTimes(1);
+ expect(done1).toBeInstanceOf(Function);
+
+ jest.advanceTimersByTime(1001);
+ expect(fn1).toHaveBeenCalledTimes(1);
+
+ done1();
+
+ jest.advanceTimersByTime(500);
+ expect(fn1).toHaveBeenCalledTimes(1);
+ jest.advanceTimersByTime(501);
+ expect(fn1).toHaveBeenCalledTimes(2);
+
+ done1();
+
+ jest.advanceTimersByTime(500);
+ expect(fn1).toHaveBeenCalledTimes(2);
+ jest.advanceTimersByTime(501);
+ expect(fn1).toHaveBeenCalledTimes(2);
+});
diff --git a/src/plugins/data/public/query/timefilter/lib/auto_refresh_loop.ts b/src/plugins/data/public/query/timefilter/lib/auto_refresh_loop.ts
new file mode 100644
index 00000000000000..1e213b36e1d8b6
--- /dev/null
+++ b/src/plugins/data/public/query/timefilter/lib/auto_refresh_loop.ts
@@ -0,0 +1,80 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { defer, Subject } from 'rxjs';
+import { finalize, map } from 'rxjs/operators';
+import { once } from 'lodash';
+
+export type AutoRefreshDoneFn = () => void;
+
+/**
+ * Creates a loop for timepicker's auto refresh
+ * It has a "confirmation" mechanism:
+ * When auto refresh loop emits, it won't continue automatically,
+ * until each subscriber calls received `done` function.
+ *
+ * @internal
+ */
+export const createAutoRefreshLoop = () => {
+ let subscribersCount = 0;
+ const tick = new Subject();
+
+ let _timeoutHandle: number;
+ let _timeout: number = 0;
+
+ function start() {
+ stop();
+ if (_timeout === 0) return;
+ const timeoutHandle = window.setTimeout(() => {
+ let pendingDoneCount = subscribersCount;
+ const done = () => {
+ if (timeoutHandle !== _timeoutHandle) return;
+
+ pendingDoneCount--;
+ if (pendingDoneCount === 0) {
+ start();
+ }
+ };
+ tick.next(done);
+ }, _timeout);
+
+ _timeoutHandle = timeoutHandle;
+ }
+
+ function stop() {
+ window.clearTimeout(_timeoutHandle);
+ _timeoutHandle = -1;
+ }
+
+ return {
+ stop: () => {
+ _timeout = 0;
+ stop();
+ },
+ start: (timeout: number) => {
+ _timeout = timeout;
+ if (subscribersCount > 0) {
+ start();
+ }
+ },
+ loop$: defer(() => {
+ subscribersCount++;
+ start(); // restart the loop on a new subscriber
+ return tick.pipe(map((doneCb) => once(doneCb))); // each subscriber allowed to call done only once
+ }).pipe(
+ finalize(() => {
+ subscribersCount--;
+ if (subscribersCount === 0) {
+ stop();
+ } else {
+ start(); // restart the loop to potentially unblock the interval
+ }
+ })
+ ),
+ };
+};
diff --git a/src/plugins/data/public/query/timefilter/timefilter.test.ts b/src/plugins/data/public/query/timefilter/timefilter.test.ts
index 8e1e76ed19e6d2..92ee6b0c304289 100644
--- a/src/plugins/data/public/query/timefilter/timefilter.test.ts
+++ b/src/plugins/data/public/query/timefilter/timefilter.test.ts
@@ -10,7 +10,7 @@ jest.useFakeTimers();
import sinon from 'sinon';
import moment from 'moment';
-import { Timefilter } from './timefilter';
+import { AutoRefreshDoneFn, Timefilter } from './timefilter';
import { Subscription } from 'rxjs';
import { TimeRange, RefreshInterval } from '../../../common';
import { createNowProviderMock } from '../../now_provider/mocks';
@@ -121,7 +121,7 @@ describe('setRefreshInterval', () => {
beforeEach(() => {
update = sinon.spy();
fetch = sinon.spy();
- autoRefreshFetch = sinon.spy();
+ autoRefreshFetch = sinon.spy((done) => done());
timefilter.setRefreshInterval({
pause: false,
value: 0,
@@ -344,3 +344,44 @@ describe('calculateBounds', () => {
expect(() => timefilter.calculateBounds(timeRange)).toThrowError();
});
});
+
+describe('getAutoRefreshFetch$', () => {
+ test('next auto refresh loop starts after "done" called', () => {
+ const autoRefreshFetch = jest.fn();
+ let doneCb: AutoRefreshDoneFn | undefined;
+ timefilter.getAutoRefreshFetch$().subscribe((done) => {
+ autoRefreshFetch();
+ doneCb = done;
+ });
+ timefilter.setRefreshInterval({ pause: false, value: 1000 });
+
+ expect(autoRefreshFetch).toBeCalledTimes(0);
+ jest.advanceTimersByTime(5000);
+ expect(autoRefreshFetch).toBeCalledTimes(1);
+
+ if (doneCb) doneCb();
+
+ jest.advanceTimersByTime(1005);
+ expect(autoRefreshFetch).toBeCalledTimes(2);
+ });
+
+ test('new getAutoRefreshFetch$ subscription restarts refresh loop', () => {
+ const autoRefreshFetch = jest.fn();
+ const fetch$ = timefilter.getAutoRefreshFetch$();
+ const sub1 = fetch$.subscribe((done) => {
+ autoRefreshFetch();
+ // this done will be never called, but loop will be reset by another subscription
+ });
+ timefilter.setRefreshInterval({ pause: false, value: 1000 });
+
+ expect(autoRefreshFetch).toBeCalledTimes(0);
+ jest.advanceTimersByTime(5000);
+ expect(autoRefreshFetch).toBeCalledTimes(1);
+
+ fetch$.subscribe(autoRefreshFetch);
+ expect(autoRefreshFetch).toBeCalledTimes(1);
+ sub1.unsubscribe();
+ jest.advanceTimersByTime(1005);
+ expect(autoRefreshFetch).toBeCalledTimes(2);
+ });
+});
diff --git a/src/plugins/data/public/query/timefilter/timefilter.ts b/src/plugins/data/public/query/timefilter/timefilter.ts
index 436b18f70a2f88..9894010601d2b1 100644
--- a/src/plugins/data/public/query/timefilter/timefilter.ts
+++ b/src/plugins/data/public/query/timefilter/timefilter.ts
@@ -22,6 +22,9 @@ import {
TimeRange,
} from '../../../common';
import { TimeHistoryContract } from './time_history';
+import { createAutoRefreshLoop, AutoRefreshDoneFn } from './lib/auto_refresh_loop';
+
+export { AutoRefreshDoneFn };
// TODO: remove!
@@ -32,8 +35,6 @@ export class Timefilter {
private timeUpdate$ = new Subject();
// Fired when a user changes the the autorefresh settings
private refreshIntervalUpdate$ = new Subject();
- // Used when an auto refresh is triggered
- private autoRefreshFetch$ = new Subject();
private fetch$ = new Subject();
private _time: TimeRange;
@@ -45,11 +46,12 @@ export class Timefilter {
private _isTimeRangeSelectorEnabled: boolean = false;
private _isAutoRefreshSelectorEnabled: boolean = false;
- private _autoRefreshIntervalId: number = 0;
-
private readonly timeDefaults: TimeRange;
private readonly refreshIntervalDefaults: RefreshInterval;
+ // Used when an auto refresh is triggered
+ private readonly autoRefreshLoop = createAutoRefreshLoop();
+
constructor(
config: TimefilterConfig,
timeHistory: TimeHistoryContract,
@@ -86,9 +88,13 @@ export class Timefilter {
return this.refreshIntervalUpdate$.asObservable();
};
- public getAutoRefreshFetch$ = () => {
- return this.autoRefreshFetch$.asObservable();
- };
+ /**
+ * Get an observable that emits when it is time to refetch data due to refresh interval
+ * Each subscription to this observable resets internal interval
+ * Emitted value is a callback {@link AutoRefreshDoneFn} that must be called to restart refresh interval loop
+ * Apps should use this callback to start next auto refresh loop when view finished updating
+ */
+ public getAutoRefreshFetch$ = () => this.autoRefreshLoop.loop$;
public getFetch$ = () => {
return this.fetch$.asObservable();
@@ -166,13 +172,9 @@ export class Timefilter {
}
}
- // Clear the previous auto refresh interval and start a new one (if not paused)
- clearInterval(this._autoRefreshIntervalId);
- if (!newRefreshInterval.pause) {
- this._autoRefreshIntervalId = window.setInterval(
- () => this.autoRefreshFetch$.next(),
- newRefreshInterval.value
- );
+ this.autoRefreshLoop.stop();
+ if (!newRefreshInterval.pause && newRefreshInterval.value !== 0) {
+ this.autoRefreshLoop.start(newRefreshInterval.value);
}
};
diff --git a/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts b/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts
index 0f2b01f6181869..c22f62f45a709f 100644
--- a/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts
+++ b/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts
@@ -20,7 +20,7 @@ const createSetupContractMock = () => {
getEnabledUpdated$: jest.fn(),
getTimeUpdate$: jest.fn(),
getRefreshIntervalUpdate$: jest.fn(),
- getAutoRefreshFetch$: jest.fn(() => new Observable()),
+ getAutoRefreshFetch$: jest.fn(() => new Observable<() => void>()),
getFetch$: jest.fn(),
getTime: jest.fn(),
setTime: jest.fn(),
diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts
index fded4c46992c04..92a5c36202e6f3 100644
--- a/src/plugins/data/public/search/index.ts
+++ b/src/plugins/data/public/search/index.ts
@@ -45,6 +45,8 @@ export {
ISessionsClient,
noSearchSessionStorageCapabilityMessage,
SEARCH_SESSIONS_MANAGEMENT_ID,
+ waitUntilNextSessionCompletes$,
+ WaitUntilNextSessionCompletesOptions,
} from './session';
export { getEsPreference } from './es_search';
diff --git a/src/plugins/data/public/search/session/index.ts b/src/plugins/data/public/search/session/index.ts
index 15410400a33e64..ce578378a2fe81 100644
--- a/src/plugins/data/public/search/session/index.ts
+++ b/src/plugins/data/public/search/session/index.ts
@@ -11,3 +11,7 @@ export { SearchSessionState } from './search_session_state';
export { SessionsClient, ISessionsClient } from './sessions_client';
export { noSearchSessionStorageCapabilityMessage } from './i18n';
export { SEARCH_SESSIONS_MANAGEMENT_ID } from './constants';
+export {
+ waitUntilNextSessionCompletes$,
+ WaitUntilNextSessionCompletesOptions,
+} from './session_helpers';
diff --git a/src/plugins/data/public/search/session/session_helpers.test.ts b/src/plugins/data/public/search/session/session_helpers.test.ts
new file mode 100644
index 00000000000000..5b64e7b554d18f
--- /dev/null
+++ b/src/plugins/data/public/search/session/session_helpers.test.ts
@@ -0,0 +1,88 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { waitUntilNextSessionCompletes$ } from './session_helpers';
+import { ISessionService, SessionService } from './session_service';
+import { BehaviorSubject } from 'rxjs';
+import { SearchSessionState } from './search_session_state';
+import { NowProviderInternalContract } from '../../now_provider';
+import { coreMock } from '../../../../../core/public/mocks';
+import { createNowProviderMock } from '../../now_provider/mocks';
+import { SEARCH_SESSIONS_MANAGEMENT_ID } from './constants';
+import { getSessionsClientMock } from './mocks';
+
+let sessionService: ISessionService;
+let state$: BehaviorSubject;
+let nowProvider: jest.Mocked;
+let currentAppId$: BehaviorSubject;
+
+beforeEach(() => {
+ const initializerContext = coreMock.createPluginInitializerContext();
+ const startService = coreMock.createSetup().getStartServices;
+ nowProvider = createNowProviderMock();
+ currentAppId$ = new BehaviorSubject('app');
+ sessionService = new SessionService(
+ initializerContext,
+ () =>
+ startService().then(([coreStart, ...rest]) => [
+ {
+ ...coreStart,
+ application: {
+ ...coreStart.application,
+ currentAppId$,
+ capabilities: {
+ ...coreStart.application.capabilities,
+ management: {
+ kibana: {
+ [SEARCH_SESSIONS_MANAGEMENT_ID]: true,
+ },
+ },
+ },
+ },
+ },
+ ...rest,
+ ]),
+ getSessionsClientMock(),
+ nowProvider,
+ { freezeState: false } // needed to use mocks inside state container
+ );
+ state$ = new BehaviorSubject(SearchSessionState.None);
+ sessionService.state$.subscribe(state$);
+});
+
+describe('waitUntilNextSessionCompletes$', () => {
+ beforeEach(() => {
+ jest.useFakeTimers();
+ });
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+ test('emits when next session starts', () => {
+ sessionService.start();
+ let untrackSearch = sessionService.trackSearch({ abort: () => {} });
+ untrackSearch();
+
+ const next = jest.fn();
+ const complete = jest.fn();
+ waitUntilNextSessionCompletes$(sessionService).subscribe({ next, complete });
+ expect(next).not.toBeCalled();
+
+ sessionService.start();
+ expect(next).not.toBeCalled();
+
+ untrackSearch = sessionService.trackSearch({ abort: () => {} });
+ untrackSearch();
+
+ expect(next).not.toBeCalled();
+ jest.advanceTimersByTime(500);
+ expect(next).not.toBeCalled();
+ jest.advanceTimersByTime(1000);
+ expect(next).toBeCalledTimes(1);
+ expect(complete).toBeCalled();
+ });
+});
diff --git a/src/plugins/data/public/search/session/session_helpers.ts b/src/plugins/data/public/search/session/session_helpers.ts
new file mode 100644
index 00000000000000..1f0a2da7e93f4d
--- /dev/null
+++ b/src/plugins/data/public/search/session/session_helpers.ts
@@ -0,0 +1,48 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { debounceTime, first, skipUntil } from 'rxjs/operators';
+import { ISessionService } from './session_service';
+import { SearchSessionState } from './search_session_state';
+
+/**
+ * Options for {@link waitUntilNextSessionCompletes$}
+ */
+export interface WaitUntilNextSessionCompletesOptions {
+ /**
+ * For how long to wait between session state transitions before considering that session completed
+ */
+ waitForIdle?: number;
+}
+
+/**
+ * Creates an observable that emits when next search session completes.
+ * This utility is helpful to use in the application to delay some tasks until next session completes.
+ *
+ * @param sessionService - {@link ISessionService}
+ * @param opts - {@link WaitUntilNextSessionCompletesOptions}
+ */
+export function waitUntilNextSessionCompletes$(
+ sessionService: ISessionService,
+ { waitForIdle = 1000 }: WaitUntilNextSessionCompletesOptions = { waitForIdle: 1000 }
+) {
+ return sessionService.state$.pipe(
+ // wait until new session starts
+ skipUntil(sessionService.state$.pipe(first((state) => state === SearchSessionState.None))),
+ // wait until new session starts loading
+ skipUntil(sessionService.state$.pipe(first((state) => state === SearchSessionState.Loading))),
+ // debounce to ignore quick switches from loading <-> completed.
+ // that could happen between sequential search requests inside a single session
+ debounceTime(waitForIdle),
+ // then wait until it finishes
+ first(
+ (state) =>
+ state === SearchSessionState.Completed || state === SearchSessionState.BackgroundCompleted
+ )
+ );
+}
diff --git a/src/plugins/data/public/ui/query_string_input/_query_bar.scss b/src/plugins/data/public/ui/query_string_input/_query_bar.scss
index 466cc8c3de0b76..4e12f116687344 100644
--- a/src/plugins/data/public/ui/query_string_input/_query_bar.scss
+++ b/src/plugins/data/public/ui/query_string_input/_query_bar.scss
@@ -17,6 +17,16 @@
@include kbnThemeStyle('v8') {
background-color: $euiFormBackgroundColor;
+ border-radius: $euiFormControlBorderRadius;
+
+ &.kbnQueryBar__textareaWrap--hasPrepend {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+ &.kbnQueryBar__textareaWrap--hasAppend {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ }
}
}
@@ -35,8 +45,16 @@
}
@include kbnThemeStyle('v8') {
- border-radius: 0;
padding-bottom: $euiSizeS + 1px;
+
+ &.kbnQueryBar__textarea--hasPrepend {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+ &.kbnQueryBar__textarea--hasAppend {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ }
}
&:not(.kbnQueryBar__textarea--autoHeight):not(:invalid) {
diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx
index 900a4ab7d7eb7a..0f660f87266fd8 100644
--- a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx
+++ b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx
@@ -682,7 +682,14 @@ export default class QueryStringInputUI extends Component {
);
const inputClassName = classNames(
'kbnQueryBar__textarea',
- this.props.iconType ? 'kbnQueryBar__textarea--withIcon' : null
+ this.props.iconType ? 'kbnQueryBar__textarea--withIcon' : null,
+ this.props.prepend ? 'kbnQueryBar__textarea--hasPrepend' : null,
+ !this.props.disableLanguageSwitcher ? 'kbnQueryBar__textarea--hasAppend' : null
+ );
+ const inputWrapClassName = classNames(
+ 'euiFormControlLayout__childrenWrapper kbnQueryBar__textareaWrap',
+ this.props.prepend ? 'kbnQueryBar__textareaWrap--hasPrepend' : null,
+ !this.props.disableLanguageSwitcher ? 'kbnQueryBar__textareaWrap--hasAppend' : null
);
return (
@@ -711,7 +718,7 @@ export default class QueryStringInputUI extends Component {
>
{
+ autoRefreshDoneCb = done;
+ }),
+ filter(() => $scope.fetchStatus !== fetchStatuses.LOADING)
+ ),
data.query.queryString.getUpdates$(),
searchSessionManager.newSearchSessionIdFromURL$
).pipe(debounceTime(100));
@@ -508,7 +515,16 @@ function discoverController($route, $scope) {
$scope,
fetch$,
{
- next: $scope.fetch,
+ next: async () => {
+ try {
+ await $scope.fetch();
+ } finally {
+ // if there is a saved `autoRefreshDoneCb`, notify auto refresh service that
+ // the last fetch is completed so it starts the next auto refresh loop if needed
+ autoRefreshDoneCb?.();
+ autoRefreshDoneCb = undefined;
+ }
+ },
},
(error) => addFatalError(core.fatalErrors, error)
)
diff --git a/src/plugins/discover/public/application/components/sidebar/__snapshots__/discover_field_details_footer.test.tsx.snap b/src/plugins/discover/public/application/components/sidebar/__snapshots__/discover_field_details_footer.test.tsx.snap
index f3c8990388024c..f976b961d8520f 100644
--- a/src/plugins/discover/public/application/components/sidebar/__snapshots__/discover_field_details_footer.test.tsx.snap
+++ b/src/plugins/discover/public/application/components/sidebar/__snapshots__/discover_field_details_footer.test.tsx.snap
@@ -543,6 +543,7 @@ exports[`discover sidebar field details footer renders properly 1`] = `
"_source",
],
"popularizeField": [Function],
+ "setFieldFormat": [Function],
"stubSetFieldFormat": [Function],
"timeFieldName": "time",
"title": "logstash-*",
diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx
index e8418970d83f74..a0cd213b7bf248 100644
--- a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx
+++ b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx
@@ -9,7 +9,7 @@
import { cloneDeep, isEqual } from 'lodash';
import * as Rx from 'rxjs';
import { merge } from 'rxjs';
-import { debounceTime, distinctUntilChanged, map, mapTo, skip } from 'rxjs/operators';
+import { debounceTime, distinctUntilChanged, map, skip } from 'rxjs/operators';
import { RenderCompleteDispatcher } from '../../../../kibana_utils/public';
import { Adapters } from '../types';
import { IContainer } from '../containers';
@@ -111,10 +111,9 @@ export abstract class Embeddable<
* In case corresponding state change triggered `reload` this stream is guarantied to emit later,
* which allows to skip any state handling in case `reload` already handled it.
*/
- public getUpdated$(): Readonly> {
+ public getUpdated$(): Readonly> {
return merge(this.getInput$().pipe(skip(1)), this.getOutput$().pipe(skip(1))).pipe(
- debounceTime(0),
- mapTo(undefined)
+ debounceTime(0)
);
}
diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md
index b9719542adc813..3f0907acabdfa0 100644
--- a/src/plugins/embeddable/public/public.api.md
+++ b/src/plugins/embeddable/public/public.api.md
@@ -282,7 +282,7 @@ export abstract class Embeddable>;
+ getUpdated$(): Readonly>;
// (undocumented)
readonly id: string;
// (undocumented)
diff --git a/src/plugins/expressions/public/loader.ts b/src/plugins/expressions/public/loader.ts
index 65925b5a2e4c20..4165b8906a20ee 100644
--- a/src/plugins/expressions/public/loader.ts
+++ b/src/plugins/expressions/public/loader.ts
@@ -118,12 +118,15 @@ export class ExpressionLoader {
return this.execution ? (this.execution.inspect() as Adapters) : undefined;
}
- update(expression?: string | ExpressionAstExpression, params?: IExpressionLoaderParams): void {
+ async update(
+ expression?: string | ExpressionAstExpression,
+ params?: IExpressionLoaderParams
+ ): Promise {
this.setParams(params);
this.loadingSubject.next(true);
if (expression) {
- this.loadData(expression, this.params);
+ await this.loadData(expression, this.params);
} else if (this.data) {
this.render(this.data);
}
diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx
index e0ca654c956c64..13830f9233b5e9 100644
--- a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx
+++ b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx
@@ -143,7 +143,7 @@ const FieldEditorFlyoutContentComponent = ({
const [isValidating, setIsValidating] = useState(false);
const [isModalVisible, setIsModalVisible] = useState(false);
- const [confirmContent, setConfirmContent] = useState();
+ const [confirmContent, setConfirmContent] = useState('');
const { submit, isValid: isFormValid, isSubmitted } = formState;
const { fields } = indexPattern;
diff --git a/src/plugins/kibana_usage_collection/tsconfig.json b/src/plugins/kibana_usage_collection/tsconfig.json
index d664d936f66671..ee07dfe589e4a5 100644
--- a/src/plugins/kibana_usage_collection/tsconfig.json
+++ b/src/plugins/kibana_usage_collection/tsconfig.json
@@ -5,7 +5,8 @@
"outDir": "./target/types",
"emitDeclarationOnly": true,
"declaration": true,
- "declarationMap": true
+ "declarationMap": true,
+ "isolatedModules": true
},
"include": [
"common/*",
diff --git a/src/plugins/kibana_utils/common/errors/errors.ts b/src/plugins/kibana_utils/common/errors/errors.ts
index 7a9495cc8f4134..7f3efc6d9571fb 100644
--- a/src/plugins/kibana_utils/common/errors/errors.ts
+++ b/src/plugins/kibana_utils/common/errors/errors.ts
@@ -32,7 +32,7 @@ export class DuplicateField extends KbnError {
export class SavedObjectNotFound extends KbnError {
public savedObjectType: string;
public savedObjectId?: string;
- constructor(type: string, id?: string, link?: string) {
+ constructor(type: string, id?: string, link?: string, customMessage?: string) {
const idMsg = id ? ` (id: ${id})` : '';
let message = `Could not locate that ${type}${idMsg}`;
@@ -40,13 +40,31 @@ export class SavedObjectNotFound extends KbnError {
message += `, [click here to re-create it](${link})`;
}
- super(message);
+ super(customMessage || message);
this.savedObjectType = type;
this.savedObjectId = id;
}
}
+/**
+ * A saved field doesn't exist anymore
+ */
+export class SavedFieldNotFound extends KbnError {
+ constructor(message: string) {
+ super(message);
+ }
+}
+
+/**
+ * A saved field type isn't compatible with aggregation
+ */
+export class SavedFieldTypeInvalidForAgg extends KbnError {
+ constructor(message: string) {
+ super(message);
+ }
+}
+
/**
* This error is for scenarios where a saved object is detected that has invalid JSON properties.
* There was a scenario where we were importing objects with double-encoded JSON, and the system
diff --git a/src/plugins/telemetry/common/telemetry_config/index.ts b/src/plugins/telemetry/common/telemetry_config/index.ts
index 84b6486f35b246..cc4ff102742d7f 100644
--- a/src/plugins/telemetry/common/telemetry_config/index.ts
+++ b/src/plugins/telemetry/common/telemetry_config/index.ts
@@ -9,7 +9,5 @@
export { getTelemetryOptIn } from './get_telemetry_opt_in';
export { getTelemetrySendUsageFrom } from './get_telemetry_send_usage_from';
export { getTelemetryAllowChangingOptInStatus } from './get_telemetry_allow_changing_opt_in_status';
-export {
- getTelemetryFailureDetails,
- TelemetryFailureDetails,
-} from './get_telemetry_failure_details';
+export { getTelemetryFailureDetails } from './get_telemetry_failure_details';
+export type { TelemetryFailureDetails } from './get_telemetry_failure_details';
diff --git a/src/plugins/telemetry/public/index.ts b/src/plugins/telemetry/public/index.ts
index 6cca9bdf881dd8..47ba7828eaec2d 100644
--- a/src/plugins/telemetry/public/index.ts
+++ b/src/plugins/telemetry/public/index.ts
@@ -8,7 +8,7 @@
import { PluginInitializerContext } from 'kibana/public';
import { TelemetryPlugin, TelemetryPluginConfig } from './plugin';
-export { TelemetryPluginStart, TelemetryPluginSetup } from './plugin';
+export type { TelemetryPluginStart, TelemetryPluginSetup } from './plugin';
export function plugin(initializerContext: PluginInitializerContext) {
return new TelemetryPlugin(initializerContext);
diff --git a/src/plugins/telemetry/server/index.ts b/src/plugins/telemetry/server/index.ts
index debdf7515cd583..1c335426ffd03c 100644
--- a/src/plugins/telemetry/server/index.ts
+++ b/src/plugins/telemetry/server/index.ts
@@ -13,7 +13,7 @@ import { configSchema, TelemetryConfigType } from './config';
export { FetcherTask } from './fetcher';
export { handleOldSettings } from './handle_old_settings';
-export { TelemetryPluginSetup, TelemetryPluginStart } from './plugin';
+export type { TelemetryPluginSetup, TelemetryPluginStart } from './plugin';
export const config: PluginConfigDescriptor = {
schema: configSchema,
@@ -34,9 +34,12 @@ export { constants };
export {
getClusterUuids,
getLocalStats,
- TelemetryLocalStats,
DATA_TELEMETRY_ID,
+ buildDataTelemetryPayload,
+} from './telemetry_collection';
+
+export type {
+ TelemetryLocalStats,
DataTelemetryIndex,
DataTelemetryPayload,
- buildDataTelemetryPayload,
} from './telemetry_collection';
diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts
index def1131dfb1a34..c93b7e872924ba 100644
--- a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts
+++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts
@@ -7,10 +7,5 @@
*/
export { DATA_TELEMETRY_ID } from './constants';
-
-export {
- getDataTelemetry,
- buildDataTelemetryPayload,
- DataTelemetryPayload,
- DataTelemetryIndex,
-} from './get_data_telemetry';
+export { getDataTelemetry, buildDataTelemetryPayload } from './get_data_telemetry';
+export type { DataTelemetryPayload, DataTelemetryIndex } from './get_data_telemetry';
diff --git a/src/plugins/telemetry/server/telemetry_collection/index.ts b/src/plugins/telemetry/server/telemetry_collection/index.ts
index 55f9c7f0e624c8..151e89a11a1923 100644
--- a/src/plugins/telemetry/server/telemetry_collection/index.ts
+++ b/src/plugins/telemetry/server/telemetry_collection/index.ts
@@ -6,12 +6,9 @@
* Side Public License, v 1.
*/
-export {
- DATA_TELEMETRY_ID,
- DataTelemetryIndex,
- DataTelemetryPayload,
- buildDataTelemetryPayload,
-} from './get_data_telemetry';
-export { getLocalStats, TelemetryLocalStats } from './get_local_stats';
+export { DATA_TELEMETRY_ID, buildDataTelemetryPayload } from './get_data_telemetry';
+export type { DataTelemetryIndex, DataTelemetryPayload } from './get_data_telemetry';
+export { getLocalStats } from './get_local_stats';
+export type { TelemetryLocalStats } from './get_local_stats';
export { getClusterUuids } from './get_cluster_stats';
export { registerCollection } from './register_collection';
diff --git a/src/plugins/telemetry/server/telemetry_repository/index.ts b/src/plugins/telemetry/server/telemetry_repository/index.ts
index 4e3f046f7611fe..594b53259a65f0 100644
--- a/src/plugins/telemetry/server/telemetry_repository/index.ts
+++ b/src/plugins/telemetry/server/telemetry_repository/index.ts
@@ -8,7 +8,7 @@
export { getTelemetrySavedObject } from './get_telemetry_saved_object';
export { updateTelemetrySavedObject } from './update_telemetry_saved_object';
-export {
+export type {
TelemetrySavedObject,
TelemetrySavedObjectAttributes,
} from '../../common/telemetry_config/types';
diff --git a/src/plugins/telemetry/tsconfig.json b/src/plugins/telemetry/tsconfig.json
index bdced01d9eb6f0..6629e479906c98 100644
--- a/src/plugins/telemetry/tsconfig.json
+++ b/src/plugins/telemetry/tsconfig.json
@@ -5,7 +5,8 @@
"outDir": "./target/types",
"emitDeclarationOnly": true,
"declaration": true,
- "declarationMap": true
+ "declarationMap": true,
+ "isolatedModules": true
},
"include": [
"public/**/**/*",
diff --git a/src/plugins/telemetry_collection_manager/server/index.ts b/src/plugins/telemetry_collection_manager/server/index.ts
index 77077b73cf8adc..c0cd124a132c07 100644
--- a/src/plugins/telemetry_collection_manager/server/index.ts
+++ b/src/plugins/telemetry_collection_manager/server/index.ts
@@ -16,7 +16,7 @@ export function plugin(initializerContext: PluginInitializerContext) {
return new TelemetryCollectionManagerPlugin(initializerContext);
}
-export {
+export type {
TelemetryCollectionManagerPluginSetup,
TelemetryCollectionManagerPluginStart,
StatsCollectionConfig,
diff --git a/src/plugins/telemetry_collection_manager/tsconfig.json b/src/plugins/telemetry_collection_manager/tsconfig.json
index 1bba81769f0dd0..13299798606031 100644
--- a/src/plugins/telemetry_collection_manager/tsconfig.json
+++ b/src/plugins/telemetry_collection_manager/tsconfig.json
@@ -5,7 +5,8 @@
"outDir": "./target/types",
"emitDeclarationOnly": true,
"declaration": true,
- "declarationMap": true
+ "declarationMap": true,
+ "isolatedModules": true
},
"include": [
"server/**/*",
diff --git a/src/plugins/telemetry_management_section/public/index.ts b/src/plugins/telemetry_management_section/public/index.ts
index 28b04418f512d2..db6ea17556ed30 100644
--- a/src/plugins/telemetry_management_section/public/index.ts
+++ b/src/plugins/telemetry_management_section/public/index.ts
@@ -10,7 +10,7 @@ import { TelemetryManagementSectionPlugin } from './plugin';
export { OptInExampleFlyout } from './components';
-export { TelemetryManagementSectionPluginSetup } from './plugin';
+export type { TelemetryManagementSectionPluginSetup } from './plugin';
export function plugin() {
return new TelemetryManagementSectionPlugin();
}
diff --git a/src/plugins/telemetry_management_section/tsconfig.json b/src/plugins/telemetry_management_section/tsconfig.json
index 48e40814b8570d..2daee868ac200c 100644
--- a/src/plugins/telemetry_management_section/tsconfig.json
+++ b/src/plugins/telemetry_management_section/tsconfig.json
@@ -5,7 +5,8 @@
"outDir": "./target/types",
"emitDeclarationOnly": true,
"declaration": true,
- "declarationMap": true
+ "declarationMap": true,
+ "isolatedModules": true
},
"include": [
"public/**/*",
diff --git a/src/plugins/usage_collection/public/index.ts b/src/plugins/usage_collection/public/index.ts
index b9e0e0a8985b16..9b009b1d9e2641 100644
--- a/src/plugins/usage_collection/public/index.ts
+++ b/src/plugins/usage_collection/public/index.ts
@@ -10,7 +10,7 @@ import { PluginInitializerContext } from '../../../core/public';
import { UsageCollectionPlugin } from './plugin';
export { METRIC_TYPE } from '@kbn/analytics';
-export { UsageCollectionSetup, UsageCollectionStart } from './plugin';
+export type { UsageCollectionSetup, UsageCollectionStart } from './plugin';
export { TrackApplicationView } from './components';
export function plugin(initializerContext: PluginInitializerContext) {
diff --git a/src/plugins/usage_collection/server/collector/index.ts b/src/plugins/usage_collection/server/collector/index.ts
index 5f48f9fb938137..d5e0d95659e58d 100644
--- a/src/plugins/usage_collection/server/collector/index.ts
+++ b/src/plugins/usage_collection/server/collector/index.ts
@@ -6,9 +6,10 @@
* Side Public License, v 1.
*/
-export { CollectorSet, CollectorSetPublic } from './collector_set';
-export {
- Collector,
+export { CollectorSet } from './collector_set';
+export type { CollectorSetPublic } from './collector_set';
+export { Collector } from './collector';
+export type {
AllowedSchemaTypes,
AllowedSchemaNumberTypes,
SchemaField,
@@ -16,4 +17,5 @@ export {
CollectorOptions,
CollectorFetchContext,
} from './collector';
-export { UsageCollector, UsageCollectorOptions } from './usage_collector';
+export { UsageCollector } from './usage_collector';
+export type { UsageCollectorOptions } from './usage_collector';
diff --git a/src/plugins/usage_collection/server/index.ts b/src/plugins/usage_collection/server/index.ts
index dfc9d19b69646a..dd9e6644a827dc 100644
--- a/src/plugins/usage_collection/server/index.ts
+++ b/src/plugins/usage_collection/server/index.ts
@@ -9,17 +9,16 @@
import { PluginInitializerContext } from 'src/core/server';
import { UsageCollectionPlugin } from './plugin';
-export {
+export { Collector } from './collector';
+export type {
AllowedSchemaTypes,
MakeSchemaFrom,
SchemaField,
CollectorOptions,
UsageCollectorOptions,
- Collector,
CollectorFetchContext,
} from './collector';
-
-export { UsageCollectionSetup } from './plugin';
+export type { UsageCollectionSetup } from './plugin';
export { config } from './config';
export const plugin = (initializerContext: PluginInitializerContext) =>
new UsageCollectionPlugin(initializerContext);
diff --git a/src/plugins/usage_collection/server/usage_collection.mock.ts b/src/plugins/usage_collection/server/usage_collection.mock.ts
index 1a60d84e7948c4..7e3f4273bbea8d 100644
--- a/src/plugins/usage_collection/server/usage_collection.mock.ts
+++ b/src/plugins/usage_collection/server/usage_collection.mock.ts
@@ -16,7 +16,8 @@ import {
import { CollectorOptions, Collector, UsageCollector } from './collector';
import { UsageCollectionSetup, CollectorFetchContext } from './index';
-export { CollectorOptions, Collector };
+export type { CollectorOptions };
+export { Collector };
const logger = loggingSystemMock.createLogger();
diff --git a/src/plugins/usage_collection/tsconfig.json b/src/plugins/usage_collection/tsconfig.json
index 96b2c4d37e17c2..68a0853994e80d 100644
--- a/src/plugins/usage_collection/tsconfig.json
+++ b/src/plugins/usage_collection/tsconfig.json
@@ -5,7 +5,8 @@
"outDir": "./target/types",
"emitDeclarationOnly": true,
"declaration": true,
- "declarationMap": true
+ "declarationMap": true,
+ "isolatedModules": true
},
"include": [
"public/**/*",
diff --git a/src/plugins/vis_default_editor/public/_default.scss b/src/plugins/vis_default_editor/public/_default.scss
index c412b9d915e553..56c6a0f0f63f6c 100644
--- a/src/plugins/vis_default_editor/public/_default.scss
+++ b/src/plugins/vis_default_editor/public/_default.scss
@@ -1,6 +1,4 @@
.visEditor--default {
- // height: 1px is in place to make editor children take their height in the parent
- height: 1px;
flex: 1 1 auto;
display: flex;
}
@@ -80,6 +78,7 @@
.visEditor__collapsibleSidebar {
width: 100% !important; // force the editor to take 100% width
+ flex-grow: 0;
}
.visEditor__collapsibleSidebar-isClosed {
@@ -91,8 +90,10 @@
}
.visEditor__visualization__wrapper {
- // force the visualization to take 100% width and height.
+ // force the visualization to take 100% width.
width: 100% !important;
- height: 100% !important;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
}
}
diff --git a/src/plugins/vis_default_editor/public/components/controls/field.test.tsx b/src/plugins/vis_default_editor/public/components/controls/field.test.tsx
index 94f767510c4bdd..277804567c2b7a 100644
--- a/src/plugins/vis_default_editor/public/components/controls/field.test.tsx
+++ b/src/plugins/vis_default_editor/public/components/controls/field.test.tsx
@@ -11,7 +11,7 @@ import { act } from 'react-dom/test-utils';
import { mount, shallow, ReactWrapper } from 'enzyme';
import { EuiComboBoxProps, EuiComboBox } from '@elastic/eui';
-import { IAggConfig, IndexPatternField } from 'src/plugins/data/public';
+import { IAggConfig, IndexPatternField, AggParam } from 'src/plugins/data/public';
import { ComboBoxGroupedOptions } from '../../utils';
import { FieldParamEditor, FieldParamEditorProps } from './field';
import { EditorVisState } from '../sidebar/state/reducers';
@@ -42,7 +42,7 @@ describe('FieldParamEditor component', () => {
setTouched = jest.fn();
onChange = jest.fn();
- field = { displayName: 'bytes' } as IndexPatternField;
+ field = { displayName: 'bytes', type: 'bytes' } as IndexPatternField;
option = { label: 'bytes', target: field };
indexedFields = [
{
@@ -52,7 +52,16 @@ describe('FieldParamEditor component', () => {
];
defaultProps = {
- agg: {} as IAggConfig,
+ agg: {
+ type: {
+ params: [
+ ({
+ name: 'field',
+ filterFieldTypes: ['bytes'],
+ } as unknown) as AggParam,
+ ],
+ },
+ } as IAggConfig,
aggParam: {
name: 'field',
type: 'field',
diff --git a/src/plugins/vis_default_editor/public/components/controls/field.tsx b/src/plugins/vis_default_editor/public/components/controls/field.tsx
index 95843dc6ae3a8f..f8db2d89888a2d 100644
--- a/src/plugins/vis_default_editor/public/components/controls/field.tsx
+++ b/src/plugins/vis_default_editor/public/components/controls/field.tsx
@@ -13,7 +13,13 @@ import useMount from 'react-use/lib/useMount';
import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { AggParam, IAggConfig, IFieldParamType, IndexPatternField } from 'src/plugins/data/public';
+import {
+ AggParam,
+ IAggConfig,
+ IFieldParamType,
+ IndexPatternField,
+ KBN_FIELD_TYPES,
+} from '../../../../../plugins/data/public';
import { formatListAsProse, parseCommaSeparatedList, useValidation } from './utils';
import { AggParamEditorProps } from '../agg_param_props';
import { ComboBoxGroupedOptions } from '../../utils';
@@ -55,6 +61,7 @@ function FieldParamEditor({
}
};
const errors = customError ? [customError] : [];
+ let showErrorMessageImmediately = false;
if (!indexedFields.length) {
errors.push(
@@ -69,9 +76,38 @@ function FieldParamEditor({
);
}
+ if (value && value.type === KBN_FIELD_TYPES.MISSING) {
+ errors.push(
+ i18n.translate('visDefaultEditor.controls.field.fieldIsNotExists', {
+ defaultMessage:
+ 'The field "{fieldParameter}" associated with this object no longer exists in the index pattern. Please use another field.',
+ values: {
+ fieldParameter: value.name,
+ },
+ })
+ );
+ showErrorMessageImmediately = true;
+ } else if (
+ value &&
+ !getFieldTypes(agg).find((type: string) => type === value.type || type === '*')
+ ) {
+ errors.push(
+ i18n.translate('visDefaultEditor.controls.field.invalidFieldForAggregation', {
+ defaultMessage:
+ 'Saved field "{fieldParameter}" of index pattern "{indexPatternTitle}" is invalid for use with this aggregation. Please select a new field.',
+ values: {
+ fieldParameter: value?.name,
+ indexPatternTitle: agg.getIndexPattern && agg.getIndexPattern().title,
+ },
+ })
+ );
+ showErrorMessageImmediately = true;
+ }
+
const isValid = !!value && !errors.length && !isDirty;
// we show an error message right away if there is no compatible fields
- const showErrorMessage = (showValidation || !indexedFields.length) && !isValid;
+ const showErrorMessage =
+ (showValidation || !indexedFields.length || showErrorMessageImmediately) && !isValid;
useValidation(setValidity, isValid);
useMount(() => {
@@ -122,10 +158,14 @@ function FieldParamEditor({
}
function getFieldTypesString(agg: IAggConfig) {
+ return formatListAsProse(getFieldTypes(agg), { inclusive: false });
+}
+
+function getFieldTypes(agg: IAggConfig) {
const param =
get(agg, 'type.params', []).find((p: AggParam) => p.name === 'field') ||
({} as IFieldParamType);
- return formatListAsProse(parseCommaSeparatedList(param.filterFieldTypes), { inclusive: false });
+ return parseCommaSeparatedList(param.filterFieldTypes || []);
}
export { FieldParamEditor };
diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts
index 429dabeeef0425..3bb52eb15758a0 100644
--- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts
+++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts
@@ -149,8 +149,9 @@ export class VisualizeEmbeddable
}
this.subscriptions.push(
- this.getUpdated$().subscribe(() => {
+ this.getUpdated$().subscribe((value) => {
const isDirty = this.handleChanges();
+
if (isDirty && this.handler) {
this.updateHandler();
}
@@ -367,8 +368,8 @@ export class VisualizeEmbeddable
}
}
- public reload = () => {
- this.handleVisUpdate();
+ public reload = async () => {
+ await this.handleVisUpdate();
};
private async updateHandler() {
@@ -395,13 +396,13 @@ export class VisualizeEmbeddable
});
if (this.handler && !abortController.signal.aborted) {
- this.handler.update(this.expression, expressionParams);
+ await this.handler.update(this.expression, expressionParams);
}
}
private handleVisUpdate = async () => {
this.handleChanges();
- this.updateHandler();
+ await this.updateHandler();
};
private uiStateChangeHandler = () => {
diff --git a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx
index 256e634ac6c40d..f6ef1caf9c9e01 100644
--- a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx
+++ b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx
@@ -183,8 +183,12 @@ const TopNav = ({
useEffect(() => {
const autoRefreshFetchSub = services.data.query.timefilter.timefilter
.getAutoRefreshFetch$()
- .subscribe(() => {
- visInstance.embeddableHandler.reload();
+ .subscribe(async (done) => {
+ try {
+ await visInstance.embeddableHandler.reload();
+ } finally {
+ done();
+ }
});
return () => {
autoRefreshFetchSub.unsubscribe();
diff --git a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts
index cc0f3ce2afae58..9eda709e58c3ed 100644
--- a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts
+++ b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts
@@ -18,8 +18,17 @@ import { SavedObject } from 'src/plugins/saved_objects/public';
import { cloneDeep } from 'lodash';
import { ExpressionValueError } from 'src/plugins/expressions/public';
import { createSavedSearchesLoader } from '../../../../discover/public';
+import { SavedFieldNotFound, SavedFieldTypeInvalidForAgg } from '../../../../kibana_utils/common';
import { VisualizeServices } from '../types';
+function isErrorRelatedToRuntimeFields(error: ExpressionValueError['error']) {
+ const originalError = error.original || error;
+ return (
+ originalError instanceof SavedFieldNotFound ||
+ originalError instanceof SavedFieldTypeInvalidForAgg
+ );
+}
+
const createVisualizeEmbeddableAndLinkSavedSearch = async (
vis: Vis,
visualizeServices: VisualizeServices
@@ -37,7 +46,7 @@ const createVisualizeEmbeddableAndLinkSavedSearch = async (
})) as VisualizeEmbeddableContract;
embeddableHandler.getOutput$().subscribe((output) => {
- if (output.error) {
+ if (output.error && !isErrorRelatedToRuntimeFields(output.error)) {
data.search.showError(
((output.error as unknown) as ExpressionValueError['error']).original || output.error
);
diff --git a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts
index 64d61996495d70..965951bfbd88da 100644
--- a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts
+++ b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts
@@ -11,13 +11,12 @@ import { EventEmitter } from 'events';
import { parse } from 'query-string';
import { i18n } from '@kbn/i18n';
-import { redirectWhenMissing } from '../../../../../kibana_utils/public';
-
import { getVisualizationInstance } from '../get_visualization_instance';
import { getEditBreadcrumbs, getCreateBreadcrumbs } from '../breadcrumbs';
import { SavedVisInstance, VisualizeServices, IEditorController } from '../../types';
import { VisualizeConstants } from '../../visualize_constants';
import { getVisEditorsRegistry } from '../../../services';
+import { redirectToSavedObjectPage } from '../utils';
/**
* This effect is responsible for instantiating a saved vis or creating a new one
@@ -43,9 +42,7 @@ export const useSavedVisInstance = (
chrome,
history,
dashboard,
- setActiveUrl,
toastNotifications,
- http: { basePath },
stateTransferService,
application: { navigateToApp },
} = services;
@@ -131,27 +128,8 @@ export const useSavedVisInstance = (
visEditorController,
});
} catch (error) {
- const managementRedirectTarget = {
- app: 'management',
- path: `kibana/objects/savedVisualizations/${visualizationIdFromUrl}`,
- };
-
try {
- redirectWhenMissing({
- history,
- navigateToApp,
- toastNotifications,
- basePath,
- mapping: {
- visualization: VisualizeConstants.LANDING_PAGE_PATH,
- search: managementRedirectTarget,
- 'index-pattern': managementRedirectTarget,
- 'index-pattern-field': managementRedirectTarget,
- },
- onBeforeRedirect() {
- setActiveUrl(VisualizeConstants.LANDING_PAGE_PATH);
- },
- })(error);
+ redirectToSavedObjectPage(services, error, visualizationIdFromUrl);
} catch (e) {
toastNotifications.addWarning({
title: i18n.translate('visualize.createVisualization.failedToLoadErrorMessage', {
diff --git a/src/plugins/visualize/public/application/utils/utils.ts b/src/plugins/visualize/public/application/utils/utils.ts
index 0e529507f97e3d..c906ff5304c909 100644
--- a/src/plugins/visualize/public/application/utils/utils.ts
+++ b/src/plugins/visualize/public/application/utils/utils.ts
@@ -10,6 +10,8 @@ import { i18n } from '@kbn/i18n';
import { ChromeStart, DocLinksStart } from 'kibana/public';
import { Filter } from '../../../../data/public';
+import { redirectWhenMissing } from '../../../../kibana_utils/public';
+import { VisualizeConstants } from '../visualize_constants';
import { VisualizeServices, VisualizeEditorVisInstance } from '../types';
export const addHelpMenuToAppChrome = (chrome: ChromeStart, docLinks: DocLinksStart) => {
@@ -58,3 +60,36 @@ export const visStateToEditorState = (
linked: savedVis && savedVis.id ? !!savedVis.savedSearchId : !!savedVisState.savedSearchId,
};
};
+
+export const redirectToSavedObjectPage = (
+ services: VisualizeServices,
+ error: any,
+ savedVisualizationsId?: string
+) => {
+ const {
+ history,
+ setActiveUrl,
+ toastNotifications,
+ http: { basePath },
+ application: { navigateToApp },
+ } = services;
+ const managementRedirectTarget = {
+ app: 'management',
+ path: `kibana/objects/savedVisualizations/${savedVisualizationsId}`,
+ };
+ redirectWhenMissing({
+ history,
+ navigateToApp,
+ toastNotifications,
+ basePath,
+ mapping: {
+ visualization: VisualizeConstants.LANDING_PAGE_PATH,
+ search: managementRedirectTarget,
+ 'index-pattern': managementRedirectTarget,
+ 'index-pattern-field': managementRedirectTarget,
+ },
+ onBeforeRedirect() {
+ setActiveUrl(VisualizeConstants.LANDING_PAGE_PATH);
+ },
+ })(error);
+};
diff --git a/x-pack/examples/alerting_example/server/plugin.ts b/x-pack/examples/alerting_example/server/plugin.ts
index db9c996147c94b..f6131679874db2 100644
--- a/x-pack/examples/alerting_example/server/plugin.ts
+++ b/x-pack/examples/alerting_example/server/plugin.ts
@@ -33,7 +33,7 @@ export class AlertingExamplePlugin implements Plugin {
preconfigured: {},
proxyRejectUnauthorizedCertificates: true,
rejectUnauthorized: true,
+ proxyBypassHosts: undefined,
+ proxyOnlyHosts: undefined,
+ maxResponseContentLength: new ByteSizeValue(1000000),
+ responseTimeout: moment.duration('60s'),
});
const localActionTypeRegistryParams = {
diff --git a/x-pack/plugins/actions/server/actions_config.mock.ts b/x-pack/plugins/actions/server/actions_config.mock.ts
index 012cd63be2702d..76f6a62ce65974 100644
--- a/x-pack/plugins/actions/server/actions_config.mock.ts
+++ b/x-pack/plugins/actions/server/actions_config.mock.ts
@@ -17,6 +17,10 @@ const createActionsConfigMock = () => {
ensureActionTypeEnabled: jest.fn().mockReturnValue({}),
isRejectUnauthorizedCertificatesEnabled: jest.fn().mockReturnValue(true),
getProxySettings: jest.fn().mockReturnValue(undefined),
+ getResponseSettings: jest.fn().mockReturnValue({
+ maxContentLength: 1000000,
+ timeout: 360000,
+ }),
};
return mocked;
};
diff --git a/x-pack/plugins/actions/server/actions_config.test.ts b/x-pack/plugins/actions/server/actions_config.test.ts
index cae6777a82441f..c81f1f4a4bf2e4 100644
--- a/x-pack/plugins/actions/server/actions_config.test.ts
+++ b/x-pack/plugins/actions/server/actions_config.test.ts
@@ -5,12 +5,14 @@
* 2.0.
*/
+import { ByteSizeValue } from '@kbn/config-schema';
import { ActionsConfig } from './config';
import {
getActionsConfigurationUtilities,
AllowedHosts,
EnabledActionTypes,
} from './actions_config';
+import moment from 'moment';
const defaultActionsConfig: ActionsConfig = {
enabled: false,
@@ -19,6 +21,8 @@ const defaultActionsConfig: ActionsConfig = {
preconfigured: {},
proxyRejectUnauthorizedCertificates: true,
rejectUnauthorized: true,
+ maxResponseContentLength: new ByteSizeValue(1000000),
+ responseTimeout: moment.duration(60000),
};
describe('ensureUriAllowed', () => {
@@ -253,3 +257,94 @@ describe('ensureActionTypeEnabled', () => {
expect(getActionsConfigurationUtilities(config).ensureActionTypeEnabled('foo')).toBeUndefined();
});
});
+
+describe('getResponseSettingsFromConfig', () => {
+ test('returns expected parsed values for default config for responseTimeout and maxResponseContentLength', () => {
+ const config: ActionsConfig = {
+ ...defaultActionsConfig,
+ };
+ expect(getActionsConfigurationUtilities(config).getResponseSettings()).toEqual({
+ timeout: 60000,
+ maxContentLength: 1000000,
+ });
+ });
+});
+
+describe('getProxySettings', () => {
+ test('returns undefined when no proxy URL set', () => {
+ const config: ActionsConfig = {
+ ...defaultActionsConfig,
+ proxyHeaders: { someHeaderName: 'some header value' },
+ proxyBypassHosts: ['avoid-proxy.co'],
+ };
+
+ const proxySettings = getActionsConfigurationUtilities(config).getProxySettings();
+ expect(proxySettings).toBeUndefined();
+ });
+
+ test('returns proxy url', () => {
+ const config: ActionsConfig = {
+ ...defaultActionsConfig,
+ proxyUrl: 'https://proxy.elastic.co',
+ };
+ const proxySettings = getActionsConfigurationUtilities(config).getProxySettings();
+ expect(proxySettings?.proxyUrl).toBe(config.proxyUrl);
+ });
+
+ test('returns proxyRejectUnauthorizedCertificates', () => {
+ const configTrue: ActionsConfig = {
+ ...defaultActionsConfig,
+ proxyUrl: 'https://proxy.elastic.co',
+ proxyRejectUnauthorizedCertificates: true,
+ };
+ let proxySettings = getActionsConfigurationUtilities(configTrue).getProxySettings();
+ expect(proxySettings?.proxyRejectUnauthorizedCertificates).toBe(true);
+
+ const configFalse: ActionsConfig = {
+ ...defaultActionsConfig,
+ proxyUrl: 'https://proxy.elastic.co',
+ proxyRejectUnauthorizedCertificates: false,
+ };
+ proxySettings = getActionsConfigurationUtilities(configFalse).getProxySettings();
+ expect(proxySettings?.proxyRejectUnauthorizedCertificates).toBe(false);
+ });
+
+ test('returns proxy headers', () => {
+ const proxyHeaders = {
+ someHeaderName: 'some header value',
+ someOtherHeader: 'some other header',
+ };
+ const config: ActionsConfig = {
+ ...defaultActionsConfig,
+ proxyUrl: 'https://proxy.elastic.co',
+ proxyHeaders,
+ };
+
+ const proxySettings = getActionsConfigurationUtilities(config).getProxySettings();
+ expect(proxySettings?.proxyHeaders).toEqual(config.proxyHeaders);
+ });
+
+ test('returns proxy bypass hosts', () => {
+ const proxyBypassHosts = ['proxy-bypass-1.elastic.co', 'proxy-bypass-2.elastic.co'];
+ const config: ActionsConfig = {
+ ...defaultActionsConfig,
+ proxyUrl: 'https://proxy.elastic.co',
+ proxyBypassHosts,
+ };
+
+ const proxySettings = getActionsConfigurationUtilities(config).getProxySettings();
+ expect(proxySettings?.proxyBypassHosts).toEqual(new Set(proxyBypassHosts));
+ });
+
+ test('returns proxy only hosts', () => {
+ const proxyOnlyHosts = ['proxy-only-1.elastic.co', 'proxy-only-2.elastic.co'];
+ const config: ActionsConfig = {
+ ...defaultActionsConfig,
+ proxyUrl: 'https://proxy.elastic.co',
+ proxyOnlyHosts,
+ };
+
+ const proxySettings = getActionsConfigurationUtilities(config).getProxySettings();
+ expect(proxySettings?.proxyOnlyHosts).toEqual(new Set(proxyOnlyHosts));
+ });
+});
diff --git a/x-pack/plugins/actions/server/actions_config.ts b/x-pack/plugins/actions/server/actions_config.ts
index 2787f8f9711015..4c73cab76f9e8c 100644
--- a/x-pack/plugins/actions/server/actions_config.ts
+++ b/x-pack/plugins/actions/server/actions_config.ts
@@ -11,17 +11,11 @@ import url from 'url';
import { curry } from 'lodash';
import { pipe } from 'fp-ts/lib/pipeable';
-import { ActionsConfig } from './config';
+import { ActionsConfig, AllowedHosts, EnabledActionTypes } from './config';
import { ActionTypeDisabledError } from './lib';
-import { ProxySettings } from './types';
+import { ProxySettings, ResponseSettings } from './types';
-export enum AllowedHosts {
- Any = '*',
-}
-
-export enum EnabledActionTypes {
- Any = '*',
-}
+export { AllowedHosts, EnabledActionTypes } from './config';
enum AllowListingField {
URL = 'url',
@@ -37,6 +31,7 @@ export interface ActionsConfigurationUtilities {
ensureActionTypeEnabled: (actionType: string) => void;
isRejectUnauthorizedCertificatesEnabled: () => boolean;
getProxySettings: () => undefined | ProxySettings;
+ getResponseSettings: () => ResponseSettings;
}
function allowListErrorMessage(field: AllowListingField, value: string) {
@@ -93,11 +88,25 @@ function getProxySettingsFromConfig(config: ActionsConfig): undefined | ProxySet
return {
proxyUrl: config.proxyUrl,
+ proxyBypassHosts: arrayAsSet(config.proxyBypassHosts),
+ proxyOnlyHosts: arrayAsSet(config.proxyOnlyHosts),
proxyHeaders: config.proxyHeaders,
proxyRejectUnauthorizedCertificates: config.proxyRejectUnauthorizedCertificates,
};
}
+function arrayAsSet(arr: T[] | undefined): Set | undefined {
+ if (!arr) return;
+ return new Set(arr);
+}
+
+function getResponseSettingsFromConfig(config: ActionsConfig): ResponseSettings {
+ return {
+ maxContentLength: config.maxResponseContentLength.getValueInBytes(),
+ timeout: config.responseTimeout.asMilliseconds(),
+ };
+}
+
export function getActionsConfigurationUtilities(
config: ActionsConfig
): ActionsConfigurationUtilities {
@@ -109,6 +118,7 @@ export function getActionsConfigurationUtilities(
isUriAllowed,
isActionTypeEnabled,
getProxySettings: () => getProxySettingsFromConfig(config),
+ getResponseSettings: () => getResponseSettingsFromConfig(config),
isRejectUnauthorizedCertificatesEnabled: () => config.rejectUnauthorized,
ensureUriAllowed(uri: string) {
if (!isUriAllowed(uri)) {
diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts
index b858d5491a6bd1..4596619c509405 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts
@@ -283,6 +283,7 @@ describe('execute()', () => {
"ensureHostnameAllowed": [MockFunction],
"ensureUriAllowed": [MockFunction],
"getProxySettings": [MockFunction],
+ "getResponseSettings": [MockFunction],
"isActionTypeEnabled": [MockFunction],
"isHostnameAllowed": [MockFunction],
"isRejectUnauthorizedCertificatesEnabled": [MockFunction],
@@ -342,6 +343,7 @@ describe('execute()', () => {
"ensureHostnameAllowed": [MockFunction],
"ensureUriAllowed": [MockFunction],
"getProxySettings": [MockFunction],
+ "getResponseSettings": [MockFunction],
"isActionTypeEnabled": [MockFunction],
"isHostnameAllowed": [MockFunction],
"isRejectUnauthorizedCertificatesEnabled": [MockFunction],
diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts
index 6a67f4f6752c25..edc9429e4fac6a 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts
@@ -7,12 +7,16 @@
import axios from 'axios';
import { Agent as HttpsAgent } from 'https';
+import HttpProxyAgent from 'http-proxy-agent';
+import { HttpsProxyAgent } from 'https-proxy-agent';
import { Logger } from '../../../../../../src/core/server';
import { addTimeZoneToDate, request, patch, getErrorMessage } from './axios_utils';
import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
import { actionsConfigMock } from '../../actions_config.mock';
import { getCustomAgents } from './get_custom_agents';
+const TestUrl = 'https://elastic.co/foo/bar/baz';
+
const logger = loggingSystemMock.create().get() as jest.Mocked;
const configurationUtilities = actionsConfigMock.create();
jest.mock('axios');
@@ -38,6 +42,10 @@ describe('request', () => {
headers: { 'content-type': 'application/json' },
data: { incidentId: '123' },
}));
+ configurationUtilities.getResponseSettings.mockReturnValue({
+ maxContentLength: 1000000,
+ timeout: 360000,
+ });
});
test('it fetch correctly with defaults', async () => {
@@ -54,6 +62,8 @@ describe('request', () => {
httpAgent: undefined,
httpsAgent: expect.any(HttpsAgent),
proxy: false,
+ maxContentLength: 1000000,
+ timeout: 360000,
});
expect(res).toEqual({
status: 200,
@@ -66,22 +76,26 @@ describe('request', () => {
configurationUtilities.getProxySettings.mockReturnValue({
proxyRejectUnauthorizedCertificates: true,
proxyUrl: 'https://localhost:1212',
+ proxyBypassHosts: undefined,
+ proxyOnlyHosts: undefined,
});
- const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger);
+ const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, TestUrl);
const res = await request({
axios,
- url: 'http://testProxy',
+ url: TestUrl,
logger,
configurationUtilities,
});
- expect(axiosMock).toHaveBeenCalledWith('http://testProxy', {
+ expect(axiosMock).toHaveBeenCalledWith(TestUrl, {
method: 'get',
data: {},
httpAgent,
httpsAgent,
proxy: false,
+ maxContentLength: 1000000,
+ timeout: 360000,
});
expect(res).toEqual({
status: 200,
@@ -94,6 +108,8 @@ describe('request', () => {
configurationUtilities.getProxySettings.mockReturnValue({
proxyUrl: ':nope:',
proxyRejectUnauthorizedCertificates: false,
+ proxyBypassHosts: undefined,
+ proxyOnlyHosts: undefined,
});
const res = await request({
axios,
@@ -108,6 +124,8 @@ describe('request', () => {
httpAgent: undefined,
httpsAgent: expect.any(HttpsAgent),
proxy: false,
+ maxContentLength: 1000000,
+ timeout: 360000,
});
expect(res).toEqual({
status: 200,
@@ -116,6 +134,90 @@ describe('request', () => {
});
});
+ test('it bypasses with proxyBypassHosts when expected', async () => {
+ configurationUtilities.getProxySettings.mockReturnValue({
+ proxyRejectUnauthorizedCertificates: true,
+ proxyUrl: 'https://elastic.proxy.co',
+ proxyBypassHosts: new Set(['elastic.co']),
+ proxyOnlyHosts: undefined,
+ });
+
+ await request({
+ axios,
+ url: TestUrl,
+ logger,
+ configurationUtilities,
+ });
+
+ expect(axiosMock.mock.calls.length).toBe(1);
+ const { httpAgent, httpsAgent } = axiosMock.mock.calls[0][1];
+ expect(httpAgent instanceof HttpProxyAgent).toBe(false);
+ expect(httpsAgent instanceof HttpsProxyAgent).toBe(false);
+ });
+
+ test('it does not bypass with proxyBypassHosts when expected', async () => {
+ configurationUtilities.getProxySettings.mockReturnValue({
+ proxyRejectUnauthorizedCertificates: true,
+ proxyUrl: 'https://elastic.proxy.co',
+ proxyBypassHosts: new Set(['not-elastic.co']),
+ proxyOnlyHosts: undefined,
+ });
+
+ await request({
+ axios,
+ url: TestUrl,
+ logger,
+ configurationUtilities,
+ });
+
+ expect(axiosMock.mock.calls.length).toBe(1);
+ const { httpAgent, httpsAgent } = axiosMock.mock.calls[0][1];
+ expect(httpAgent instanceof HttpProxyAgent).toBe(true);
+ expect(httpsAgent instanceof HttpsProxyAgent).toBe(true);
+ });
+
+ test('it proxies with proxyOnlyHosts when expected', async () => {
+ configurationUtilities.getProxySettings.mockReturnValue({
+ proxyRejectUnauthorizedCertificates: true,
+ proxyUrl: 'https://elastic.proxy.co',
+ proxyBypassHosts: undefined,
+ proxyOnlyHosts: new Set(['elastic.co']),
+ });
+
+ await request({
+ axios,
+ url: TestUrl,
+ logger,
+ configurationUtilities,
+ });
+
+ expect(axiosMock.mock.calls.length).toBe(1);
+ const { httpAgent, httpsAgent } = axiosMock.mock.calls[0][1];
+ expect(httpAgent instanceof HttpProxyAgent).toBe(true);
+ expect(httpsAgent instanceof HttpsProxyAgent).toBe(true);
+ });
+
+ test('it does not proxy with proxyOnlyHosts when expected', async () => {
+ configurationUtilities.getProxySettings.mockReturnValue({
+ proxyRejectUnauthorizedCertificates: true,
+ proxyUrl: 'https://elastic.proxy.co',
+ proxyBypassHosts: undefined,
+ proxyOnlyHosts: new Set(['not-elastic.co']),
+ });
+
+ await request({
+ axios,
+ url: TestUrl,
+ logger,
+ configurationUtilities,
+ });
+
+ expect(axiosMock.mock.calls.length).toBe(1);
+ const { httpAgent, httpsAgent } = axiosMock.mock.calls[0][1];
+ expect(httpAgent instanceof HttpProxyAgent).toBe(false);
+ expect(httpsAgent instanceof HttpsProxyAgent).toBe(false);
+ });
+
test('it fetch correctly', async () => {
const res = await request({
axios,
@@ -132,6 +234,8 @@ describe('request', () => {
httpAgent: undefined,
httpsAgent: expect.any(HttpsAgent),
proxy: false,
+ maxContentLength: 1000000,
+ timeout: 360000,
});
expect(res).toEqual({
status: 200,
@@ -143,10 +247,15 @@ describe('request', () => {
describe('patch', () => {
beforeEach(() => {
+ jest.resetAllMocks();
axiosMock.mockImplementation(() => ({
status: 200,
headers: { 'content-type': 'application/json' },
}));
+ configurationUtilities.getResponseSettings.mockReturnValue({
+ maxContentLength: 1000000,
+ timeout: 360000,
+ });
});
test('it fetch correctly', async () => {
@@ -157,6 +266,8 @@ describe('patch', () => {
httpAgent: undefined,
httpsAgent: expect.any(HttpsAgent),
proxy: false,
+ maxContentLength: 1000000,
+ timeout: 360000,
});
});
});
diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts
index f86f3b86c506af..af353e1d1da5aa 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts
@@ -30,7 +30,8 @@ export const request = async ({
validateStatus?: (status: number) => boolean;
auth?: AxiosBasicCredentials;
}): Promise => {
- const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger);
+ const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, url);
+ const { maxContentLength, timeout } = configurationUtilities.getResponseSettings();
return await axios(url, {
...rest,
@@ -40,6 +41,8 @@ export const request = async ({
httpAgent,
httpsAgent,
proxy: false,
+ maxContentLength,
+ timeout,
});
};
diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.test.ts
index 340ac0f6dda3a2..f6d1be9bffc6b6 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.test.ts
@@ -14,6 +14,10 @@ import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
import { actionsConfigMock } from '../../actions_config.mock';
const logger = loggingSystemMock.create().get() as jest.Mocked;
+const targetHost = 'elastic.co';
+const targetUrl = `https://${targetHost}/foo/bar/baz`;
+const nonMatchingUrl = `https://${targetHost}m/foo/bar/baz`;
+
describe('getCustomAgents', () => {
const configurationUtilities = actionsConfigMock.create();
@@ -21,8 +25,10 @@ describe('getCustomAgents', () => {
configurationUtilities.getProxySettings.mockReturnValue({
proxyUrl: 'https://someproxyhost',
proxyRejectUnauthorizedCertificates: false,
+ proxyBypassHosts: undefined,
+ proxyOnlyHosts: undefined,
});
- const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger);
+ const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, targetUrl);
expect(httpAgent instanceof HttpProxyAgent).toBeTruthy();
expect(httpsAgent instanceof HttpsProxyAgent).toBeTruthy();
});
@@ -31,15 +37,73 @@ describe('getCustomAgents', () => {
configurationUtilities.getProxySettings.mockReturnValue({
proxyUrl: ':nope: not a valid URL',
proxyRejectUnauthorizedCertificates: false,
+ proxyBypassHosts: undefined,
+ proxyOnlyHosts: undefined,
});
- const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger);
+ const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, targetUrl);
expect(httpAgent).toBe(undefined);
expect(httpsAgent instanceof HttpsAgent).toBeTruthy();
});
test('return default agents for undefined proxy options', () => {
- const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger);
+ const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, targetUrl);
expect(httpAgent).toBe(undefined);
expect(httpsAgent instanceof HttpsAgent).toBeTruthy();
});
+
+ test('returns non-proxy agents for matching proxyBypassHosts', () => {
+ configurationUtilities.getProxySettings.mockReturnValue({
+ proxyUrl: 'https://someproxyhost',
+ proxyRejectUnauthorizedCertificates: false,
+ proxyBypassHosts: new Set([targetHost]),
+ proxyOnlyHosts: undefined,
+ });
+ const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, targetUrl);
+ expect(httpAgent instanceof HttpProxyAgent).toBeFalsy();
+ expect(httpsAgent instanceof HttpsProxyAgent).toBeFalsy();
+ });
+
+ test('returns proxy agents for non-matching proxyBypassHosts', () => {
+ configurationUtilities.getProxySettings.mockReturnValue({
+ proxyUrl: 'https://someproxyhost',
+ proxyRejectUnauthorizedCertificates: false,
+ proxyBypassHosts: new Set([targetHost]),
+ proxyOnlyHosts: undefined,
+ });
+ const { httpAgent, httpsAgent } = getCustomAgents(
+ configurationUtilities,
+ logger,
+ nonMatchingUrl
+ );
+ expect(httpAgent instanceof HttpProxyAgent).toBeTruthy();
+ expect(httpsAgent instanceof HttpsProxyAgent).toBeTruthy();
+ });
+
+ test('returns proxy agents for matching proxyOnlyHosts', () => {
+ configurationUtilities.getProxySettings.mockReturnValue({
+ proxyUrl: 'https://someproxyhost',
+ proxyRejectUnauthorizedCertificates: false,
+ proxyBypassHosts: undefined,
+ proxyOnlyHosts: new Set([targetHost]),
+ });
+ const { httpAgent, httpsAgent } = getCustomAgents(configurationUtilities, logger, targetUrl);
+ expect(httpAgent instanceof HttpProxyAgent).toBeTruthy();
+ expect(httpsAgent instanceof HttpsProxyAgent).toBeTruthy();
+ });
+
+ test('returns non-proxy agents for non-matching proxyOnlyHosts', () => {
+ configurationUtilities.getProxySettings.mockReturnValue({
+ proxyUrl: 'https://someproxyhost',
+ proxyRejectUnauthorizedCertificates: false,
+ proxyBypassHosts: undefined,
+ proxyOnlyHosts: new Set([targetHost]),
+ });
+ const { httpAgent, httpsAgent } = getCustomAgents(
+ configurationUtilities,
+ logger,
+ nonMatchingUrl
+ );
+ expect(httpAgent instanceof HttpProxyAgent).toBeFalsy();
+ expect(httpsAgent instanceof HttpsProxyAgent).toBeFalsy();
+ });
});
diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.ts
index 92ababf830aa73..ff2d005f4d8415 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.ts
@@ -19,7 +19,8 @@ interface GetCustomAgentsResponse {
export function getCustomAgents(
configurationUtilities: ActionsConfigurationUtilities,
- logger: Logger
+ logger: Logger,
+ url: string
): GetCustomAgentsResponse {
const proxySettings = configurationUtilities.getProxySettings();
const defaultAgents = {
@@ -33,6 +34,28 @@ export function getCustomAgents(
return defaultAgents;
}
+ let targetUrl: URL;
+ try {
+ targetUrl = new URL(url);
+ } catch (err) {
+ logger.warn(`error determining proxy state for invalid url "${url}", using default agents`);
+ return defaultAgents;
+ }
+
+ // filter out hostnames in the proxy bypass or only lists
+ const { hostname } = targetUrl;
+
+ if (proxySettings.proxyBypassHosts) {
+ if (proxySettings.proxyBypassHosts.has(hostname)) {
+ return defaultAgents;
+ }
+ }
+
+ if (proxySettings.proxyOnlyHosts) {
+ if (!proxySettings.proxyOnlyHosts.has(hostname)) {
+ return defaultAgents;
+ }
+ }
logger.debug(`Creating proxy agents for proxy: ${proxySettings.proxyUrl}`);
let proxyUrl: URL;
try {
diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts
index cc3f03f50c36fe..4b45c6d787cd6b 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts
@@ -76,6 +76,8 @@ describe('send_email module', () => {
{
proxyUrl: 'https://example.com',
proxyRejectUnauthorizedCertificates: false,
+ proxyBypassHosts: undefined,
+ proxyOnlyHosts: undefined,
}
);
@@ -222,6 +224,138 @@ describe('send_email module', () => {
await expect(sendEmail(mockLogger, sendEmailOptions)).rejects.toThrow('wops');
});
+
+ test('it bypasses with proxyBypassHosts when expected', async () => {
+ const sendEmailOptions = getSendEmailOptionsNoAuth(
+ {
+ transport: {
+ host: 'example.com',
+ port: 1025,
+ },
+ },
+ {
+ proxyUrl: 'https://proxy.com',
+ proxyRejectUnauthorizedCertificates: false,
+ proxyBypassHosts: new Set(['example.com']),
+ proxyOnlyHosts: undefined,
+ }
+ );
+
+ const result = await sendEmail(mockLogger, sendEmailOptions);
+ expect(result).toBe(sendMailMockResult);
+ expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "host": "example.com",
+ "port": 1025,
+ "secure": false,
+ "tls": Object {
+ "rejectUnauthorized": false,
+ },
+ },
+ ]
+ `);
+ });
+
+ test('it does not bypass with proxyBypassHosts when expected', async () => {
+ const sendEmailOptions = getSendEmailOptionsNoAuth(
+ {
+ transport: {
+ host: 'example.com',
+ port: 1025,
+ },
+ },
+ {
+ proxyUrl: 'https://proxy.com',
+ proxyRejectUnauthorizedCertificates: false,
+ proxyBypassHosts: new Set(['not-example.com']),
+ proxyOnlyHosts: undefined,
+ }
+ );
+
+ const result = await sendEmail(mockLogger, sendEmailOptions);
+ expect(result).toBe(sendMailMockResult);
+ expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "headers": undefined,
+ "host": "example.com",
+ "port": 1025,
+ "proxy": "https://proxy.com",
+ "secure": false,
+ "tls": Object {
+ "rejectUnauthorized": false,
+ },
+ },
+ ]
+ `);
+ });
+
+ test('it proxies with proxyOnlyHosts when expected', async () => {
+ const sendEmailOptions = getSendEmailOptionsNoAuth(
+ {
+ transport: {
+ host: 'example.com',
+ port: 1025,
+ },
+ },
+ {
+ proxyUrl: 'https://proxy.com',
+ proxyRejectUnauthorizedCertificates: false,
+ proxyBypassHosts: undefined,
+ proxyOnlyHosts: new Set(['example.com']),
+ }
+ );
+
+ const result = await sendEmail(mockLogger, sendEmailOptions);
+ expect(result).toBe(sendMailMockResult);
+ expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "headers": undefined,
+ "host": "example.com",
+ "port": 1025,
+ "proxy": "https://proxy.com",
+ "secure": false,
+ "tls": Object {
+ "rejectUnauthorized": false,
+ },
+ },
+ ]
+ `);
+ });
+
+ test('it does not proxy with proxyOnlyHosts when expected', async () => {
+ const sendEmailOptions = getSendEmailOptionsNoAuth(
+ {
+ transport: {
+ host: 'example.com',
+ port: 1025,
+ },
+ },
+ {
+ proxyUrl: 'https://proxy.com',
+ proxyRejectUnauthorizedCertificates: false,
+ proxyBypassHosts: undefined,
+ proxyOnlyHosts: new Set(['not-example.com']),
+ }
+ );
+
+ const result = await sendEmail(mockLogger, sendEmailOptions);
+ expect(result).toBe(sendMailMockResult);
+ expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "host": "example.com",
+ "port": 1025,
+ "secure": false,
+ "tls": Object {
+ "rejectUnauthorized": false,
+ },
+ },
+ ]
+ `);
+ });
});
function getSendEmailOptions(
diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts
index d4905015f7663b..c0a254967b4fed 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts
@@ -63,6 +63,17 @@ export async function sendEmail(logger: Logger, options: SendEmailOptions): Prom
};
}
+ let useProxy = !!proxySettings;
+
+ if (host) {
+ if (proxySettings?.proxyBypassHosts && proxySettings?.proxyBypassHosts?.has(host)) {
+ useProxy = false;
+ }
+ if (proxySettings?.proxyOnlyHosts && !proxySettings?.proxyOnlyHosts?.has(host)) {
+ useProxy = false;
+ }
+ }
+
if (service === JSON_TRANSPORT_SERVICE) {
transportConfig.jsonTransport = true;
delete transportConfig.auth;
@@ -73,7 +84,7 @@ export async function sendEmail(logger: Logger, options: SendEmailOptions): Prom
transportConfig.port = port;
transportConfig.secure = !!secure;
- if (proxySettings) {
+ if (proxySettings && useProxy) {
transportConfig.tls = {
// do not fail on invalid certs if value is false
rejectUnauthorized: proxySettings?.proxyRejectUnauthorizedCertificates,
diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts
index 6479e29b5a76ff..76612696e8e583 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts
@@ -195,6 +195,8 @@ describe('execute()', () => {
configurationUtilities.getProxySettings.mockReturnValue({
proxyUrl: 'https://someproxyhost',
proxyRejectUnauthorizedCertificates: false,
+ proxyBypassHosts: undefined,
+ proxyOnlyHosts: undefined,
});
const actionTypeProxy = getActionType({
logger: mockedLogger,
@@ -212,6 +214,106 @@ describe('execute()', () => {
);
});
+ test('ensure proxy bypass will bypass when expected', async () => {
+ mockedLogger.debug.mockReset();
+ const configurationUtilities = actionsConfigMock.create();
+ configurationUtilities.getProxySettings.mockReturnValue({
+ proxyUrl: 'https://someproxyhost',
+ proxyRejectUnauthorizedCertificates: false,
+ proxyBypassHosts: new Set(['example.com']),
+ proxyOnlyHosts: undefined,
+ });
+ const actionTypeProxy = getActionType({
+ logger: mockedLogger,
+ configurationUtilities,
+ });
+ await actionTypeProxy.executor({
+ actionId: 'some-id',
+ services,
+ config: {},
+ secrets: { webhookUrl: 'http://example.com' },
+ params: { message: 'this invocation should succeed' },
+ });
+ expect(mockedLogger.debug).not.toHaveBeenCalledWith(
+ 'IncomingWebhook was called with proxyUrl https://someproxyhost'
+ );
+ });
+
+ test('ensure proxy bypass will not bypass when expected', async () => {
+ mockedLogger.debug.mockReset();
+ const configurationUtilities = actionsConfigMock.create();
+ configurationUtilities.getProxySettings.mockReturnValue({
+ proxyUrl: 'https://someproxyhost',
+ proxyRejectUnauthorizedCertificates: false,
+ proxyBypassHosts: new Set(['not-example.com']),
+ proxyOnlyHosts: undefined,
+ });
+ const actionTypeProxy = getActionType({
+ logger: mockedLogger,
+ configurationUtilities,
+ });
+ await actionTypeProxy.executor({
+ actionId: 'some-id',
+ services,
+ config: {},
+ secrets: { webhookUrl: 'http://example.com' },
+ params: { message: 'this invocation should succeed' },
+ });
+ expect(mockedLogger.debug).toHaveBeenCalledWith(
+ 'IncomingWebhook was called with proxyUrl https://someproxyhost'
+ );
+ });
+
+ test('ensure proxy only will proxy when expected', async () => {
+ mockedLogger.debug.mockReset();
+ const configurationUtilities = actionsConfigMock.create();
+ configurationUtilities.getProxySettings.mockReturnValue({
+ proxyUrl: 'https://someproxyhost',
+ proxyRejectUnauthorizedCertificates: false,
+ proxyBypassHosts: undefined,
+ proxyOnlyHosts: new Set(['example.com']),
+ });
+ const actionTypeProxy = getActionType({
+ logger: mockedLogger,
+ configurationUtilities,
+ });
+ await actionTypeProxy.executor({
+ actionId: 'some-id',
+ services,
+ config: {},
+ secrets: { webhookUrl: 'http://example.com' },
+ params: { message: 'this invocation should succeed' },
+ });
+ expect(mockedLogger.debug).toHaveBeenCalledWith(
+ 'IncomingWebhook was called with proxyUrl https://someproxyhost'
+ );
+ });
+
+ test('ensure proxy only will not proxy when expected', async () => {
+ mockedLogger.debug.mockReset();
+ const configurationUtilities = actionsConfigMock.create();
+ configurationUtilities.getProxySettings.mockReturnValue({
+ proxyUrl: 'https://someproxyhost',
+ proxyRejectUnauthorizedCertificates: false,
+ proxyBypassHosts: undefined,
+ proxyOnlyHosts: new Set(['not-example.com']),
+ });
+ const actionTypeProxy = getActionType({
+ logger: mockedLogger,
+ configurationUtilities,
+ });
+ await actionTypeProxy.executor({
+ actionId: 'some-id',
+ services,
+ config: {},
+ secrets: { webhookUrl: 'http://example.com' },
+ params: { message: 'this invocation should succeed' },
+ });
+ expect(mockedLogger.debug).not.toHaveBeenCalledWith(
+ 'IncomingWebhook was called with proxyUrl https://someproxyhost'
+ );
+ });
+
test('renders parameter templates as expected', async () => {
expect(actionType.renderParameterTemplates).toBeTruthy();
const paramsWithTemplates = {
diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.ts
index a6173229e3267a..d0fb4a8c4b9359 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/slack.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/slack.ts
@@ -7,6 +7,8 @@
import { URL } from 'url';
import { curry } from 'lodash';
+import HttpProxyAgent from 'http-proxy-agent';
+import { HttpsProxyAgent } from 'https-proxy-agent';
import { i18n } from '@kbn/i18n';
import { schema, TypeOf } from '@kbn/config-schema';
import { IncomingWebhook, IncomingWebhookResult } from '@slack/webhook';
@@ -131,13 +133,15 @@ async function slackExecutor(
const { message } = params;
const proxySettings = configurationUtilities.getProxySettings();
- const customAgents = getCustomAgents(configurationUtilities, logger);
+ const customAgents = getCustomAgents(configurationUtilities, logger, webhookUrl);
const agent = webhookUrl.toLowerCase().startsWith('https')
? customAgents.httpsAgent
: customAgents.httpAgent;
if (proxySettings) {
- logger.debug(`IncomingWebhook was called with proxyUrl ${proxySettings.proxyUrl}`);
+ if (agent instanceof HttpProxyAgent || agent instanceof HttpsProxyAgent) {
+ logger.debug(`IncomingWebhook was called with proxyUrl ${proxySettings.proxyUrl}`);
+ }
}
try {
diff --git a/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts b/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts
index c31adddc5a57e7..8a185d353de021 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts
@@ -168,6 +168,7 @@ describe('execute()', () => {
"ensureHostnameAllowed": [MockFunction],
"ensureUriAllowed": [MockFunction],
"getProxySettings": [MockFunction],
+ "getResponseSettings": [MockFunction],
"isActionTypeEnabled": [MockFunction],
"isHostnameAllowed": [MockFunction],
"isRejectUnauthorizedCertificatesEnabled": [MockFunction],
@@ -230,6 +231,7 @@ describe('execute()', () => {
"ensureHostnameAllowed": [MockFunction],
"ensureUriAllowed": [MockFunction],
"getProxySettings": [MockFunction],
+ "getResponseSettings": [MockFunction],
"isActionTypeEnabled": [MockFunction],
"isHostnameAllowed": [MockFunction],
"isRejectUnauthorizedCertificatesEnabled": [MockFunction],
diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts
index c4684532478099..d3f059eede6157 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts
@@ -291,6 +291,7 @@ describe('execute()', () => {
"ensureHostnameAllowed": [MockFunction],
"ensureUriAllowed": [MockFunction],
"getProxySettings": [MockFunction],
+ "getResponseSettings": [MockFunction],
"isActionTypeEnabled": [MockFunction],
"isHostnameAllowed": [MockFunction],
"isRejectUnauthorizedCertificatesEnabled": [MockFunction],
@@ -329,6 +330,33 @@ describe('execute()', () => {
`);
});
+ test('execute with exception maxContentLength size exceeded should log the proper error', async () => {
+ const config: ActionTypeConfigType = {
+ url: 'https://abc.def/my-webhook',
+ method: WebhookMethods.POST,
+ headers: {
+ aheader: 'a value',
+ },
+ hasAuth: true,
+ };
+ requestMock.mockReset();
+ requestMock.mockRejectedValueOnce({
+ tag: 'err',
+ isAxiosError: true,
+ message: 'maxContentLength size of 1000000 exceeded',
+ });
+ await actionType.executor({
+ actionId: 'some-id',
+ services,
+ config,
+ secrets: { user: 'abc', password: '123' },
+ params: { body: 'some data' },
+ });
+ expect(mockedLogger.error).toBeCalledWith(
+ 'error on some-id webhook event: maxContentLength size of 1000000 exceeded'
+ );
+ });
+
test('execute without username/password sends request without basic auth', async () => {
const config: ActionTypeConfigType = {
url: 'https://abc.def/my-webhook',
@@ -355,6 +383,7 @@ describe('execute()', () => {
"ensureHostnameAllowed": [MockFunction],
"ensureUriAllowed": [MockFunction],
"getProxySettings": [MockFunction],
+ "getResponseSettings": [MockFunction],
"isActionTypeEnabled": [MockFunction],
"isHostnameAllowed": [MockFunction],
"isRejectUnauthorizedCertificatesEnabled": [MockFunction],
diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts
index 269449686acf0d..93c9bbdbab18af 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts
@@ -180,7 +180,6 @@ export async function executor(
return successResult(actionId, data);
} else {
const { error } = result;
-
if (error.response) {
const {
status,
@@ -211,6 +210,10 @@ export async function executor(
const message = `[${error.code}] ${error.message}`;
logger.error(`error on ${actionId} webhook event: ${message}`);
return errorResultRequestFailed(actionId, message);
+ } else if (error.isAxiosError) {
+ const message = `${error.message}`;
+ logger.error(`error on ${actionId} webhook event: ${message}`);
+ return errorResultRequestFailed(actionId, message);
}
logger.error(`error on ${actionId} webhook action: unexpected error`);
diff --git a/x-pack/plugins/actions/server/config.test.ts b/x-pack/plugins/actions/server/config.test.ts
index c90a5b2fb9768c..2eecaa19da0c50 100644
--- a/x-pack/plugins/actions/server/config.test.ts
+++ b/x-pack/plugins/actions/server/config.test.ts
@@ -5,9 +5,17 @@
* 2.0.
*/
-import { configSchema } from './config';
+import { configSchema, ActionsConfig, getValidatedConfig } from './config';
+import { Logger } from '../../../..//src/core/server';
+import { loggingSystemMock } from '../../../..//src/core/server/mocks';
+
+const mockLogger = loggingSystemMock.create().get() as jest.Mocked;
describe('config validation', () => {
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
test('action defaults', () => {
const config: Record = {};
expect(configSchema.validate(config)).toMatchInlineSnapshot(`
@@ -19,9 +27,13 @@ describe('config validation', () => {
"enabledActionTypes": Array [
"*",
],
+ "maxResponseContentLength": ByteSizeValue {
+ "valueInBytes": 1048576,
+ },
"preconfigured": Object {},
"proxyRejectUnauthorizedCertificates": true,
"rejectUnauthorized": true,
+ "responseTimeout": "PT1M",
}
`);
});
@@ -49,6 +61,9 @@ describe('config validation', () => {
"enabledActionTypes": Array [
"*",
],
+ "maxResponseContentLength": ByteSizeValue {
+ "valueInBytes": 1048576,
+ },
"preconfigured": Object {
"mySlack1": Object {
"actionTypeId": ".slack",
@@ -61,6 +76,7 @@ describe('config validation', () => {
},
"proxyRejectUnauthorizedCertificates": false,
"rejectUnauthorized": false,
+ "responseTimeout": "PT1M",
}
`);
});
@@ -84,6 +100,56 @@ describe('config validation', () => {
`"[preconfigured]: invalid preconfigured action id \\"__proto__\\""`
);
});
+
+ test('validates proxyBypassHosts and proxyOnlyHosts', () => {
+ const bypassHosts = ['bypass.elastic.co'];
+ const onlyHosts = ['only.elastic.co'];
+ let validated: ActionsConfig;
+
+ validated = configSchema.validate({});
+ expect(validated.proxyBypassHosts).toEqual(undefined);
+ expect(validated.proxyOnlyHosts).toEqual(undefined);
+
+ validated = configSchema.validate({
+ proxyBypassHosts: bypassHosts,
+ });
+ expect(validated.proxyBypassHosts).toEqual(bypassHosts);
+ expect(validated.proxyOnlyHosts).toEqual(undefined);
+
+ validated = configSchema.validate({
+ proxyOnlyHosts: onlyHosts,
+ });
+ expect(validated.proxyBypassHosts).toEqual(undefined);
+ expect(validated.proxyOnlyHosts).toEqual(onlyHosts);
+ });
+
+ test('validates proxyBypassHosts and proxyOnlyHosts used at the same time', () => {
+ const bypassHosts = ['bypass.elastic.co'];
+ const onlyHosts = ['only.elastic.co'];
+ const config: Record = {
+ proxyBypassHosts: bypassHosts,
+ proxyOnlyHosts: onlyHosts,
+ };
+
+ let validated: ActionsConfig;
+
+ // the config schema validation validates with both set
+ validated = configSchema.validate(config);
+ expect(validated.proxyBypassHosts).toEqual(bypassHosts);
+ expect(validated.proxyOnlyHosts).toEqual(onlyHosts);
+
+ // getValidatedConfig will warn and set onlyHosts to undefined with both set
+ validated = getValidatedConfig(mockLogger, validated);
+ expect(validated.proxyBypassHosts).toEqual(bypassHosts);
+ expect(validated.proxyOnlyHosts).toEqual(undefined);
+ expect(mockLogger.warn.mock.calls).toMatchInlineSnapshot(`
+ Array [
+ Array [
+ "The confgurations xpack.actions.proxyBypassHosts and xpack.actions.proxyOnlyHosts can not be used at the same time. The configuration xpack.actions.proxyOnlyHosts will be ignored.",
+ ],
+ ]
+ `);
+ });
});
// object creator that ensures we can create a property named __proto__ on an
diff --git a/x-pack/plugins/actions/server/config.ts b/x-pack/plugins/actions/server/config.ts
index b4f29b752957fb..4aa77ded315b85 100644
--- a/x-pack/plugins/actions/server/config.ts
+++ b/x-pack/plugins/actions/server/config.ts
@@ -6,7 +6,15 @@
*/
import { schema, TypeOf } from '@kbn/config-schema';
-import { AllowedHosts, EnabledActionTypes } from './actions_config';
+import { Logger } from '../../../../src/core/server';
+
+export enum AllowedHosts {
+ Any = '*',
+}
+
+export enum EnabledActionTypes {
+ Any = '*',
+}
const preconfiguredActionSchema = schema.object({
name: schema.string({ minLength: 1 }),
@@ -36,11 +44,34 @@ export const configSchema = schema.object({
proxyUrl: schema.maybe(schema.string()),
proxyHeaders: schema.maybe(schema.recordOf(schema.string(), schema.string())),
proxyRejectUnauthorizedCertificates: schema.boolean({ defaultValue: true }),
+ proxyBypassHosts: schema.maybe(schema.arrayOf(schema.string({ hostname: true }))),
+ proxyOnlyHosts: schema.maybe(schema.arrayOf(schema.string({ hostname: true }))),
rejectUnauthorized: schema.boolean({ defaultValue: true }),
+ maxResponseContentLength: schema.byteSize({ defaultValue: '1mb' }),
+ responseTimeout: schema.duration({ defaultValue: '60s' }),
});
export type ActionsConfig = TypeOf;
+// It would be nicer to add the proxyBypassHosts / proxyOnlyHosts restriction on
+// simultaneous usage in the config validator directly, but there's no good way to express
+// this relationship in the cloud config constraints, so we're doing it "live".
+export function getValidatedConfig(logger: Logger, originalConfig: ActionsConfig): ActionsConfig {
+ const proxyBypassHosts = originalConfig.proxyBypassHosts;
+ const proxyOnlyHosts = originalConfig.proxyOnlyHosts;
+
+ if (proxyBypassHosts && proxyOnlyHosts) {
+ logger.warn(
+ 'The confgurations xpack.actions.proxyBypassHosts and xpack.actions.proxyOnlyHosts can not be used at the same time. The configuration xpack.actions.proxyOnlyHosts will be ignored.'
+ );
+ const tmp: Record = originalConfig;
+ delete tmp.proxyOnlyHosts;
+ return tmp as ActionsConfig;
+ }
+
+ return originalConfig;
+}
+
const invalidActionIds = new Set(['', '__proto__', 'constructor']);
function validatePreconfigured(preconfigured: Record): string | undefined {
diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts
index b8f83e91239e2c..30bbedbedbe9cd 100644
--- a/x-pack/plugins/actions/server/plugin.test.ts
+++ b/x-pack/plugins/actions/server/plugin.test.ts
@@ -5,6 +5,8 @@
* 2.0.
*/
+import moment from 'moment';
+import { ByteSizeValue } from '@kbn/config-schema';
import { PluginInitializerContext, RequestHandlerContext } from '../../../../src/core/server';
import { coreMock, httpServerMock } from '../../../../src/core/server/mocks';
import { usageCollectionPluginMock } from '../../../../src/plugins/usage_collection/server/mocks';
@@ -37,6 +39,8 @@ describe('Actions Plugin', () => {
preconfigured: {},
proxyRejectUnauthorizedCertificates: true,
rejectUnauthorized: true,
+ maxResponseContentLength: new ByteSizeValue(1000000),
+ responseTimeout: moment.duration(60000),
});
plugin = new ActionsPlugin(context);
coreSetup = coreMock.createSetup();
@@ -197,6 +201,8 @@ describe('Actions Plugin', () => {
},
proxyRejectUnauthorizedCertificates: true,
rejectUnauthorized: true,
+ maxResponseContentLength: new ByteSizeValue(1000000),
+ responseTimeout: moment.duration(60000),
});
plugin = new ActionsPlugin(context);
coreSetup = coreMock.createSetup();
diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts
index 5ec9241533b3c3..bfe3b0a09ff2e0 100644
--- a/x-pack/plugins/actions/server/plugin.ts
+++ b/x-pack/plugins/actions/server/plugin.ts
@@ -30,7 +30,7 @@ import { SpacesPluginStart } from '../../spaces/server';
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
import { SecurityPluginSetup } from '../../security/server';
-import { ActionsConfig } from './config';
+import { ActionsConfig, getValidatedConfig } from './config';
import { ActionExecutor, TaskRunnerFactory, LicenseState, ILicenseState } from './lib';
import { ActionsClient } from './actions_client';
import { ActionTypeRegistry } from './action_type_registry';
@@ -141,8 +141,8 @@ export class ActionsPlugin implements Plugin();
this.logger = initContext.logger.get('actions');
+ this.actionsConfig = getValidatedConfig(this.logger, initContext.config.get());
this.telemetryLogger = initContext.logger.get('usage');
this.preconfiguredActions = [];
this.kibanaIndexConfig = initContext.config.legacy.get();
diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts
index 4e3916f5d6e231..b7a6750a520ea9 100644
--- a/x-pack/plugins/actions/server/types.ts
+++ b/x-pack/plugins/actions/server/types.ts
@@ -133,6 +133,13 @@ export interface ActionTaskExecutorParams {
export interface ProxySettings {
proxyUrl: string;
+ proxyBypassHosts: Set | undefined;
+ proxyOnlyHosts: Set | undefined;
proxyHeaders?: Record;
proxyRejectUnauthorizedCertificates: boolean;
}
+
+export interface ResponseSettings {
+ maxContentLength: number;
+ timeout: number;
+}
diff --git a/x-pack/plugins/apm/common/latency_aggregation_types.ts b/x-pack/plugins/apm/common/latency_aggregation_types.ts
index d9db58f2231443..964d6f4ed1015a 100644
--- a/x-pack/plugins/apm/common/latency_aggregation_types.ts
+++ b/x-pack/plugins/apm/common/latency_aggregation_types.ts
@@ -14,7 +14,7 @@ export enum LatencyAggregationType {
}
export const latencyAggregationTypeRt = t.union([
- t.literal('avg'),
- t.literal('p95'),
- t.literal('p99'),
+ t.literal(LatencyAggregationType.avg),
+ t.literal(LatencyAggregationType.p95),
+ t.literal(LatencyAggregationType.p99),
]);
diff --git a/x-pack/plugins/apm/common/runtime_types/iso_to_epoch_rt/index.ts b/x-pack/plugins/apm/common/runtime_types/iso_to_epoch_rt/index.ts
index 1a17f82a521413..970e39bc4f86f0 100644
--- a/x-pack/plugins/apm/common/runtime_types/iso_to_epoch_rt/index.ts
+++ b/x-pack/plugins/apm/common/runtime_types/iso_to_epoch_rt/index.ts
@@ -21,8 +21,5 @@ export const isoToEpochRt = new t.Type(
? t.failure(input, context)
: t.success(epochDate);
}),
- (a) => {
- const d = new Date(a);
- return d.toISOString();
- }
+ (output) => new Date(output).toISOString()
);
diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx
index 7ef3cbca3ad2f5..b338d1e4ab03dc 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx
@@ -19,7 +19,7 @@ import { useLicenseContext } from '../../../context/license/use_license_context'
import { useTheme } from '../../../hooks/use_theme';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { DatePicker } from '../../shared/DatePicker';
-import { LicensePrompt } from '../../shared/LicensePrompt';
+import { LicensePrompt } from '../../shared/license_prompt';
import { Controls } from './Controls';
import { Cytoscape } from './Cytoscape';
import { getCytoscapeDivStyle } from './cytoscape_options';
diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/LinkPreview.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/LinkPreview.tsx
deleted file mode 100644
index 0312b802df173e..00000000000000
--- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/LinkPreview.tsx
+++ /dev/null
@@ -1,133 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React, { useEffect, useState } from 'react';
-import {
- EuiPanel,
- EuiText,
- EuiSpacer,
- EuiLink,
- EuiToolTip,
- EuiIcon,
- EuiFlexGroup,
- EuiFlexItem,
-} from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
-import { debounce } from 'lodash';
-import { Filter } from '../../../../../../../common/custom_link/custom_link_types';
-import { Transaction } from '../../../../../../../typings/es_schemas/ui/transaction';
-import { callApmApi } from '../../../../../../services/rest/createCallApmApi';
-import { replaceTemplateVariables, convertFiltersToQuery } from './helper';
-
-interface Props {
- label: string;
- url: string;
- filters: Filter[];
-}
-
-const fetchTransaction = debounce(
- async (filters: Filter[], callback: (transaction: Transaction) => void) => {
- const transaction = await callApmApi({
- signal: null,
- endpoint: 'GET /api/apm/settings/custom_links/transaction',
- params: { query: convertFiltersToQuery(filters) },
- });
- callback(transaction);
- },
- 1000
-);
-
-const getTextColor = (value?: string) => (value ? 'default' : 'subdued');
-
-export function LinkPreview({ label, url, filters }: Props) {
- const [transaction, setTransaction] = useState();
-
- useEffect(() => {
- /*
- React throwns "Can't perform a React state update on an unmounted component"
- It happens when the Custom Link flyout is closed before the return of the api request.
- To avoid such case, sets the isUnmounted to true when component unmount and check its value before update the transaction.
- */
- let isUnmounted = false;
- fetchTransaction(filters, (_transaction: Transaction) => {
- if (!isUnmounted) {
- setTransaction(_transaction);
- }
- });
- return () => {
- isUnmounted = true;
- };
- }, [filters]);
-
- const { formattedUrl, error } = replaceTemplateVariables(url, transaction);
-
- return (
-
-
- {label
- ? label
- : i18n.translate(
- 'xpack.apm.settings.customizeUI.customLink.default.label',
- { defaultMessage: 'Elastic.co' }
- )}
-
-
-
- {url ? (
-
- {formattedUrl}
-
- ) : (
- i18n.translate(
- 'xpack.apm.settings.customizeUI.customLink.default.url',
- { defaultMessage: 'https://www.elastic.co' }
- )
- )}
-
-
-
-
-
- {i18n.translate(
- 'xpack.apm.settings.customizeUI.customLink.linkPreview.descrition',
- {
- defaultMessage:
- 'Test your link with values from an example transaction document based on the filters above.',
- }
- )}
-
-
-
-
- {error && (
-
-
-
- )}
-
-
-
- );
-}
diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/index.tsx
index ccd2b0d4257430..dfe768735d19b4 100644
--- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/index.tsx
@@ -22,7 +22,7 @@ import { FiltersSection } from './FiltersSection';
import { FlyoutFooter } from './FlyoutFooter';
import { LinkSection } from './LinkSection';
import { saveCustomLink } from './saveCustomLink';
-import { LinkPreview } from './LinkPreview';
+import { LinkPreview } from './link_preview';
import { Documentation } from './Documentation';
interface Props {
diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.stories.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.stories.tsx
new file mode 100644
index 00000000000000..3bf17a733bf8a1
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.stories.tsx
@@ -0,0 +1,39 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { ComponentProps } from 'react';
+import { CoreStart } from 'kibana/public';
+import { createCallApmApi } from '../../../../../../services/rest/createCallApmApi';
+import { LinkPreview } from './link_preview';
+
+export default {
+ title:
+ 'app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/LinkPreview',
+ component: LinkPreview,
+};
+
+export function Example({
+ filters,
+ label,
+ url,
+}: ComponentProps) {
+ const coreMock = ({
+ http: {
+ get: async () => ({ transaction: { id: '0' } }),
+ },
+ uiSettings: { get: () => false },
+ } as unknown) as CoreStart;
+
+ createCallApmApi(coreMock);
+
+ return ;
+}
+Example.args = {
+ filters: [],
+ label: 'Example label',
+ url: 'https://example.com',
+} as ComponentProps;
diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.test.tsx
index 63481571042871..407f460f25ad37 100644
--- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.test.tsx
+++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.test.tsx
@@ -6,7 +6,7 @@
*/
import React from 'react';
-import { LinkPreview } from '../CreateEditCustomLinkFlyout/LinkPreview';
+import { LinkPreview } from '../CreateEditCustomLinkFlyout/link_preview';
import {
render,
getNodeText,
@@ -14,15 +14,18 @@ import {
act,
waitFor,
} from '@testing-library/react';
-import * as apmApi from '../../../../../../services/rest/createCallApmApi';
+import {
+ getCallApmApiSpy,
+ CallApmApiSpy,
+} from '../../../../../../services/rest/callApmApiSpy';
export const removeExternalLinkText = (str: string) =>
str.replace(/\(opens in a new tab or window\)/g, '');
describe('LinkPreview', () => {
- let callApmApiSpy: jest.SpyInstance;
+ let callApmApiSpy: CallApmApiSpy;
beforeAll(() => {
- callApmApiSpy = jest.spyOn(apmApi, 'callApmApi').mockResolvedValue({
+ callApmApiSpy = getCallApmApiSpy().mockResolvedValue({
transaction: { id: 'foo' },
});
});
diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.tsx
new file mode 100644
index 00000000000000..726d4ba0d65ee1
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateEditCustomLinkFlyout/link_preview.tsx
@@ -0,0 +1,147 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useEffect, useState } from 'react';
+import {
+ EuiPanel,
+ EuiText,
+ EuiSpacer,
+ EuiLink,
+ EuiToolTip,
+ EuiIcon,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiTitle,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { debounce } from 'lodash';
+import { Filter } from '../../../../../../../common/custom_link/custom_link_types';
+import { Transaction } from '../../../../../../../typings/es_schemas/ui/transaction';
+import { callApmApi } from '../../../../../../services/rest/createCallApmApi';
+import { replaceTemplateVariables, convertFiltersToQuery } from './helper';
+
+export interface LinkPreviewProps {
+ label: string;
+ url: string;
+ filters: Filter[];
+}
+
+const fetchTransaction = debounce(
+ async (filters: Filter[], callback: (transaction: Transaction) => void) => {
+ const transaction = await callApmApi({
+ signal: null,
+ endpoint: 'GET /api/apm/settings/custom_links/transaction',
+ params: { query: convertFiltersToQuery(filters) },
+ });
+ callback(transaction);
+ },
+ 1000
+);
+
+const getTextColor = (value?: string) => (value ? 'default' : 'subdued');
+
+export function LinkPreview({ label, url, filters }: LinkPreviewProps) {
+ const [transaction, setTransaction] = useState();
+
+ useEffect(() => {
+ /*
+ React throwns "Can't perform a React state update on an unmounted component"
+ It happens when the Custom Link flyout is closed before the return of the api request.
+ To avoid such case, sets the isUnmounted to true when component unmount and check its value before update the transaction.
+ */
+ let isUnmounted = false;
+ fetchTransaction(filters, (_transaction: Transaction) => {
+ if (!isUnmounted) {
+ setTransaction(_transaction);
+ }
+ });
+ return () => {
+ isUnmounted = true;
+ };
+ }, [filters]);
+
+ const { formattedUrl, error } = replaceTemplateVariables(url, transaction);
+
+ return (
+ <>
+
+
+ {i18n.translate(
+ 'xpack.apm.settings.customizeUI.customLink.previewSectionTitle',
+ {
+ defaultMessage: 'Preview',
+ }
+ )}
+
+
+
+
+
+ {label
+ ? label
+ : i18n.translate(
+ 'xpack.apm.settings.customizeUI.customLink.default.label',
+ { defaultMessage: 'Elastic.co' }
+ )}
+
+
+
+ {url ? (
+
+ {formattedUrl}
+
+ ) : (
+ i18n.translate(
+ 'xpack.apm.settings.customizeUI.customLink.default.url',
+ { defaultMessage: 'https://www.elastic.co' }
+ )
+ )}
+
+
+
+
+
+ {i18n.translate(
+ 'xpack.apm.settings.customizeUI.customLink.linkPreview.descrition',
+ {
+ defaultMessage:
+ 'Test your link with values from an example transaction document based on the filters above.',
+ }
+ )}
+
+
+
+
+ {error && (
+
+
+
+ )}
+
+
+
+ >
+ );
+}
diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx
index 77835afef863a6..7d119b8c406da2 100644
--- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx
+++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx
@@ -8,6 +8,7 @@
import { fireEvent, render, RenderResult } from '@testing-library/react';
import React from 'react';
import { act } from 'react-dom/test-utils';
+import { getCallApmApiSpy } from '../../../../../services/rest/callApmApiSpy';
import { CustomLinkOverview } from '.';
import { License } from '../../../../../../../licensing/common/license';
import { ApmPluginContextValue } from '../../../../../context/apm_plugin/apm_plugin_context';
@@ -17,7 +18,6 @@ import {
} from '../../../../../context/apm_plugin/mock_apm_plugin_context';
import { LicenseContext } from '../../../../../context/license/license_context';
import * as hooks from '../../../../../hooks/use_fetcher';
-import * as apmApi from '../../../../../services/rest/createCallApmApi';
import {
expectTextsInDocument,
expectTextsNotInDocument,
@@ -43,7 +43,7 @@ function getMockAPMContext({ canSave }: { canSave: boolean }) {
describe('CustomLink', () => {
beforeAll(() => {
- jest.spyOn(apmApi, 'callApmApi').mockResolvedValue({});
+ getCallApmApiSpy().mockResolvedValue({});
});
afterAll(() => {
jest.resetAllMocks();
diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx
index 49fa3eab47862e..ab18a31e769172 100644
--- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx
@@ -20,7 +20,7 @@ import { INVALID_LICENSE } from '../../../../../../common/custom_link';
import { CustomLink } from '../../../../../../common/custom_link/custom_link_types';
import { FETCH_STATUS, useFetcher } from '../../../../../hooks/use_fetcher';
import { useLicenseContext } from '../../../../../context/license/use_license_context';
-import { LicensePrompt } from '../../../../shared/LicensePrompt';
+import { LicensePrompt } from '../../../../shared/license_prompt';
import { CreateCustomLinkButton } from './CreateCustomLinkButton';
import { CreateEditCustomLinkFlyout } from './CreateEditCustomLinkFlyout';
import { CustomLinkTable } from './CustomLinkTable';
diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx
index 72f0249f07bf68..62b39664cf63da 100644
--- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx
@@ -14,7 +14,7 @@ import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plug
import { JobsList } from './jobs_list';
import { AddEnvironments } from './add_environments';
import { useFetcher } from '../../../../hooks/use_fetcher';
-import { LicensePrompt } from '../../../shared/LicensePrompt';
+import { LicensePrompt } from '../../../shared/license_prompt';
import { useLicenseContext } from '../../../../context/license/use_license_context';
import { APIReturnType } from '../../../../services/rest/createCallApmApi';
diff --git a/x-pack/plugins/apm/public/components/app/correlations/index.tsx b/x-pack/plugins/apm/public/components/app/correlations/index.tsx
index e0651edbeb79b5..62c547aa69e0d5 100644
--- a/x-pack/plugins/apm/public/components/app/correlations/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/correlations/index.tsx
@@ -34,7 +34,7 @@ import {
} from '../../../../../observability/public';
import { isActivePlatinumLicense } from '../../../../common/license_check';
import { useLicenseContext } from '../../../context/license/use_license_context';
-import { LicensePrompt } from '../../shared/LicensePrompt';
+import { LicensePrompt } from '../../shared/license_prompt';
import { IUrlParams } from '../../../context/url_params_context/types';
const latencyTab = {
diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx
index b30faac7a65af8..c6ed4e640693f0 100644
--- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx
+++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx
@@ -22,9 +22,12 @@ import * as useTransactionBreakdownHooks from '../../shared/charts/transaction_b
import { renderWithTheme } from '../../../utils/testHelpers';
import { ServiceOverview } from './';
import { waitFor } from '@testing-library/dom';
-import * as callApmApiModule from '../../../services/rest/createCallApmApi';
import * as useApmServiceContextHooks from '../../../context/apm_service/use_apm_service_context';
import { LatencyAggregationType } from '../../../../common/latency_aggregation_types';
+import {
+ getCallApmApiSpy,
+ getCreateCallApmApiSpy,
+} from '../../../services/rest/callApmApiSpy';
const KibanaReactContext = createKibanaReactContext({
usageCollection: { reportUiCounter: () => {} },
@@ -83,10 +86,10 @@ describe('ServiceOverview', () => {
/* eslint-disable @typescript-eslint/naming-convention */
const calls = {
'GET /api/apm/services/{serviceName}/error_groups/primary_statistics': {
- error_groups: [],
+ error_groups: [] as any[],
},
'GET /api/apm/services/{serviceName}/transactions/groups/primary_statistics': {
- transactionGroups: [],
+ transactionGroups: [] as any[],
totalTransactionGroups: 0,
isAggregationAccurate: true,
},
@@ -95,19 +98,17 @@ describe('ServiceOverview', () => {
};
/* eslint-enable @typescript-eslint/naming-convention */
- jest
- .spyOn(callApmApiModule, 'createCallApmApi')
- .mockImplementation(() => {});
-
- const callApmApi = jest
- .spyOn(callApmApiModule, 'callApmApi')
- .mockImplementation(({ endpoint }) => {
+ const callApmApiSpy = getCallApmApiSpy().mockImplementation(
+ ({ endpoint }) => {
const response = calls[endpoint as keyof typeof calls];
return response
? Promise.resolve(response)
: Promise.reject(`Response for ${endpoint} is not defined`);
- });
+ }
+ );
+
+ getCreateCallApmApiSpy().mockImplementation(() => callApmApiSpy as any);
jest
.spyOn(useTransactionBreakdownHooks, 'useTransactionBreakdown')
.mockReturnValue({
@@ -124,7 +125,7 @@ describe('ServiceOverview', () => {
);
await waitFor(() =>
- expect(callApmApi).toHaveBeenCalledTimes(Object.keys(calls).length)
+ expect(callApmApiSpy).toHaveBeenCalledTimes(Object.keys(calls).length)
);
expect((await findAllByText('Latency')).length).toBeGreaterThan(0);
diff --git a/x-pack/plugins/apm/public/components/shared/LicensePrompt/index.tsx b/x-pack/plugins/apm/public/components/shared/LicensePrompt/index.tsx
deleted file mode 100644
index 97a48a61e47cc0..00000000000000
--- a/x-pack/plugins/apm/public/components/shared/LicensePrompt/index.tsx
+++ /dev/null
@@ -1,63 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { EuiButton, EuiEmptyPrompt, EuiPanel } from '@elastic/eui';
-import React from 'react';
-import { i18n } from '@kbn/i18n';
-import { useKibanaUrl } from '../../../hooks/useKibanaUrl';
-
-interface Props {
- text: string;
- showBetaBadge?: boolean;
-}
-
-export function LicensePrompt({ text, showBetaBadge = false }: Props) {
- const licensePageUrl = useKibanaUrl(
- '/app/management/stack/license_management'
- );
-
- const renderLicenseBody = (
-
- {i18n.translate('xpack.apm.license.title', {
- defaultMessage: 'Start free 30-day trial',
- })}
-
- }
- body={{text}
}
- actions={
-
- {i18n.translate('xpack.apm.license.button', {
- defaultMessage: 'Start trial',
- })}
-
- }
- />
- );
-
- const renderWithBetaBadge = (
-
- {renderLicenseBody}
-
- );
-
- return <>{showBetaBadge ? renderWithBetaBadge : renderLicenseBody}>;
-}
diff --git a/x-pack/plugins/apm/public/components/shared/license_prompt/index.tsx b/x-pack/plugins/apm/public/components/shared/license_prompt/index.tsx
new file mode 100644
index 00000000000000..0950cff5127fc0
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/license_prompt/index.tsx
@@ -0,0 +1,59 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EuiButton, EuiCard, EuiTextColor } from '@elastic/eui';
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { useKibanaUrl } from '../../../hooks/useKibanaUrl';
+
+export interface LicensePromptProps {
+ text: string;
+ showBetaBadge?: boolean;
+}
+
+export function LicensePrompt({
+ text,
+ showBetaBadge = false,
+}: LicensePromptProps) {
+ const licensePageUrl = useKibanaUrl(
+ '/app/management/stack/license_management'
+ );
+
+ return (
+ {text}}
+ footer={
+
+ {i18n.translate('xpack.apm.license.button', {
+ defaultMessage: 'Start trial',
+ })}
+
+ }
+ />
+ );
+}
diff --git a/x-pack/plugins/apm/public/components/shared/LicensePrompt/LicensePrompt.stories.tsx b/x-pack/plugins/apm/public/components/shared/license_prompt/license_prompt.stories.tsx
similarity index 61%
rename from x-pack/plugins/apm/public/components/shared/LicensePrompt/LicensePrompt.stories.tsx
rename to x-pack/plugins/apm/public/components/shared/license_prompt/license_prompt.stories.tsx
index 57f782a0200826..35e22b50306d95 100644
--- a/x-pack/plugins/apm/public/components/shared/LicensePrompt/LicensePrompt.stories.tsx
+++ b/x-pack/plugins/apm/public/components/shared/license_prompt/license_prompt.stories.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { ComponentType } from 'react';
+import React, { ComponentProps, ComponentType } from 'react';
import { LicensePrompt } from '.';
import {
ApmPluginContext,
@@ -17,19 +17,25 @@ const contextMock = ({
} as unknown) as ApmPluginContextValue;
export default {
- title: 'app/LicensePrompt',
+ title: 'shared/LicensePrompt',
component: LicensePrompt,
decorators: [
(Story: ComponentType) => (
- {' '}
+
),
],
};
-export function Example() {
- return (
-
- );
+export function Example({
+ showBetaBadge,
+ text,
+}: ComponentProps) {
+ return ;
}
+Example.args = {
+ showBetaBadge: false,
+ text:
+ 'To create Feature name, you must be subscribed to an Elastic X license or above.',
+} as ComponentProps;
diff --git a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts
index 29fabc51fd5827..00447607cf7873 100644
--- a/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts
+++ b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts
@@ -10,10 +10,10 @@ import {
fetchObservabilityOverviewPageData,
getHasData,
} from './apm_observability_overview_fetchers';
-import * as createCallApmApi from './createCallApmApi';
+import { getCallApmApiSpy } from './callApmApiSpy';
describe('Observability dashboard data', () => {
- const callApmApiMock = jest.spyOn(createCallApmApi, 'callApmApi');
+ const callApmApiMock = getCallApmApiSpy();
const params = {
absoluteTime: {
start: moment('2020-07-02T13:25:11.629Z').valueOf(),
@@ -84,7 +84,7 @@ describe('Observability dashboard data', () => {
callApmApiMock.mockImplementation(() =>
Promise.resolve({
serviceCount: 0,
- transactionPerMinute: { value: null, timeseries: [] },
+ transactionPerMinute: { value: null, timeseries: [] as any },
})
);
const response = await fetchObservabilityOverviewPageData(params);
diff --git a/x-pack/plugins/apm/public/services/rest/callApmApiSpy.ts b/x-pack/plugins/apm/public/services/rest/callApmApiSpy.ts
new file mode 100644
index 00000000000000..ba9f740e06d0de
--- /dev/null
+++ b/x-pack/plugins/apm/public/services/rest/callApmApiSpy.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import * as createCallApmApi from './createCallApmApi';
+import type { AbstractAPMClient } from './createCallApmApi';
+
+export type CallApmApiSpy = jest.SpyInstance<
+ Promise,
+ Parameters
+>;
+
+export type CreateCallApmApiSpy = jest.SpyInstance;
+
+export const getCreateCallApmApiSpy = () =>
+ (jest.spyOn(
+ createCallApmApi,
+ 'createCallApmApi'
+ ) as unknown) as CreateCallApmApiSpy;
+export const getCallApmApiSpy = () =>
+ (jest.spyOn(createCallApmApi, 'callApmApi') as unknown) as CallApmApiSpy;
diff --git a/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts b/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts
index b0cce3296fe210..0e82d70faf1e19 100644
--- a/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts
+++ b/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts
@@ -6,30 +6,68 @@
*/
import { CoreSetup, CoreStart } from 'kibana/public';
-import { parseEndpoint } from '../../../common/apm_api/parse_endpoint';
+import * as t from 'io-ts';
+import type {
+ ClientRequestParamsOf,
+ EndpointOf,
+ ReturnOf,
+ RouteRepositoryClient,
+ ServerRouteRepository,
+ ServerRoute,
+} from '@kbn/server-route-repository';
+import { formatRequest } from '@kbn/server-route-repository/target/format_request';
import { FetchOptions } from '../../../common/fetch_options';
import { callApi } from './callApi';
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import type { APMAPI } from '../../../server/routes/create_apm_api';
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import type { Client } from '../../../server/routes/typings';
-
-export type APMClient = Client;
-export type AutoAbortedAPMClient = Client;
+import type {
+ APMServerRouteRepository,
+ InspectResponse,
+ APMRouteHandlerResources,
+ // eslint-disable-next-line @kbn/eslint/no-restricted-paths
+} from '../../../server';
export type APMClientOptions = Omit<
FetchOptions,
'query' | 'body' | 'pathname' | 'signal'
> & {
- endpoint: string;
signal: AbortSignal | null;
- params?: {
- body?: any;
- query?: Record;
- path?: Record;
- };
};
+export type APMClient = RouteRepositoryClient<
+ APMServerRouteRepository,
+ APMClientOptions
+>;
+
+export type AutoAbortedAPMClient = RouteRepositoryClient<
+ APMServerRouteRepository,
+ Omit
+>;
+
+export type APIReturnType<
+ TEndpoint extends EndpointOf
+> = ReturnOf & {
+ _inspect?: InspectResponse;
+};
+
+export type APIEndpoint = EndpointOf;
+
+export type APIClientRequestParamsOf<
+ TEndpoint extends EndpointOf
+> = ClientRequestParamsOf;
+
+export type AbstractAPMRepository = ServerRouteRepository<
+ APMRouteHandlerResources,
+ {},
+ Record<
+ string,
+ ServerRoute
+ >
+>;
+
+export type AbstractAPMClient = RouteRepositoryClient<
+ AbstractAPMRepository,
+ APMClientOptions
+>;
+
export let callApmApi: APMClient = () => {
throw new Error(
'callApmApi has to be initialized before used. Call createCallApmApi first.'
@@ -37,9 +75,13 @@ export let callApmApi: APMClient = () => {
};
export function createCallApmApi(core: CoreStart | CoreSetup) {
- callApmApi = ((options: APMClientOptions) => {
- const { endpoint, params, ...opts } = options;
- const { method, pathname } = parseEndpoint(endpoint, params?.path);
+ callApmApi = ((options) => {
+ const { endpoint, ...opts } = options;
+ const { params } = (options as unknown) as {
+ params?: Partial>;
+ };
+
+ const { method, pathname } = formatRequest(endpoint, params?.path);
return callApi(core, {
...opts,
@@ -50,10 +92,3 @@ export function createCallApmApi(core: CoreStart | CoreSetup) {
});
}) as APMClient;
}
-
-// infer return type from API
-export type APIReturnType<
- TPath extends keyof APMAPI['_S']
-> = APMAPI['_S'][TPath] extends { ret: any }
- ? APMAPI['_S'][TPath]['ret']
- : unknown;
diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts
index 00910353ac2787..9ab56c1a303ea7 100644
--- a/x-pack/plugins/apm/server/index.ts
+++ b/x-pack/plugins/apm/server/index.ts
@@ -120,5 +120,9 @@ export function mergeConfigs(
export const plugin = (initContext: PluginInitializerContext) =>
new APMPlugin(initContext);
-export { APMPlugin, APMPluginSetup } from './plugin';
+export { APMPlugin } from './plugin';
+export { APMPluginSetup } from './types';
+export { APMServerRouteRepository } from './routes/get_global_apm_server_route_repository';
+export { InspectResponse, APMRouteHandlerResources } from './routes/typings';
+
export type { ProcessorEvent } from '../common/processor_event';
diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts
index 1f0aa401bcab0c..989297544c78fe 100644
--- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts
+++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts
@@ -10,7 +10,7 @@
import { omit } from 'lodash';
import chalk from 'chalk';
import { KibanaRequest } from '../../../../../../../src/core/server';
-import { inspectableEsQueriesMap } from '../../../routes/create_api';
+import { inspectableEsQueriesMap } from '../../../routes/register_routes';
function formatObj(obj: Record) {
return JSON.stringify(obj, null, 2);
diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts
index 45e17c1678518e..9d7434d127ead0 100644
--- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts
+++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_internal_es_client/index.ts
@@ -5,7 +5,6 @@
* 2.0.
*/
-import { KibanaRequest } from 'src/core/server';
import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport';
import {
CreateIndexRequest,
@@ -13,7 +12,7 @@ import {
IndexRequest,
} from '@elastic/elasticsearch/api/types';
import { unwrapEsResponse } from '../../../../../../observability/server';
-import { APMRequestHandlerContext } from '../../../../routes/typings';
+import { APMRouteHandlerResources } from '../../../../routes/typings';
import {
ESSearchResponse,
ESSearchRequest,
@@ -31,11 +30,9 @@ export type APMInternalClient = ReturnType;
export function createInternalESClient({
context,
+ debug,
request,
-}: {
- context: APMRequestHandlerContext;
- request: KibanaRequest;
-}) {
+}: Pick & { debug: boolean }) {
const { asInternalUser } = context.core.elasticsearch.client;
function callEs({
@@ -53,7 +50,7 @@ export function createInternalESClient({
title: getDebugTitle(request),
body: getDebugBody(params, requestType),
}),
- debug: context.params.query._inspect,
+ debug,
isCalledWithInternalUser: true,
request,
requestType,
diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts
index c0707d02861804..c0ff0cab88f47f 100644
--- a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts
+++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts
@@ -7,8 +7,7 @@
import { setupRequest } from './setup_request';
import { APMConfig } from '../..';
-import { APMRequestHandlerContext } from '../../routes/typings';
-import { KibanaRequest } from '../../../../../../src/core/server';
+import { APMRouteHandlerResources } from '../../routes/typings';
import { ProcessorEvent } from '../../../common/processor_event';
import { PROCESSOR_EVENT } from '../../../common/elasticsearch_fieldnames';
@@ -32,7 +31,7 @@ jest.mock('../index_pattern/get_dynamic_index_pattern', () => ({
},
}));
-function getMockRequest() {
+function getMockResources() {
const esClientMock = {
asCurrentUser: {
search: jest.fn().mockResolvedValue({ body: {} }),
@@ -42,7 +41,7 @@ function getMockRequest() {
},
};
- const mockContext = ({
+ const mockResources = ({
config: new Proxy(
{},
{
@@ -54,65 +53,69 @@ function getMockRequest() {
_inspect: false,
},
},
- core: {
- elasticsearch: {
- client: esClientMock,
- },
- uiSettings: {
- client: {
- get: jest.fn().mockResolvedValue(false),
+ context: {
+ core: {
+ elasticsearch: {
+ client: esClientMock,
},
- },
- savedObjects: {
- client: {
- get: jest.fn(),
+ uiSettings: {
+ client: {
+ get: jest.fn().mockResolvedValue(false),
+ },
+ },
+ savedObjects: {
+ client: {
+ get: jest.fn(),
+ },
},
},
},
plugins: {
ml: undefined,
},
- } as unknown) as APMRequestHandlerContext & {
- core: {
- elasticsearch: {
- client: typeof esClientMock;
- };
- uiSettings: {
- client: {
- get: jest.Mock;
+ request: {
+ url: '',
+ events: {
+ aborted$: {
+ subscribe: jest.fn().mockReturnValue({ unsubscribe: jest.fn() }),
+ },
+ },
+ },
+ } as unknown) as APMRouteHandlerResources & {
+ context: {
+ core: {
+ elasticsearch: {
+ client: typeof esClientMock;
};
- };
- savedObjects: {
- client: {
- get: jest.Mock;
+ uiSettings: {
+ client: {
+ get: jest.Mock;
+ };
+ };
+ savedObjects: {
+ client: {
+ get: jest.Mock;
+ };
};
};
};
};
- const mockRequest = ({
- url: '',
- events: {
- aborted$: {
- subscribe: jest.fn().mockReturnValue({ unsubscribe: jest.fn() }),
- },
- },
- } as unknown) as KibanaRequest;
-
- return { mockContext, mockRequest };
+ return mockResources;
}
describe('setupRequest', () => {
describe('with default args', () => {
it('calls callWithRequest', async () => {
- const { mockContext, mockRequest } = getMockRequest();
- const { apmEventClient } = await setupRequest(mockContext, mockRequest);
+ const mockResources = getMockResources();
+ const { apmEventClient } = await setupRequest(mockResources);
await apmEventClient.search({
apm: { events: [ProcessorEvent.transaction] },
body: { foo: 'bar' },
});
+
expect(
- mockContext.core.elasticsearch.client.asCurrentUser.search
+ mockResources.context.core.elasticsearch.client.asCurrentUser.search
).toHaveBeenCalledWith({
index: ['apm-*'],
body: {
@@ -132,14 +135,14 @@ describe('setupRequest', () => {
});
it('calls callWithInternalUser', async () => {
- const { mockContext, mockRequest } = getMockRequest();
- const { internalClient } = await setupRequest(mockContext, mockRequest);
+ const mockResources = getMockResources();
+ const { internalClient } = await setupRequest(mockResources);
await internalClient.search({
index: ['apm-*'],
body: { foo: 'bar' },
} as any);
expect(
- mockContext.core.elasticsearch.client.asInternalUser.search
+ mockResources.context.core.elasticsearch.client.asInternalUser.search
).toHaveBeenCalledWith({
index: ['apm-*'],
body: {
@@ -151,8 +154,8 @@ describe('setupRequest', () => {
describe('with a bool filter', () => {
it('adds a range filter for `observer.version_major` to the existing filter', async () => {
- const { mockContext, mockRequest } = getMockRequest();
- const { apmEventClient } = await setupRequest(mockContext, mockRequest);
+ const mockResources = getMockResources();
+ const { apmEventClient } = await setupRequest(mockResources);
await apmEventClient.search({
apm: {
events: [ProcessorEvent.transaction],
@@ -162,8 +165,8 @@ describe('setupRequest', () => {
},
});
const params =
- mockContext.core.elasticsearch.client.asCurrentUser.search.mock
- .calls[0][0];
+ mockResources.context.core.elasticsearch.client.asCurrentUser.search
+ .mock.calls[0][0];
expect(params.body).toEqual({
query: {
bool: {
@@ -178,8 +181,8 @@ describe('setupRequest', () => {
});
it('does not add a range filter for `observer.version_major` if includeLegacyData=true', async () => {
- const { mockContext, mockRequest } = getMockRequest();
- const { apmEventClient } = await setupRequest(mockContext, mockRequest);
+ const mockResources = getMockResources();
+ const { apmEventClient } = await setupRequest(mockResources);
await apmEventClient.search(
{
apm: {
@@ -194,8 +197,8 @@ describe('setupRequest', () => {
}
);
const params =
- mockContext.core.elasticsearch.client.asCurrentUser.search.mock
- .calls[0][0];
+ mockResources.context.core.elasticsearch.client.asCurrentUser.search
+ .mock.calls[0][0];
expect(params.body).toEqual({
query: {
bool: {
@@ -216,15 +219,15 @@ describe('setupRequest', () => {
describe('without a bool filter', () => {
it('adds a range filter for `observer.version_major`', async () => {
- const { mockContext, mockRequest } = getMockRequest();
- const { apmEventClient } = await setupRequest(mockContext, mockRequest);
+ const mockResources = getMockResources();
+ const { apmEventClient } = await setupRequest(mockResources);
await apmEventClient.search({
apm: {
events: [ProcessorEvent.error],
},
});
const params =
- mockContext.core.elasticsearch.client.asCurrentUser.search.mock
+ mockResources.context.core.elasticsearch.client.asCurrentUser.search.mock
.calls[0][0];
expect(params.body).toEqual({
query: {
@@ -241,12 +244,12 @@ describe('without a bool filter', () => {
describe('with includeFrozen=false', () => {
it('sets `ignore_throttled=true`', async () => {
- const { mockContext, mockRequest } = getMockRequest();
+ const mockResources = getMockResources();
// mock includeFrozen to return false
- mockContext.core.uiSettings.client.get.mockResolvedValue(false);
+ mockResources.context.core.uiSettings.client.get.mockResolvedValue(false);
- const { apmEventClient } = await setupRequest(mockContext, mockRequest);
+ const { apmEventClient } = await setupRequest(mockResources);
await apmEventClient.search({
apm: {
@@ -255,7 +258,7 @@ describe('with includeFrozen=false', () => {
});
const params =
- mockContext.core.elasticsearch.client.asCurrentUser.search.mock
+ mockResources.context.core.elasticsearch.client.asCurrentUser.search.mock
.calls[0][0];
expect(params.ignore_throttled).toBe(true);
});
@@ -263,19 +266,19 @@ describe('with includeFrozen=false', () => {
describe('with includeFrozen=true', () => {
it('sets `ignore_throttled=false`', async () => {
- const { mockContext, mockRequest } = getMockRequest();
+ const mockResources = getMockResources();
// mock includeFrozen to return true
- mockContext.core.uiSettings.client.get.mockResolvedValue(true);
+ mockResources.context.core.uiSettings.client.get.mockResolvedValue(true);
- const { apmEventClient } = await setupRequest(mockContext, mockRequest);
+ const { apmEventClient } = await setupRequest(mockResources);
await apmEventClient.search({
apm: { events: [] },
});
const params =
- mockContext.core.elasticsearch.client.asCurrentUser.search.mock
+ mockResources.context.core.elasticsearch.client.asCurrentUser.search.mock
.calls[0][0];
expect(params.ignore_throttled).toBe(false);
});
diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts
index fff661250c6dfe..40836cb6635e32 100644
--- a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts
+++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts
@@ -11,7 +11,7 @@ import { APMConfig } from '../..';
import { KibanaRequest } from '../../../../../../src/core/server';
import { UI_SETTINGS } from '../../../../../../src/plugins/data/common';
import { UIFilters } from '../../../typings/ui_filters';
-import { APMRequestHandlerContext } from '../../routes/typings';
+import { APMRouteHandlerResources } from '../../routes/typings';
import {
ApmIndicesConfig,
getApmIndices,
@@ -44,7 +44,7 @@ export interface SetupTimeRange {
}
interface SetupRequestParams {
- query?: {
+ query: {
_inspect?: boolean;
/**
@@ -64,13 +64,19 @@ type InferSetup = Setup &
(TParams extends { query: { start: number } } ? { start: number } : {}) &
(TParams extends { query: { end: number } } ? { end: number } : {});
-export async function setupRequest(
- context: APMRequestHandlerContext,
- request: KibanaRequest
-): Promise> {
+export async function setupRequest({
+ context,
+ params,
+ core,
+ plugins,
+ request,
+ config,
+ logger,
+}: APMRouteHandlerResources & {
+ params: TParams;
+}): Promise> {
return withApmSpan('setup_request', async () => {
- const { config, logger } = context;
- const { query } = context.params;
+ const { query } = params;
const [indices, includeFrozen] = await Promise.all([
getApmIndices({
@@ -88,7 +94,7 @@ export async function setupRequest(
indices,
apmEventClient: createApmEventClient({
esClient: context.core.elasticsearch.client.asCurrentUser,
- debug: context.params.query._inspect,
+ debug: query._inspect,
request,
indices,
options: { includeFrozen },
@@ -96,11 +102,12 @@ export async function setupRequest(
internalClient: createInternalESClient({
context,
request,
+ debug: query._inspect,
}),
ml:
- context.plugins.ml && isActivePlatinumLicense(context.licensing.license)
+ plugins.ml && isActivePlatinumLicense(context.licensing.license)
? getMlSetup(
- context.plugins.ml,
+ plugins.ml.setup,
context.core.savedObjects.client,
request
)
@@ -118,8 +125,8 @@ export async function setupRequest(
}
function getMlSetup(
- ml: Required['ml'],
- savedObjectsClient: APMRequestHandlerContext['core']['savedObjects']['client'],
+ ml: Required['ml']['setup'],
+ savedObjectsClient: APMRouteHandlerResources['context']['core']['savedObjects']['client'],
request: KibanaRequest
) {
return {
diff --git a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.test.ts b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.test.ts
index 19163da449b907..a5340c1220b443 100644
--- a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.test.ts
+++ b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.test.ts
@@ -8,21 +8,9 @@
import { createStaticIndexPattern } from './create_static_index_pattern';
import { Setup } from '../helpers/setup_request';
import * as HistoricalAgentData from '../services/get_services/has_historical_agent_data';
-import { APMRequestHandlerContext } from '../../routes/typings';
import { InternalSavedObjectsClient } from '../helpers/get_internal_saved_objects_client';
+import { APMConfig } from '../..';
-function getMockContext(config: Record) {
- return ({
- config,
- core: {
- savedObjects: {
- client: {
- create: jest.fn(),
- },
- },
- },
- } as unknown) as APMRequestHandlerContext;
-}
function getMockSavedObjectsClient() {
return ({
create: jest.fn(),
@@ -32,13 +20,13 @@ function getMockSavedObjectsClient() {
describe('createStaticIndexPattern', () => {
it(`should not create index pattern if 'xpack.apm.autocreateApmIndexPattern=false'`, async () => {
const setup = {} as Setup;
- const context = getMockContext({
- 'xpack.apm.autocreateApmIndexPattern': false,
- });
+
const savedObjectsClient = getMockSavedObjectsClient();
await createStaticIndexPattern(
setup,
- context,
+ {
+ 'xpack.apm.autocreateApmIndexPattern': false,
+ } as APMConfig,
savedObjectsClient,
'default'
);
@@ -47,9 +35,6 @@ describe('createStaticIndexPattern', () => {
it(`should not create index pattern if no APM data is found`, async () => {
const setup = {} as Setup;
- const context = getMockContext({
- 'xpack.apm.autocreateApmIndexPattern': true,
- });
// does not have APM data
jest
@@ -60,7 +45,9 @@ describe('createStaticIndexPattern', () => {
await createStaticIndexPattern(
setup,
- context,
+ {
+ 'xpack.apm.autocreateApmIndexPattern': true,
+ } as APMConfig,
savedObjectsClient,
'default'
);
@@ -69,9 +56,6 @@ describe('createStaticIndexPattern', () => {
it(`should create index pattern`, async () => {
const setup = {} as Setup;
- const context = getMockContext({
- 'xpack.apm.autocreateApmIndexPattern': true,
- });
// does have APM data
jest
@@ -82,7 +66,9 @@ describe('createStaticIndexPattern', () => {
await createStaticIndexPattern(
setup,
- context,
+ {
+ 'xpack.apm.autocreateApmIndexPattern': true,
+ } as APMConfig,
savedObjectsClient,
'default'
);
diff --git a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts
index b91fb8342a2123..e627e9ed1d6cf6 100644
--- a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts
+++ b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts
@@ -12,20 +12,18 @@ import {
} from '../../../../../../src/plugins/apm_oss/server';
import { hasHistoricalAgentData } from '../services/get_services/has_historical_agent_data';
import { Setup } from '../helpers/setup_request';
-import { APMRequestHandlerContext } from '../../routes/typings';
+import { APMRouteHandlerResources } from '../../routes/typings';
import { InternalSavedObjectsClient } from '../helpers/get_internal_saved_objects_client.js';
import { withApmSpan } from '../../utils/with_apm_span';
import { getApmIndexPatternTitle } from './get_apm_index_pattern_title';
export async function createStaticIndexPattern(
setup: Setup,
- context: APMRequestHandlerContext,
+ config: APMRouteHandlerResources['config'],
savedObjectsClient: InternalSavedObjectsClient,
spaceId: string | undefined
): Promise {
return withApmSpan('create_static_index_pattern', async () => {
- const { config } = context;
-
// don't autocreate APM index pattern if it's been disabled via the config
if (!config['xpack.apm.autocreateApmIndexPattern']) {
return false;
@@ -39,7 +37,7 @@ export async function createStaticIndexPattern(
}
try {
- const apmIndexPatternTitle = getApmIndexPatternTitle(context);
+ const apmIndexPatternTitle = getApmIndexPatternTitle(config);
await withApmSpan('create_index_pattern_saved_object', () =>
savedObjectsClient.create(
'index-pattern',
diff --git a/x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.ts b/x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.ts
index 41abe82de8ff27..faec64c798c7d9 100644
--- a/x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.ts
+++ b/x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.ts
@@ -5,8 +5,10 @@
* 2.0.
*/
-import { APMRequestHandlerContext } from '../../routes/typings';
+import { APMRouteHandlerResources } from '../../routes/typings';
-export function getApmIndexPatternTitle(context: APMRequestHandlerContext) {
- return context.config['apm_oss.indexPattern'];
+export function getApmIndexPatternTitle(
+ config: APMRouteHandlerResources['config']
+) {
+ return config['apm_oss.indexPattern'];
}
diff --git a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts
index 8b81101fd2f39e..8bbc22fbf289d3 100644
--- a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts
+++ b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts
@@ -5,12 +5,11 @@
* 2.0.
*/
-import LRU from 'lru-cache';
import {
IndexPatternsFetcher,
FieldDescriptor,
} from '../../../../../../src/plugins/data/server';
-import { APMRequestHandlerContext } from '../../routes/typings';
+import { APMRouteHandlerResources } from '../../routes/typings';
import { withApmSpan } from '../../utils/with_apm_span';
export interface IndexPatternTitleAndFields {
@@ -19,24 +18,14 @@ export interface IndexPatternTitleAndFields {
fields: FieldDescriptor[];
}
-const cache = new LRU({
- max: 100,
- maxAge: 1000 * 60,
-});
-
// TODO: this is currently cached globally. In the future we might want to cache this per user
export const getDynamicIndexPattern = ({
+ config,
context,
-}: {
- context: APMRequestHandlerContext;
-}) => {
+ logger,
+}: Pick) => {
return withApmSpan('get_dynamic_index_pattern', async () => {
- const indexPatternTitle = context.config['apm_oss.indexPattern'];
-
- const CACHE_KEY = `apm_dynamic_index_pattern_${indexPatternTitle}`;
- if (cache.has(CACHE_KEY)) {
- return cache.get(CACHE_KEY);
- }
+ const indexPatternTitle = config['apm_oss.indexPattern'];
const indexPatternsFetcher = new IndexPatternsFetcher(
context.core.elasticsearch.client.asCurrentUser
@@ -57,14 +46,11 @@ export const getDynamicIndexPattern = ({
title: indexPatternTitle,
};
- cache.set(CACHE_KEY, indexPattern);
return indexPattern;
} catch (e) {
- // since `getDynamicIndexPattern` can be called multiple times per request it can be expensive not to cache failed lookups
- cache.set(CACHE_KEY, undefined);
const notExists = e.output?.statusCode === 404;
if (notExists) {
- context.logger.error(
+ logger.error(
`Could not get dynamic index pattern because indices "${indexPatternTitle}" don't exist`
);
return;
diff --git a/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts b/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts
index a1587611b0a2a9..d8dbc242986a65 100644
--- a/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts
+++ b/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts
@@ -14,7 +14,7 @@ import {
APM_INDICES_SAVED_OBJECT_ID,
} from '../../../../common/apm_saved_object_constants';
import { APMConfig } from '../../..';
-import { APMRequestHandlerContext } from '../../../routes/typings';
+import { APMRouteHandlerResources } from '../../../routes/typings';
import { withApmSpan } from '../../../utils/with_apm_span';
type ISavedObjectsClient = Pick;
@@ -91,9 +91,8 @@ const APM_UI_INDICES: ApmIndicesName[] = [
export async function getApmIndexSettings({
context,
-}: {
- context: APMRequestHandlerContext;
-}) {
+ config,
+}: Pick) {
let apmIndicesSavedObject: PromiseReturnType;
try {
apmIndicesSavedObject = await getApmIndicesSavedObject(
@@ -106,7 +105,7 @@ export async function getApmIndexSettings({
throw error;
}
}
- const apmIndicesConfig = getApmIndicesConfig(context.config);
+ const apmIndicesConfig = getApmIndicesConfig(config);
return APM_UI_INDICES.map((configurationName) => ({
configurationName,
diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts
index db967946275193..074df7eaafd3cf 100644
--- a/x-pack/plugins/apm/server/plugin.ts
+++ b/x-pack/plugins/apm/server/plugin.ts
@@ -6,7 +6,7 @@
*/
import { i18n } from '@kbn/i18n';
-import { combineLatest, Observable } from 'rxjs';
+import { combineLatest } from 'rxjs';
import { map, take } from 'rxjs/operators';
import {
CoreSetup,
@@ -16,22 +16,10 @@ import {
Plugin,
PluginInitializerContext,
} from 'src/core/server';
-import { SpacesPluginSetup } from '../../spaces/server';
+import { mapValues } from 'lodash';
import { APMConfig, APMXPackConfig } from '.';
import { mergeConfigs } from './index';
-import { APMOSSPluginSetup } from '../../../../src/plugins/apm_oss/server';
-import { HomeServerPluginSetup } from '../../../../src/plugins/home/server';
-import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server';
import { UI_SETTINGS } from '../../../../src/plugins/data/common';
-import { ActionsPlugin } from '../../actions/server';
-import { AlertingPlugin } from '../../alerting/server';
-import { CloudSetup } from '../../cloud/server';
-import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
-import { LicensingPluginSetup } from '../../licensing/server';
-import { MlPluginSetup } from '../../ml/server';
-import { ObservabilityPluginSetup } from '../../observability/server';
-import { SecurityPluginSetup } from '../../security/server';
-import { TaskManagerSetupContract } from '../../task_manager/server';
import { APM_FEATURE, registerFeaturesUsage } from './feature';
import { registerApmAlerts } from './lib/alerts/register_apm_alerts';
import { createApmTelemetry } from './lib/apm_telemetry';
@@ -40,23 +28,29 @@ import { getInternalSavedObjectsClient } from './lib/helpers/get_internal_saved_
import { createApmAgentConfigurationIndex } from './lib/settings/agent_configuration/create_agent_config_index';
import { getApmIndices } from './lib/settings/apm_indices/get_apm_indices';
import { createApmCustomLinkIndex } from './lib/settings/custom_link/create_custom_link_index';
-import { createApmApi } from './routes/create_apm_api';
import { apmIndices, apmTelemetry } from './saved_objects';
import { createElasticCloudInstructions } from './tutorial/elastic_cloud';
import { uiSettings } from './ui_settings';
-import type { ApmPluginRequestHandlerContext } from './routes/typings';
-
-export interface APMPluginSetup {
- config$: Observable;
- getApmIndices: () => ReturnType;
- createApmEventClient: (params: {
- debug?: boolean;
- request: KibanaRequest;
- context: ApmPluginRequestHandlerContext;
- }) => Promise>;
-}
-
-export class APMPlugin implements Plugin {
+import type {
+ ApmPluginRequestHandlerContext,
+ APMRouteHandlerResources,
+} from './routes/typings';
+import {
+ APMPluginSetup,
+ APMPluginSetupDependencies,
+ APMPluginStartDependencies,
+} from './types';
+import { registerRoutes } from './routes/register_routes';
+import { getGlobalApmServerRouteRepository } from './routes/get_global_apm_server_route_repository';
+
+export class APMPlugin
+ implements
+ Plugin<
+ APMPluginSetup,
+ void,
+ APMPluginSetupDependencies,
+ APMPluginStartDependencies
+ > {
private currentConfig?: APMConfig;
private logger?: Logger;
constructor(private readonly initContext: PluginInitializerContext) {
@@ -64,22 +58,8 @@ export class APMPlugin implements Plugin {
}
public setup(
- core: CoreSetup,
- plugins: {
- spaces?: SpacesPluginSetup;
- apmOss: APMOSSPluginSetup;
- home: HomeServerPluginSetup;
- licensing: LicensingPluginSetup;
- cloud?: CloudSetup;
- usageCollection?: UsageCollectionSetup;
- taskManager?: TaskManagerSetupContract;
- alerting?: AlertingPlugin['setup'];
- actions?: ActionsPlugin['setup'];
- observability?: ObservabilityPluginSetup;
- features: FeaturesPluginSetup;
- security?: SecurityPluginSetup;
- ml?: MlPluginSetup;
- }
+ core: CoreSetup,
+ plugins: Omit
) {
this.logger = this.initContext.logger.get();
const config$ = this.initContext.config.create();
@@ -101,11 +81,13 @@ export class APMPlugin implements Plugin {
});
}
- this.currentConfig = mergeConfigs(
+ const currentConfig = mergeConfigs(
plugins.apmOss.config,
this.initContext.config.get()
);
+ this.currentConfig = currentConfig;
+
if (
plugins.taskManager &&
plugins.usageCollection &&
@@ -122,8 +104,8 @@ export class APMPlugin implements Plugin {
}
const ossTutorialProvider = plugins.apmOss.getRegisteredTutorialProvider();
- plugins.home.tutorials.unregisterTutorial(ossTutorialProvider);
- plugins.home.tutorials.registerTutorial(() => {
+ plugins.home?.tutorials.unregisterTutorial(ossTutorialProvider);
+ plugins.home?.tutorials.registerTutorial(() => {
const ossPart = ossTutorialProvider({});
if (this.currentConfig!['xpack.apm.ui.enabled'] && ossPart.artifacts) {
ossPart.artifacts.application = {
@@ -147,10 +129,26 @@ export class APMPlugin implements Plugin {
registerFeaturesUsage({ licensingPlugin: plugins.licensing });
- createApmApi().init(core, {
- config$: mergedConfig$,
- logger: this.logger!,
- plugins,
+ registerRoutes({
+ core: {
+ setup: core,
+ start: () => core.getStartServices().then(([coreStart]) => coreStart),
+ },
+ logger: this.logger,
+ config: currentConfig,
+ repository: getGlobalApmServerRouteRepository(),
+ plugins: mapValues(plugins, (value, key) => {
+ return {
+ setup: value,
+ start: () =>
+ core.getStartServices().then((services) => {
+ const [, pluginsStartContracts] = services;
+ return pluginsStartContracts[
+ key as keyof APMPluginStartDependencies
+ ];
+ }),
+ };
+ }) as APMRouteHandlerResources['plugins'],
});
const boundGetApmIndices = async () =>
diff --git a/x-pack/plugins/apm/server/routes/alerts/chart_preview.ts b/x-pack/plugins/apm/server/routes/alerts/chart_preview.ts
index 3bebcd49ec34a1..0175860e93d35e 100644
--- a/x-pack/plugins/apm/server/routes/alerts/chart_preview.ts
+++ b/x-pack/plugins/apm/server/routes/alerts/chart_preview.ts
@@ -10,7 +10,8 @@ import { getTransactionDurationChartPreview } from '../../lib/alerts/chart_previ
import { getTransactionErrorCountChartPreview } from '../../lib/alerts/chart_preview/get_transaction_error_count';
import { getTransactionErrorRateChartPreview } from '../../lib/alerts/chart_preview/get_transaction_error_rate';
import { setupRequest } from '../../lib/helpers/setup_request';
-import { createRoute } from '../create_route';
+import { createApmServerRoute } from '../create_apm_server_route';
+import { createApmServerRouteRepository } from '../create_apm_server_route_repository';
import { rangeRt } from '../default_api_types';
const alertParamsRt = t.intersection([
@@ -29,13 +30,14 @@ const alertParamsRt = t.intersection([
export type AlertParams = t.TypeOf;
-export const transactionErrorRateChartPreview = createRoute({
+const transactionErrorRateChartPreview = createApmServerRoute({
endpoint: 'GET /api/apm/alerts/chart_preview/transaction_error_rate',
params: t.type({ query: alertParamsRt }),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { _inspect, ...alertParams } = context.params.query;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+ const { _inspect, ...alertParams } = params.query;
const errorRateChartPreview = await getTransactionErrorRateChartPreview({
setup,
@@ -46,13 +48,16 @@ export const transactionErrorRateChartPreview = createRoute({
},
});
-export const transactionErrorCountChartPreview = createRoute({
+const transactionErrorCountChartPreview = createApmServerRoute({
endpoint: 'GET /api/apm/alerts/chart_preview/transaction_error_count',
params: t.type({ query: alertParamsRt }),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { _inspect, ...alertParams } = context.params.query;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+
+ const { _inspect, ...alertParams } = params.query;
+
const errorCountChartPreview = await getTransactionErrorCountChartPreview({
setup,
alertParams,
@@ -62,13 +67,16 @@ export const transactionErrorCountChartPreview = createRoute({
},
});
-export const transactionDurationChartPreview = createRoute({
+const transactionDurationChartPreview = createApmServerRoute({
endpoint: 'GET /api/apm/alerts/chart_preview/transaction_duration',
params: t.type({ query: alertParamsRt }),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { _inspect, ...alertParams } = context.params.query;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+
+ const { params } = resources;
+
+ const { _inspect, ...alertParams } = params.query;
const latencyChartPreview = await getTransactionDurationChartPreview({
alertParams,
@@ -78,3 +86,9 @@ export const transactionDurationChartPreview = createRoute({
return { latencyChartPreview };
},
});
+
+export const alertsChartPreviewRouteRepository = createApmServerRouteRepository()
+ .add(transactionErrorRateChartPreview)
+ .add(transactionDurationChartPreview)
+ .add(transactionErrorCountChartPreview)
+ .add(transactionDurationChartPreview);
diff --git a/x-pack/plugins/apm/server/routes/correlations.ts b/x-pack/plugins/apm/server/routes/correlations.ts
index c7c69e07748229..4728aa2e8d3f6a 100644
--- a/x-pack/plugins/apm/server/routes/correlations.ts
+++ b/x-pack/plugins/apm/server/routes/correlations.ts
@@ -14,7 +14,8 @@ import { getOverallErrorTimeseries } from '../lib/correlations/errors/get_overal
import { getCorrelationsForSlowTransactions } from '../lib/correlations/latency/get_correlations_for_slow_transactions';
import { getOverallLatencyDistribution } from '../lib/correlations/latency/get_overall_latency_distribution';
import { setupRequest } from '../lib/helpers/setup_request';
-import { createRoute } from './create_route';
+import { createApmServerRoute } from './create_apm_server_route';
+import { createApmServerRouteRepository } from './create_apm_server_route_repository';
import { environmentRt, kueryRt, rangeRt } from './default_api_types';
const INVALID_LICENSE = i18n.translate(
@@ -25,7 +26,7 @@ const INVALID_LICENSE = i18n.translate(
}
);
-export const correlationsLatencyDistributionRoute = createRoute({
+const correlationsLatencyDistributionRoute = createApmServerRoute({
endpoint: 'GET /api/apm/correlations/latency/overall_distribution',
params: t.type({
query: t.intersection([
@@ -40,18 +41,19 @@ export const correlationsLatencyDistributionRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
+ handler: async (resources) => {
+ const { context, params } = resources;
if (!isActivePlatinumLicense(context.licensing.license)) {
throw Boom.forbidden(INVALID_LICENSE);
}
- const setup = await setupRequest(context, request);
+ const setup = await setupRequest(resources);
const {
environment,
kuery,
serviceName,
transactionType,
transactionName,
- } = context.params.query;
+ } = params.query;
return getOverallLatencyDistribution({
environment,
@@ -64,7 +66,7 @@ export const correlationsLatencyDistributionRoute = createRoute({
},
});
-export const correlationsForSlowTransactionsRoute = createRoute({
+const correlationsForSlowTransactionsRoute = createApmServerRoute({
endpoint: 'GET /api/apm/correlations/latency/slow_transactions',
params: t.type({
query: t.intersection([
@@ -85,11 +87,13 @@ export const correlationsForSlowTransactionsRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
+ handler: async (resources) => {
+ const { context, params } = resources;
+
if (!isActivePlatinumLicense(context.licensing.license)) {
throw Boom.forbidden(INVALID_LICENSE);
}
- const setup = await setupRequest(context, request);
+ const setup = await setupRequest(resources);
const {
environment,
kuery,
@@ -100,7 +104,7 @@ export const correlationsForSlowTransactionsRoute = createRoute({
fieldNames,
maxLatency,
distributionInterval,
- } = context.params.query;
+ } = params.query;
return getCorrelationsForSlowTransactions({
environment,
@@ -117,7 +121,7 @@ export const correlationsForSlowTransactionsRoute = createRoute({
},
});
-export const correlationsErrorDistributionRoute = createRoute({
+const correlationsErrorDistributionRoute = createApmServerRoute({
endpoint: 'GET /api/apm/correlations/errors/overall_timeseries',
params: t.type({
query: t.intersection([
@@ -132,18 +136,20 @@ export const correlationsErrorDistributionRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
+ handler: async (resources) => {
+ const { params, context } = resources;
+
if (!isActivePlatinumLicense(context.licensing.license)) {
throw Boom.forbidden(INVALID_LICENSE);
}
- const setup = await setupRequest(context, request);
+ const setup = await setupRequest(resources);
const {
environment,
kuery,
serviceName,
transactionType,
transactionName,
- } = context.params.query;
+ } = params.query;
return getOverallErrorTimeseries({
environment,
@@ -156,7 +162,7 @@ export const correlationsErrorDistributionRoute = createRoute({
},
});
-export const correlationsForFailedTransactionsRoute = createRoute({
+const correlationsForFailedTransactionsRoute = createApmServerRoute({
endpoint: 'GET /api/apm/correlations/errors/failed_transactions',
params: t.type({
query: t.intersection([
@@ -174,11 +180,12 @@ export const correlationsForFailedTransactionsRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
+ handler: async (resources) => {
+ const { context, params } = resources;
if (!isActivePlatinumLicense(context.licensing.license)) {
throw Boom.forbidden(INVALID_LICENSE);
}
- const setup = await setupRequest(context, request);
+ const setup = await setupRequest(resources);
const {
environment,
kuery,
@@ -186,7 +193,7 @@ export const correlationsForFailedTransactionsRoute = createRoute({
transactionType,
transactionName,
fieldNames,
- } = context.params.query;
+ } = params.query;
return getCorrelationsForFailedTransactions({
environment,
@@ -199,3 +206,9 @@ export const correlationsForFailedTransactionsRoute = createRoute({
});
},
});
+
+export const correlationsRouteRepository = createApmServerRouteRepository()
+ .add(correlationsLatencyDistributionRoute)
+ .add(correlationsForSlowTransactionsRoute)
+ .add(correlationsErrorDistributionRoute)
+ .add(correlationsForFailedTransactionsRoute);
diff --git a/x-pack/plugins/apm/server/routes/create_api/index.test.ts b/x-pack/plugins/apm/server/routes/create_api/index.test.ts
deleted file mode 100644
index 9958b8dec0124a..00000000000000
--- a/x-pack/plugins/apm/server/routes/create_api/index.test.ts
+++ /dev/null
@@ -1,368 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import * as t from 'io-ts';
-import { createApi } from './index';
-import { CoreSetup, Logger } from 'src/core/server';
-import { RouteParamsRT } from '../typings';
-import { BehaviorSubject } from 'rxjs';
-import { APMConfig } from '../..';
-import { jsonRt } from '../../../common/runtime_types/json_rt';
-
-const getCoreMock = () => {
- const get = jest.fn();
- const post = jest.fn();
- const put = jest.fn();
- const createRouter = jest.fn().mockReturnValue({
- get,
- post,
- put,
- });
-
- const mock = {} as CoreSetup;
-
- return {
- mock: {
- ...mock,
- http: {
- ...mock.http,
- createRouter,
- },
- },
- get,
- post,
- put,
- createRouter,
- context: {
- measure: () => undefined,
- config$: new BehaviorSubject({} as APMConfig),
- logger: ({
- error: jest.fn(),
- } as unknown) as Logger,
- plugins: {},
- },
- };
-};
-
-const initApi = (params?: RouteParamsRT) => {
- const { mock, context, createRouter, get, post } = getCoreMock();
- const handlerMock = jest.fn();
- createApi()
- .add(() => ({
- endpoint: 'GET /foo',
- params,
- options: { tags: ['access:apm'] },
- handler: handlerMock,
- }))
- .init(mock, context);
-
- const routeHandler = get.mock.calls[0][1];
-
- const responseMock = {
- ok: jest.fn(),
- custom: jest.fn(),
- };
-
- const simulateRequest = (requestMock: any) => {
- return routeHandler(
- {},
- {
- // stub default values
- params: {},
- query: {},
- body: null,
- ...requestMock,
- },
- responseMock
- );
- };
-
- return {
- simulateRequest,
- handlerMock,
- createRouter,
- get,
- post,
- responseMock,
- };
-};
-
-describe('createApi', () => {
- it('registers a route with the server', () => {
- const { mock, context, createRouter, post, get, put } = getCoreMock();
-
- createApi()
- .add(() => ({
- endpoint: 'GET /foo',
- options: { tags: ['access:apm'] },
- handler: async () => ({}),
- }))
- .add(() => ({
- endpoint: 'POST /bar',
- params: t.type({
- body: t.string,
- }),
- options: { tags: ['access:apm'] },
- handler: async () => ({}),
- }))
- .add(() => ({
- endpoint: 'PUT /baz',
- options: {
- tags: ['access:apm', 'access:apm_write'],
- },
- handler: async () => ({}),
- }))
- .add({
- endpoint: 'GET /qux',
- options: {
- tags: ['access:apm', 'access:apm_write'],
- },
- handler: async () => ({}),
- })
- .init(mock, context);
-
- expect(createRouter).toHaveBeenCalledTimes(1);
-
- expect(get).toHaveBeenCalledTimes(2);
- expect(post).toHaveBeenCalledTimes(1);
- expect(put).toHaveBeenCalledTimes(1);
-
- expect(get.mock.calls[0][0]).toEqual({
- options: {
- tags: ['access:apm'],
- },
- path: '/foo',
- validate: expect.anything(),
- });
-
- expect(get.mock.calls[1][0]).toEqual({
- options: {
- tags: ['access:apm', 'access:apm_write'],
- },
- path: '/qux',
- validate: expect.anything(),
- });
-
- expect(post.mock.calls[0][0]).toEqual({
- options: {
- tags: ['access:apm'],
- },
- path: '/bar',
- validate: expect.anything(),
- });
-
- expect(put.mock.calls[0][0]).toEqual({
- options: {
- tags: ['access:apm', 'access:apm_write'],
- },
- path: '/baz',
- validate: expect.anything(),
- });
- });
-
- describe('when validating', () => {
- describe('_inspect', () => {
- it('allows _inspect=true', async () => {
- const { simulateRequest, handlerMock, responseMock } = initApi();
- await simulateRequest({ query: { _inspect: 'true' } });
-
- const params = handlerMock.mock.calls[0][0].context.params;
- expect(params).toEqual({ query: { _inspect: true } });
- expect(handlerMock).toHaveBeenCalledTimes(1);
-
- // responds with ok
- expect(responseMock.custom).not.toHaveBeenCalled();
- expect(responseMock.ok).toHaveBeenCalledWith({
- body: { _inspect: [] },
- });
- });
-
- it('rejects _inspect=1', async () => {
- const { simulateRequest, responseMock } = initApi();
- await simulateRequest({ query: { _inspect: 1 } });
-
- // responds with error handler
- expect(responseMock.ok).not.toHaveBeenCalled();
- expect(responseMock.custom).toHaveBeenCalledWith({
- body: {
- attributes: { _inspect: [] },
- message:
- 'Invalid value 1 supplied to : strict_keys/query: Partial<{| _inspect: pipe(JSON, boolean) |}>/_inspect: pipe(JSON, boolean)',
- },
- statusCode: 400,
- });
- });
-
- it('allows omitting _inspect', async () => {
- const { simulateRequest, handlerMock, responseMock } = initApi();
- await simulateRequest({ query: {} });
-
- const params = handlerMock.mock.calls[0][0].context.params;
- expect(params).toEqual({ query: { _inspect: false } });
- expect(handlerMock).toHaveBeenCalledTimes(1);
-
- // responds with ok
- expect(responseMock.custom).not.toHaveBeenCalled();
- expect(responseMock.ok).toHaveBeenCalledWith({ body: {} });
- });
- });
-
- it('throws if unknown parameters are provided', async () => {
- const { simulateRequest, responseMock } = initApi();
-
- await simulateRequest({
- query: { _inspect: true, extra: '' },
- });
-
- expect(responseMock.custom).toHaveBeenCalledTimes(1);
-
- await simulateRequest({
- body: { foo: 'bar' },
- });
-
- expect(responseMock.custom).toHaveBeenCalledTimes(2);
-
- await simulateRequest({
- params: {
- foo: 'bar',
- },
- });
-
- expect(responseMock.custom).toHaveBeenCalledTimes(3);
- });
-
- it('validates path parameters', async () => {
- const { simulateRequest, handlerMock, responseMock } = initApi(
- t.type({
- path: t.type({
- foo: t.string,
- }),
- })
- );
-
- await simulateRequest({
- params: {
- foo: 'bar',
- },
- });
-
- expect(handlerMock).toHaveBeenCalledTimes(1);
-
- expect(responseMock.ok).toHaveBeenCalledTimes(1);
- expect(responseMock.custom).not.toHaveBeenCalled();
-
- const params = handlerMock.mock.calls[0][0].context.params;
-
- expect(params).toEqual({
- path: {
- foo: 'bar',
- },
- query: {
- _inspect: false,
- },
- });
-
- await simulateRequest({
- params: {
- bar: 'foo',
- },
- });
-
- expect(responseMock.custom).toHaveBeenCalledTimes(1);
-
- await simulateRequest({
- params: {
- foo: 9,
- },
- });
-
- expect(responseMock.custom).toHaveBeenCalledTimes(2);
-
- await simulateRequest({
- params: {
- foo: 'bar',
- extra: '',
- },
- });
-
- expect(responseMock.custom).toHaveBeenCalledTimes(3);
- });
-
- it('validates body parameters', async () => {
- const { simulateRequest, handlerMock, responseMock } = initApi(
- t.type({
- body: t.string,
- })
- );
-
- await simulateRequest({
- body: '',
- });
-
- expect(responseMock.custom).not.toHaveBeenCalled();
- expect(handlerMock).toHaveBeenCalledTimes(1);
- expect(responseMock.ok).toHaveBeenCalledTimes(1);
-
- const params = handlerMock.mock.calls[0][0].context.params;
-
- expect(params).toEqual({
- body: '',
- query: {
- _inspect: false,
- },
- });
-
- await simulateRequest({
- body: null,
- });
-
- expect(responseMock.custom).toHaveBeenCalledTimes(1);
- });
-
- it('validates query parameters', async () => {
- const { simulateRequest, handlerMock, responseMock } = initApi(
- t.type({
- query: t.type({
- bar: t.string,
- filterNames: jsonRt.pipe(t.array(t.string)),
- }),
- })
- );
-
- await simulateRequest({
- query: {
- bar: '',
- _inspect: 'true',
- filterNames: JSON.stringify(['hostName', 'agentName']),
- },
- });
-
- expect(responseMock.custom).not.toHaveBeenCalled();
- expect(handlerMock).toHaveBeenCalledTimes(1);
- expect(responseMock.ok).toHaveBeenCalledTimes(1);
-
- const params = handlerMock.mock.calls[0][0].context.params;
-
- expect(params).toEqual({
- query: {
- bar: '',
- _inspect: true,
- filterNames: ['hostName', 'agentName'],
- },
- });
-
- await simulateRequest({
- query: {
- bar: '',
- foo: '',
- },
- });
-
- expect(responseMock.custom).toHaveBeenCalledTimes(1);
- });
- });
-});
diff --git a/x-pack/plugins/apm/server/routes/create_api/index.ts b/x-pack/plugins/apm/server/routes/create_api/index.ts
deleted file mode 100644
index 87bc97d346984b..00000000000000
--- a/x-pack/plugins/apm/server/routes/create_api/index.ts
+++ /dev/null
@@ -1,185 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { merge as mergeLodash, pickBy, isEmpty, isPlainObject } from 'lodash';
-import Boom from '@hapi/boom';
-import { schema } from '@kbn/config-schema';
-import * as t from 'io-ts';
-import { PathReporter } from 'io-ts/lib/PathReporter';
-import { isLeft } from 'fp-ts/lib/Either';
-import { KibanaRequest, RouteRegistrar } from 'src/core/server';
-import { RequestAbortedError } from '@elastic/elasticsearch/lib/errors';
-import agent from 'elastic-apm-node';
-import { parseMethod } from '../../../common/apm_api/parse_endpoint';
-import { merge } from '../../../common/runtime_types/merge';
-import { strictKeysRt } from '../../../common/runtime_types/strict_keys_rt';
-import { APMConfig } from '../..';
-import { InspectResponse, RouteParamsRT, ServerAPI } from '../typings';
-import { jsonRt } from '../../../common/runtime_types/json_rt';
-import type { ApmPluginRequestHandlerContext } from '../typings';
-
-const inspectRt = t.exact(
- t.partial({
- query: t.exact(t.partial({ _inspect: jsonRt.pipe(t.boolean) })),
- })
-);
-
-type RouteOrRouteFactoryFn = Parameters['add']>[0];
-
-const isNotEmpty = (val: any) =>
- val !== undefined && val !== null && !(isPlainObject(val) && isEmpty(val));
-
-export const inspectableEsQueriesMap = new WeakMap<
- KibanaRequest,
- InspectResponse
->();
-
-export function createApi() {
- const routes: RouteOrRouteFactoryFn[] = [];
- const api: ServerAPI<{}> = {
- _S: {},
- add(route) {
- routes.push((route as unknown) as RouteOrRouteFactoryFn);
- return this as any;
- },
- init(core, { config$, logger, plugins }) {
- const router = core.http.createRouter();
-
- let config = {} as APMConfig;
-
- config$.subscribe((val) => {
- config = val;
- });
-
- routes.forEach((routeOrFactoryFn) => {
- const route =
- typeof routeOrFactoryFn === 'function'
- ? routeOrFactoryFn(core)
- : routeOrFactoryFn;
-
- const { params, endpoint, options, handler } = route;
-
- const [method, path] = endpoint.split(' ');
- const typedRouterMethod = parseMethod(method);
-
- // For all runtime types with props, we create an exact
- // version that will strip all keys that are unvalidated.
- const anyObject = schema.object({}, { unknowns: 'allow' });
-
- (router[typedRouterMethod] as RouteRegistrar<
- typeof typedRouterMethod,
- ApmPluginRequestHandlerContext
- >)(
- {
- path,
- options,
- validate: {
- // `body` can be null, but `validate` expects non-nullable types
- // if any validation is defined. Not having validation currently
- // means we don't get the payload. See
- // https://github.com/elastic/kibana/issues/50179
- body: schema.nullable(anyObject),
- params: anyObject,
- query: anyObject,
- },
- },
- async (context, request, response) => {
- if (agent.isStarted()) {
- agent.addLabels({
- plugin: 'apm',
- });
- }
-
- // init debug queries
- inspectableEsQueriesMap.set(request, []);
-
- try {
- const validParams = validateParams(request, params);
- const data = await handler({
- request,
- context: {
- ...context,
- plugins,
- params: validParams,
- config,
- logger,
- },
- });
-
- const body = { ...data };
- if (validParams.query._inspect) {
- body._inspect = inspectableEsQueriesMap.get(request);
- }
-
- // cleanup
- inspectableEsQueriesMap.delete(request);
-
- return response.ok({ body });
- } catch (error) {
- logger.error(error);
- const opts = {
- statusCode: 500,
- body: {
- message: error.message,
- attributes: {
- _inspect: inspectableEsQueriesMap.get(request),
- },
- },
- };
-
- if (Boom.isBoom(error)) {
- opts.statusCode = error.output.statusCode;
- }
-
- if (error instanceof RequestAbortedError) {
- opts.statusCode = 499;
- opts.body.message = 'Client closed request';
- }
-
- return response.custom(opts);
- }
- }
- );
- });
- },
- };
-
- return api;
-}
-
-function validateParams(
- request: KibanaRequest,
- params: RouteParamsRT | undefined
-) {
- const paramsRt = params ? merge([params, inspectRt]) : inspectRt;
- const paramMap = pickBy(
- {
- path: request.params,
- body: request.body,
- query: {
- _inspect: 'false',
- // @ts-ignore
- ...request.query,
- },
- },
- isNotEmpty
- );
-
- const result = strictKeysRt(paramsRt).decode(paramMap);
-
- if (isLeft(result)) {
- throw Boom.badRequest(PathReporter.report(result)[0]);
- }
-
- // Only return values for parameters that have runtime types,
- // but always include query as _inspect is always set even if
- // it's not defined in the route.
- return mergeLodash(
- { query: { _inspect: false } },
- pickBy(result.right, isNotEmpty)
- );
-}
diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts
deleted file mode 100644
index 5b74aa4347f141..00000000000000
--- a/x-pack/plugins/apm/server/routes/create_apm_api.ts
+++ /dev/null
@@ -1,230 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import {
- staticIndexPatternRoute,
- dynamicIndexPatternRoute,
- apmIndexPatternTitleRoute,
-} from './index_pattern';
-import { createApi } from './create_api';
-import { environmentsRoute } from './environments';
-import {
- errorDistributionRoute,
- errorGroupsRoute,
- errorsRoute,
-} from './errors';
-import {
- serviceAgentNameRoute,
- serviceTransactionTypesRoute,
- servicesRoute,
- serviceNodeMetadataRoute,
- serviceAnnotationsRoute,
- serviceAnnotationsCreateRoute,
- serviceErrorGroupsPrimaryStatisticsRoute,
- serviceErrorGroupsComparisonStatisticsRoute,
- serviceThroughputRoute,
- serviceDependenciesRoute,
- serviceMetadataDetailsRoute,
- serviceMetadataIconsRoute,
- serviceInstancesPrimaryStatisticsRoute,
- serviceInstancesComparisonStatisticsRoute,
- serviceProfilingStatisticsRoute,
- serviceProfilingTimelineRoute,
-} from './services';
-import {
- agentConfigurationRoute,
- getSingleAgentConfigurationRoute,
- agentConfigurationSearchRoute,
- deleteAgentConfigurationRoute,
- listAgentConfigurationEnvironmentsRoute,
- listAgentConfigurationServicesRoute,
- createOrUpdateAgentConfigurationRoute,
- agentConfigurationAgentNameRoute,
-} from './settings/agent_configuration';
-import {
- apmIndexSettingsRoute,
- apmIndicesRoute,
- saveApmIndicesRoute,
-} from './settings/apm_indices';
-import { metricsChartsRoute } from './metrics';
-import { serviceNodesRoute } from './service_nodes';
-import {
- tracesRoute,
- tracesByIdRoute,
- rootTransactionByTraceIdRoute,
-} from './traces';
-import {
- correlationsLatencyDistributionRoute,
- correlationsForSlowTransactionsRoute,
- correlationsErrorDistributionRoute,
- correlationsForFailedTransactionsRoute,
-} from './correlations';
-import {
- transactionChartsBreakdownRoute,
- transactionChartsDistributionRoute,
- transactionChartsErrorRateRoute,
- transactionGroupsRoute,
- transactionGroupsPrimaryStatisticsRoute,
- transactionLatencyChartsRoute,
- transactionThroughputChartsRoute,
- transactionGroupsComparisonStatisticsRoute,
-} from './transactions';
-import { serviceMapRoute, serviceMapServiceNodeRoute } from './service_map';
-import {
- createCustomLinkRoute,
- updateCustomLinkRoute,
- deleteCustomLinkRoute,
- listCustomLinksRoute,
- customLinkTransactionRoute,
-} from './settings/custom_link';
-import {
- observabilityOverviewHasDataRoute,
- observabilityOverviewRoute,
-} from './observability_overview';
-import {
- anomalyDetectionJobsRoute,
- createAnomalyDetectionJobsRoute,
- anomalyDetectionEnvironmentsRoute,
-} from './settings/anomaly_detection';
-import {
- rumHasDataRoute,
- rumClientMetricsRoute,
- rumJSErrors,
- rumLongTaskMetrics,
- rumOverviewLocalFiltersRoute,
- rumPageLoadDistBreakdownRoute,
- rumPageLoadDistributionRoute,
- rumPageViewsTrendRoute,
- rumServicesRoute,
- rumUrlSearch,
- rumVisitorsBreakdownRoute,
- rumWebCoreVitals,
-} from './rum_client';
-import {
- transactionErrorRateChartPreview,
- transactionErrorCountChartPreview,
- transactionDurationChartPreview,
-} from './alerts/chart_preview';
-
-const createApmApi = () => {
- const api = createApi()
- // index pattern
- .add(staticIndexPatternRoute)
- .add(dynamicIndexPatternRoute)
- .add(apmIndexPatternTitleRoute)
-
- // Environments
- .add(environmentsRoute)
-
- // Errors
- .add(errorDistributionRoute)
- .add(errorGroupsRoute)
- .add(errorsRoute)
-
- // Services
- .add(serviceAgentNameRoute)
- .add(serviceTransactionTypesRoute)
- .add(servicesRoute)
- .add(serviceNodeMetadataRoute)
- .add(serviceAnnotationsRoute)
- .add(serviceAnnotationsCreateRoute)
- .add(serviceErrorGroupsPrimaryStatisticsRoute)
- .add(serviceThroughputRoute)
- .add(serviceDependenciesRoute)
- .add(serviceMetadataDetailsRoute)
- .add(serviceMetadataIconsRoute)
- .add(serviceInstancesPrimaryStatisticsRoute)
- .add(serviceInstancesComparisonStatisticsRoute)
- .add(serviceErrorGroupsComparisonStatisticsRoute)
- .add(serviceProfilingTimelineRoute)
- .add(serviceProfilingStatisticsRoute)
-
- // Agent configuration
- .add(getSingleAgentConfigurationRoute)
- .add(agentConfigurationAgentNameRoute)
- .add(agentConfigurationRoute)
- .add(agentConfigurationSearchRoute)
- .add(deleteAgentConfigurationRoute)
- .add(listAgentConfigurationEnvironmentsRoute)
- .add(listAgentConfigurationServicesRoute)
- .add(createOrUpdateAgentConfigurationRoute)
-
- // Correlations
- .add(correlationsLatencyDistributionRoute)
- .add(correlationsForSlowTransactionsRoute)
- .add(correlationsErrorDistributionRoute)
- .add(correlationsForFailedTransactionsRoute)
-
- // APM indices
- .add(apmIndexSettingsRoute)
- .add(apmIndicesRoute)
- .add(saveApmIndicesRoute)
-
- // Metrics
- .add(metricsChartsRoute)
- .add(serviceNodesRoute)
-
- // Traces
- .add(tracesRoute)
- .add(tracesByIdRoute)
- .add(rootTransactionByTraceIdRoute)
-
- // Transactions
- .add(transactionChartsBreakdownRoute)
- .add(transactionChartsDistributionRoute)
- .add(transactionChartsErrorRateRoute)
- .add(transactionGroupsRoute)
- .add(transactionGroupsPrimaryStatisticsRoute)
- .add(transactionLatencyChartsRoute)
- .add(transactionThroughputChartsRoute)
- .add(transactionGroupsComparisonStatisticsRoute)
-
- // Service map
- .add(serviceMapRoute)
- .add(serviceMapServiceNodeRoute)
-
- // Custom links
- .add(createCustomLinkRoute)
- .add(updateCustomLinkRoute)
- .add(deleteCustomLinkRoute)
- .add(listCustomLinksRoute)
- .add(customLinkTransactionRoute)
-
- // Observability dashboard
- .add(observabilityOverviewHasDataRoute)
- .add(observabilityOverviewRoute)
-
- // Anomaly detection
- .add(anomalyDetectionJobsRoute)
- .add(createAnomalyDetectionJobsRoute)
- .add(anomalyDetectionEnvironmentsRoute)
-
- // User Experience app api routes
- .add(rumOverviewLocalFiltersRoute)
- .add(rumPageViewsTrendRoute)
- .add(rumPageLoadDistributionRoute)
- .add(rumPageLoadDistBreakdownRoute)
- .add(rumClientMetricsRoute)
- .add(rumServicesRoute)
- .add(rumVisitorsBreakdownRoute)
- .add(rumWebCoreVitals)
- .add(rumJSErrors)
- .add(rumUrlSearch)
- .add(rumLongTaskMetrics)
- .add(rumHasDataRoute)
-
- // Alerting
- .add(transactionErrorCountChartPreview)
- .add(transactionDurationChartPreview)
- .add(transactionErrorRateChartPreview);
-
- return api;
-};
-
-export type APMAPI = ReturnType;
-
-export { createApmApi };
diff --git a/x-pack/plugins/apm/server/routes/create_apm_server_route.ts b/x-pack/plugins/apm/server/routes/create_apm_server_route.ts
new file mode 100644
index 00000000000000..86330a87a8c55c
--- /dev/null
+++ b/x-pack/plugins/apm/server/routes/create_apm_server_route.ts
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { createServerRouteFactory } from '@kbn/server-route-repository';
+import { APMRouteCreateOptions, APMRouteHandlerResources } from './typings';
+
+export const createApmServerRoute = createServerRouteFactory<
+ APMRouteHandlerResources,
+ APMRouteCreateOptions
+>();
diff --git a/x-pack/plugins/apm/server/routes/create_apm_server_route_repository.ts b/x-pack/plugins/apm/server/routes/create_apm_server_route_repository.ts
new file mode 100644
index 00000000000000..b7cbe890c57dba
--- /dev/null
+++ b/x-pack/plugins/apm/server/routes/create_apm_server_route_repository.ts
@@ -0,0 +1,15 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { createServerRouteRepository } from '@kbn/server-route-repository';
+import { APMRouteCreateOptions, APMRouteHandlerResources } from './typings';
+
+export function createApmServerRouteRepository() {
+ return createServerRouteRepository<
+ APMRouteHandlerResources,
+ APMRouteCreateOptions
+ >();
+}
diff --git a/x-pack/plugins/apm/server/routes/create_route.ts b/x-pack/plugins/apm/server/routes/create_route.ts
deleted file mode 100644
index d74aac0992eb4e..00000000000000
--- a/x-pack/plugins/apm/server/routes/create_route.ts
+++ /dev/null
@@ -1,29 +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
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { CoreSetup } from 'src/core/server';
-import { HandlerReturn, Route, RouteParamsRT } from './typings';
-
-export function createRoute<
- TEndpoint extends string,
- TReturn extends HandlerReturn,
- TRouteParamsRT extends RouteParamsRT | undefined = undefined
->(
- route: Route
-): Route;
-
-export function createRoute<
- TEndpoint extends string,
- TReturn extends HandlerReturn,
- TRouteParamsRT extends RouteParamsRT | undefined = undefined
->(
- route: (core: CoreSetup) => Route
-): (core: CoreSetup) => Route;
-
-export function createRoute(routeOrFactoryFn: Function | object) {
- return routeOrFactoryFn;
-}
diff --git a/x-pack/plugins/apm/server/routes/environments.ts b/x-pack/plugins/apm/server/routes/environments.ts
index 4aa7d7e6d412fa..e06fbdf7fb6d42 100644
--- a/x-pack/plugins/apm/server/routes/environments.ts
+++ b/x-pack/plugins/apm/server/routes/environments.ts
@@ -9,10 +9,11 @@ import * as t from 'io-ts';
import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions';
import { setupRequest } from '../lib/helpers/setup_request';
import { getEnvironments } from '../lib/environments/get_environments';
-import { createRoute } from './create_route';
import { rangeRt } from './default_api_types';
+import { createApmServerRoute } from './create_apm_server_route';
+import { createApmServerRouteRepository } from './create_apm_server_route_repository';
-export const environmentsRoute = createRoute({
+const environmentsRoute = createApmServerRoute({
endpoint: 'GET /api/apm/environments',
params: t.type({
query: t.intersection([
@@ -23,9 +24,10 @@ export const environmentsRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { serviceName } = context.params.query;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+ const { serviceName } = params.query;
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
);
@@ -39,3 +41,7 @@ export const environmentsRoute = createRoute({
return { environments };
},
});
+
+export const environmentsRouteRepository = createApmServerRouteRepository().add(
+ environmentsRoute
+);
diff --git a/x-pack/plugins/apm/server/routes/errors.ts b/x-pack/plugins/apm/server/routes/errors.ts
index f69d3fc9631d1c..d6bb1d4bcbaae9 100644
--- a/x-pack/plugins/apm/server/routes/errors.ts
+++ b/x-pack/plugins/apm/server/routes/errors.ts
@@ -6,14 +6,15 @@
*/
import * as t from 'io-ts';
-import { createRoute } from './create_route';
+import { createApmServerRoute } from './create_apm_server_route';
import { getErrorDistribution } from '../lib/errors/distribution/get_distribution';
import { getErrorGroupSample } from '../lib/errors/get_error_group_sample';
import { getErrorGroups } from '../lib/errors/get_error_groups';
import { setupRequest } from '../lib/helpers/setup_request';
import { environmentRt, kueryRt, rangeRt } from './default_api_types';
+import { createApmServerRouteRepository } from './create_apm_server_route_repository';
-export const errorsRoute = createRoute({
+const errorsRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services/{serviceName}/errors',
params: t.type({
path: t.type({
@@ -30,9 +31,9 @@ export const errorsRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { params } = context;
+ handler: async (resources) => {
+ const { params } = resources;
+ const setup = await setupRequest(resources);
const { serviceName } = params.path;
const { environment, kuery, sortField, sortDirection } = params.query;
@@ -49,7 +50,7 @@ export const errorsRoute = createRoute({
},
});
-export const errorGroupsRoute = createRoute({
+const errorGroupsRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services/{serviceName}/errors/{groupId}',
params: t.type({
path: t.type({
@@ -59,10 +60,11 @@ export const errorGroupsRoute = createRoute({
query: t.intersection([environmentRt, kueryRt, rangeRt]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { serviceName, groupId } = context.params.path;
- const { environment, kuery } = context.params.query;
+ handler: async (resources) => {
+ const { params } = resources;
+ const setup = await setupRequest(resources);
+ const { serviceName, groupId } = params.path;
+ const { environment, kuery } = params.query;
return getErrorGroupSample({
environment,
@@ -74,7 +76,7 @@ export const errorGroupsRoute = createRoute({
},
});
-export const errorDistributionRoute = createRoute({
+const errorDistributionRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services/{serviceName}/errors/distribution',
params: t.type({
path: t.type({
@@ -90,9 +92,9 @@ export const errorDistributionRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { params } = context;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
const { serviceName } = params.path;
const { environment, kuery, groupId } = params.query;
return getErrorDistribution({
@@ -104,3 +106,8 @@ export const errorDistributionRoute = createRoute({
});
},
});
+
+export const errorsRouteRepository = createApmServerRouteRepository()
+ .add(errorsRoute)
+ .add(errorGroupsRoute)
+ .add(errorDistributionRoute);
diff --git a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts
new file mode 100644
index 00000000000000..c151752b4b6e04
--- /dev/null
+++ b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts
@@ -0,0 +1,82 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import type {
+ ServerRouteRepository,
+ ReturnOf,
+ EndpointOf,
+} from '@kbn/server-route-repository';
+import { PickByValue } from 'utility-types';
+import { alertsChartPreviewRouteRepository } from './alerts/chart_preview';
+import { correlationsRouteRepository } from './correlations';
+import { createApmServerRouteRepository } from './create_apm_server_route_repository';
+import { environmentsRouteRepository } from './environments';
+import { errorsRouteRepository } from './errors';
+import { indexPatternRouteRepository } from './index_pattern';
+import { metricsRouteRepository } from './metrics';
+import { observabilityOverviewRouteRepository } from './observability_overview';
+import { rumRouteRepository } from './rum_client';
+import { serviceRouteRepository } from './services';
+import { serviceMapRouteRepository } from './service_map';
+import { serviceNodeRouteRepository } from './service_nodes';
+import { agentConfigurationRouteRepository } from './settings/agent_configuration';
+import { anomalyDetectionRouteRepository } from './settings/anomaly_detection';
+import { apmIndicesRouteRepository } from './settings/apm_indices';
+import { customLinkRouteRepository } from './settings/custom_link';
+import { traceRouteRepository } from './traces';
+import { transactionRouteRepository } from './transactions';
+import { APMRouteHandlerResources } from './typings';
+
+const getTypedGlobalApmServerRouteRepository = () => {
+ const repository = createApmServerRouteRepository()
+ .merge(indexPatternRouteRepository)
+ .merge(environmentsRouteRepository)
+ .merge(errorsRouteRepository)
+ .merge(metricsRouteRepository)
+ .merge(observabilityOverviewRouteRepository)
+ .merge(rumRouteRepository)
+ .merge(serviceMapRouteRepository)
+ .merge(serviceNodeRouteRepository)
+ .merge(serviceRouteRepository)
+ .merge(traceRouteRepository)
+ .merge(transactionRouteRepository)
+ .merge(alertsChartPreviewRouteRepository)
+ .merge(correlationsRouteRepository)
+ .merge(agentConfigurationRouteRepository)
+ .merge(anomalyDetectionRouteRepository)
+ .merge(apmIndicesRouteRepository)
+ .merge(customLinkRouteRepository);
+
+ return repository;
+};
+
+const getGlobalApmServerRouteRepository = () => {
+ return getTypedGlobalApmServerRouteRepository() as ServerRouteRepository;
+};
+
+export type APMServerRouteRepository = ReturnType<
+ typeof getTypedGlobalApmServerRouteRepository
+>;
+
+// Ensure no APIs return arrays (or, by proxy, the any type),
+// to guarantee compatibility with _inspect.
+
+type CompositeEndpoint = EndpointOf;
+
+type EndpointReturnTypes = {
+ [Endpoint in CompositeEndpoint]: ReturnOf;
+};
+
+type ArrayLikeReturnTypes = PickByValue;
+
+type ViolatingEndpoints = keyof ArrayLikeReturnTypes;
+
+function assertType() {}
+
+// if any endpoint has an array-like return type, the assertion below will fail
+assertType();
+
+export { getGlobalApmServerRouteRepository };
diff --git a/x-pack/plugins/apm/server/routes/index_pattern.ts b/x-pack/plugins/apm/server/routes/index_pattern.ts
index 3b800c23135ced..aa70cde4f96ae2 100644
--- a/x-pack/plugins/apm/server/routes/index_pattern.ts
+++ b/x-pack/plugins/apm/server/routes/index_pattern.ts
@@ -6,49 +6,67 @@
*/
import { createStaticIndexPattern } from '../lib/index_pattern/create_static_index_pattern';
-import { createRoute } from './create_route';
+import { createApmServerRouteRepository } from './create_apm_server_route_repository';
import { setupRequest } from '../lib/helpers/setup_request';
-import { getInternalSavedObjectsClient } from '../lib/helpers/get_internal_saved_objects_client';
import { getApmIndexPatternTitle } from '../lib/index_pattern/get_apm_index_pattern_title';
import { getDynamicIndexPattern } from '../lib/index_pattern/get_dynamic_index_pattern';
+import { createApmServerRoute } from './create_apm_server_route';
-export const staticIndexPatternRoute = createRoute((core) => ({
+const staticIndexPatternRoute = createApmServerRoute({
endpoint: 'POST /api/apm/index_pattern/static',
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
+ handler: async (resources) => {
+ const {
+ request,
+ core,
+ plugins: { spaces },
+ config,
+ } = resources;
+
const [setup, savedObjectsClient] = await Promise.all([
- setupRequest(context, request),
- getInternalSavedObjectsClient(core),
+ setupRequest(resources),
+ core
+ .start()
+ .then((coreStart) => coreStart.savedObjects.createInternalRepository()),
]);
- const spaceId = context.plugins.spaces?.spacesService.getSpaceId(request);
+ const spaceId = spaces?.setup.spacesService.getSpaceId(request);
const didCreateIndexPattern = await createStaticIndexPattern(
setup,
- context,
+ config,
savedObjectsClient,
spaceId
);
return { created: didCreateIndexPattern };
},
-}));
+});
-export const dynamicIndexPatternRoute = createRoute({
+const dynamicIndexPatternRoute = createApmServerRoute({
endpoint: 'GET /api/apm/index_pattern/dynamic',
options: { tags: ['access:apm'] },
- handler: async ({ context }) => {
- const dynamicIndexPattern = await getDynamicIndexPattern({ context });
+ handler: async ({ context, config, logger }) => {
+ const dynamicIndexPattern = await getDynamicIndexPattern({
+ context,
+ config,
+ logger,
+ });
return { dynamicIndexPattern };
},
});
-export const apmIndexPatternTitleRoute = createRoute({
+const indexPatternTitleRoute = createApmServerRoute({
endpoint: 'GET /api/apm/index_pattern/title',
options: { tags: ['access:apm'] },
- handler: async ({ context }) => {
+ handler: async ({ config }) => {
return {
- indexPatternTitle: getApmIndexPatternTitle(context),
+ indexPatternTitle: getApmIndexPatternTitle(config),
};
},
});
+
+export const indexPatternRouteRepository = createApmServerRouteRepository()
+ .add(staticIndexPatternRoute)
+ .add(dynamicIndexPatternRoute)
+ .add(indexPatternTitleRoute);
diff --git a/x-pack/plugins/apm/server/routes/metrics.ts b/x-pack/plugins/apm/server/routes/metrics.ts
index c7e82e13d07b85..9fa2346eb72fb5 100644
--- a/x-pack/plugins/apm/server/routes/metrics.ts
+++ b/x-pack/plugins/apm/server/routes/metrics.ts
@@ -8,10 +8,11 @@
import * as t from 'io-ts';
import { setupRequest } from '../lib/helpers/setup_request';
import { getMetricsChartDataByAgent } from '../lib/metrics/get_metrics_chart_data_by_agent';
-import { createRoute } from './create_route';
+import { createApmServerRoute } from './create_apm_server_route';
+import { createApmServerRouteRepository } from './create_apm_server_route_repository';
import { environmentRt, kueryRt, rangeRt } from './default_api_types';
-export const metricsChartsRoute = createRoute({
+const metricsChartsRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services/{serviceName}/metrics/charts',
params: t.type({
path: t.type({
@@ -30,9 +31,9 @@ export const metricsChartsRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { params } = context;
+ handler: async (resources) => {
+ const { params } = resources;
+ const setup = await setupRequest(resources);
const { serviceName } = params.path;
const { agentName, environment, kuery, serviceNodeName } = params.query;
return await getMetricsChartDataByAgent({
@@ -45,3 +46,7 @@ export const metricsChartsRoute = createRoute({
});
},
});
+
+export const metricsRouteRepository = createApmServerRouteRepository().add(
+ metricsChartsRoute
+);
diff --git a/x-pack/plugins/apm/server/routes/observability_overview.ts b/x-pack/plugins/apm/server/routes/observability_overview.ts
index 1aac2c09d01c5f..d459570cf73376 100644
--- a/x-pack/plugins/apm/server/routes/observability_overview.ts
+++ b/x-pack/plugins/apm/server/routes/observability_overview.ts
@@ -10,30 +10,32 @@ import { setupRequest } from '../lib/helpers/setup_request';
import { getServiceCount } from '../lib/observability_overview/get_service_count';
import { getTransactionsPerMinute } from '../lib/observability_overview/get_transactions_per_minute';
import { getHasData } from '../lib/observability_overview/has_data';
-import { createRoute } from './create_route';
import { rangeRt } from './default_api_types';
import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions';
import { withApmSpan } from '../utils/with_apm_span';
+import { createApmServerRouteRepository } from './create_apm_server_route_repository';
+import { createApmServerRoute } from './create_apm_server_route';
-export const observabilityOverviewHasDataRoute = createRoute({
+const observabilityOverviewHasDataRoute = createApmServerRoute({
endpoint: 'GET /api/apm/observability_overview/has_data',
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
const res = await getHasData({ setup });
return { hasData: res };
},
});
-export const observabilityOverviewRoute = createRoute({
+const observabilityOverviewRoute = createApmServerRoute({
endpoint: 'GET /api/apm/observability_overview',
params: t.type({
query: t.intersection([rangeRt, t.type({ bucketSize: t.string })]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { bucketSize } = context.params.query;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { bucketSize } = resources.params.query;
+
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
);
@@ -54,3 +56,7 @@ export const observabilityOverviewRoute = createRoute({
});
},
});
+
+export const observabilityOverviewRouteRepository = createApmServerRouteRepository()
+ .add(observabilityOverviewRoute)
+ .add(observabilityOverviewHasDataRoute);
diff --git a/x-pack/plugins/apm/server/routes/register_routes/index.test.ts b/x-pack/plugins/apm/server/routes/register_routes/index.test.ts
new file mode 100644
index 00000000000000..82b73d46da5c12
--- /dev/null
+++ b/x-pack/plugins/apm/server/routes/register_routes/index.test.ts
@@ -0,0 +1,507 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { jsonRt } from '@kbn/io-ts-utils';
+import { createServerRouteRepository } from '@kbn/server-route-repository';
+import { ServerRoute } from '@kbn/server-route-repository/target/typings';
+import * as t from 'io-ts';
+import { CoreSetup, Logger } from 'src/core/server';
+import { APMConfig } from '../..';
+import { APMRouteCreateOptions, APMRouteHandlerResources } from '../typings';
+import { registerRoutes } from './index';
+
+type RegisterRouteDependencies = Parameters[0];
+
+const getRegisterRouteDependencies = () => {
+ const get = jest.fn();
+ const post = jest.fn();
+ const put = jest.fn();
+ const createRouter = jest.fn().mockReturnValue({
+ get,
+ post,
+ put,
+ });
+
+ const coreSetup = ({
+ http: {
+ createRouter,
+ },
+ } as unknown) as CoreSetup;
+
+ const logger = ({
+ error: jest.fn(),
+ } as unknown) as Logger;
+
+ return {
+ mocks: {
+ get,
+ post,
+ put,
+ createRouter,
+ coreSetup,
+ logger,
+ },
+ dependencies: ({
+ core: {
+ setup: coreSetup,
+ },
+ logger,
+ config: {} as APMConfig,
+ plugins: {},
+ } as unknown) as RegisterRouteDependencies,
+ };
+};
+
+const getRepository = () =>
+ createServerRouteRepository<
+ APMRouteHandlerResources,
+ APMRouteCreateOptions
+ >();
+
+const initApi = (
+ routes: Array<
+ ServerRoute<
+ any,
+ t.Any,
+ APMRouteHandlerResources,
+ any,
+ APMRouteCreateOptions
+ >
+ >
+) => {
+ const { mocks, dependencies } = getRegisterRouteDependencies();
+
+ let repository = getRepository();
+
+ routes.forEach((route) => {
+ repository = repository.add(route);
+ });
+
+ registerRoutes({
+ ...dependencies,
+ repository,
+ });
+
+ const responseMock = {
+ ok: jest.fn(),
+ custom: jest.fn(),
+ };
+
+ const simulateRequest = (request: {
+ method: 'get' | 'post' | 'put';
+ pathname: string;
+ params?: Record;
+ body?: unknown;
+ query?: Record;
+ }) => {
+ const [, registeredRouteHandler] =
+ mocks[request.method].mock.calls.find((call) => {
+ return call[0].path === request.pathname;
+ }) ?? [];
+
+ const result = registeredRouteHandler(
+ {},
+ {
+ params: {},
+ query: {},
+ body: null,
+ ...request,
+ },
+ responseMock
+ );
+
+ return result;
+ };
+
+ return {
+ simulateRequest,
+ mocks: {
+ ...mocks,
+ response: responseMock,
+ },
+ };
+};
+
+describe('createApi', () => {
+ it('registers a route with the server', () => {
+ const {
+ mocks: { createRouter, get, post, put },
+ } = initApi([
+ {
+ endpoint: 'GET /foo',
+ options: { tags: ['access:apm'] },
+ handler: async () => ({}),
+ },
+ {
+ endpoint: 'POST /bar',
+ params: t.type({
+ body: t.string,
+ }),
+ options: { tags: ['access:apm'] },
+ handler: async () => ({}),
+ },
+ {
+ endpoint: 'PUT /baz',
+ options: {
+ tags: ['access:apm', 'access:apm_write'],
+ },
+ handler: async () => ({}),
+ },
+ {
+ endpoint: 'GET /qux',
+ options: {
+ tags: ['access:apm', 'access:apm_write'],
+ },
+ handler: async () => ({}),
+ },
+ ]);
+
+ expect(createRouter).toHaveBeenCalledTimes(1);
+
+ expect(get).toHaveBeenCalledTimes(2);
+ expect(post).toHaveBeenCalledTimes(1);
+ expect(put).toHaveBeenCalledTimes(1);
+
+ expect(get.mock.calls[0][0]).toEqual({
+ options: {
+ tags: ['access:apm'],
+ },
+ path: '/foo',
+ validate: expect.anything(),
+ });
+
+ expect(get.mock.calls[1][0]).toEqual({
+ options: {
+ tags: ['access:apm', 'access:apm_write'],
+ },
+ path: '/qux',
+ validate: expect.anything(),
+ });
+
+ expect(post.mock.calls[0][0]).toEqual({
+ options: {
+ tags: ['access:apm'],
+ },
+ path: '/bar',
+ validate: expect.anything(),
+ });
+
+ expect(put.mock.calls[0][0]).toEqual({
+ options: {
+ tags: ['access:apm', 'access:apm_write'],
+ },
+ path: '/baz',
+ validate: expect.anything(),
+ });
+ });
+
+ describe('when validating', () => {
+ describe('_inspect', () => {
+ it('allows _inspect=true', async () => {
+ const handlerMock = jest.fn();
+ const {
+ simulateRequest,
+ mocks: { response },
+ } = initApi([
+ {
+ endpoint: 'GET /foo',
+ options: {
+ tags: [],
+ },
+ handler: handlerMock,
+ },
+ ]);
+
+ await simulateRequest({
+ method: 'get',
+ pathname: '/foo',
+ query: { _inspect: 'true' },
+ });
+
+ // responds with ok
+ expect(response.custom).not.toHaveBeenCalled();
+
+ const params = handlerMock.mock.calls[0][0].params;
+ expect(params).toEqual({ query: { _inspect: true } });
+ expect(handlerMock).toHaveBeenCalledTimes(1);
+ expect(response.ok).toHaveBeenCalledWith({
+ body: { _inspect: [] },
+ });
+ });
+
+ it('rejects _inspect=1', async () => {
+ const handlerMock = jest.fn();
+
+ const {
+ simulateRequest,
+ mocks: { response },
+ } = initApi([
+ {
+ endpoint: 'GET /foo',
+ options: {
+ tags: [],
+ },
+ handler: handlerMock,
+ },
+ ]);
+ await simulateRequest({
+ method: 'get',
+ pathname: '/foo',
+ query: { _inspect: 1 },
+ });
+
+ // responds with error handler
+ expect(response.ok).not.toHaveBeenCalled();
+ expect(response.custom).toHaveBeenCalledWith({
+ body: {
+ attributes: { _inspect: [] },
+ message:
+ 'Invalid value 1 supplied to : strict_keys/query: Partial<{| _inspect: pipe(JSON, boolean) |}>/_inspect: pipe(JSON, boolean)',
+ },
+ statusCode: 400,
+ });
+ });
+
+ it('allows omitting _inspect', async () => {
+ const handlerMock = jest.fn();
+
+ const {
+ simulateRequest,
+ mocks: { response },
+ } = initApi([
+ { endpoint: 'GET /foo', options: { tags: [] }, handler: handlerMock },
+ ]);
+ await simulateRequest({
+ method: 'get',
+ pathname: '/foo',
+ query: {},
+ });
+
+ // responds with ok
+ expect(response.custom).not.toHaveBeenCalled();
+
+ const params = handlerMock.mock.calls[0][0].params;
+ expect(params).toEqual({ query: { _inspect: false } });
+ expect(handlerMock).toHaveBeenCalledTimes(1);
+
+ expect(response.ok).toHaveBeenCalledWith({ body: {} });
+ });
+ });
+
+ it('throws if unknown parameters are provided', async () => {
+ const {
+ simulateRequest,
+ mocks: { response },
+ } = initApi([
+ { endpoint: 'GET /foo', options: { tags: [] }, handler: jest.fn() },
+ ]);
+
+ await simulateRequest({
+ method: 'get',
+ pathname: '/foo',
+ query: { _inspect: 'true', extra: '' },
+ });
+
+ expect(response.custom).toHaveBeenCalledTimes(1);
+
+ await simulateRequest({
+ method: 'get',
+ pathname: '/foo',
+ body: { foo: 'bar' },
+ });
+
+ expect(response.custom).toHaveBeenCalledTimes(2);
+
+ await simulateRequest({
+ method: 'get',
+ pathname: '/foo',
+ params: {
+ foo: 'bar',
+ },
+ });
+
+ expect(response.custom).toHaveBeenCalledTimes(3);
+ });
+
+ it('validates path parameters', async () => {
+ const handlerMock = jest.fn();
+ const {
+ simulateRequest,
+ mocks: { response },
+ } = initApi([
+ {
+ endpoint: 'GET /foo',
+ options: { tags: [] },
+ params: t.type({
+ path: t.type({
+ foo: t.string,
+ }),
+ }),
+ handler: handlerMock,
+ },
+ ]);
+
+ await simulateRequest({
+ method: 'get',
+ pathname: '/foo',
+ params: {
+ foo: 'bar',
+ },
+ });
+
+ expect(handlerMock).toHaveBeenCalledTimes(1);
+
+ expect(response.ok).toHaveBeenCalledTimes(1);
+ expect(response.custom).not.toHaveBeenCalled();
+
+ const params = handlerMock.mock.calls[0][0].params;
+
+ expect(params).toEqual({
+ path: {
+ foo: 'bar',
+ },
+ query: {
+ _inspect: false,
+ },
+ });
+
+ await simulateRequest({
+ method: 'get',
+ pathname: '/foo',
+ params: {
+ bar: 'foo',
+ },
+ });
+
+ expect(response.custom).toHaveBeenCalledTimes(1);
+
+ await simulateRequest({
+ method: 'get',
+ pathname: '/foo',
+ params: {
+ foo: 9,
+ },
+ });
+
+ expect(response.custom).toHaveBeenCalledTimes(2);
+
+ await simulateRequest({
+ method: 'get',
+ pathname: '/foo',
+ params: {
+ foo: 'bar',
+ extra: '',
+ },
+ });
+
+ expect(response.custom).toHaveBeenCalledTimes(3);
+ });
+
+ it('validates body parameters', async () => {
+ const handlerMock = jest.fn();
+ const {
+ simulateRequest,
+ mocks: { response },
+ } = initApi([
+ {
+ endpoint: 'GET /foo',
+ options: {
+ tags: [],
+ },
+ params: t.type({
+ body: t.string,
+ }),
+ handler: handlerMock,
+ },
+ ]);
+
+ await simulateRequest({
+ method: 'get',
+ pathname: '/foo',
+ body: '',
+ });
+
+ expect(response.custom).not.toHaveBeenCalled();
+ expect(handlerMock).toHaveBeenCalledTimes(1);
+ expect(response.ok).toHaveBeenCalledTimes(1);
+
+ const params = handlerMock.mock.calls[0][0].params;
+
+ expect(params).toEqual({
+ body: '',
+ query: {
+ _inspect: false,
+ },
+ });
+
+ await simulateRequest({
+ method: 'get',
+ pathname: '/foo',
+ body: null,
+ });
+
+ expect(response.custom).toHaveBeenCalledTimes(1);
+ });
+
+ it('validates query parameters', async () => {
+ const handlerMock = jest.fn();
+ const {
+ simulateRequest,
+ mocks: { response },
+ } = initApi([
+ {
+ endpoint: 'GET /foo',
+ options: {
+ tags: [],
+ },
+ params: t.type({
+ query: t.type({
+ bar: t.string,
+ filterNames: jsonRt.pipe(t.array(t.string)),
+ }),
+ }),
+ handler: handlerMock,
+ },
+ ]);
+
+ await simulateRequest({
+ method: 'get',
+ pathname: '/foo',
+ query: {
+ bar: '',
+ _inspect: 'true',
+ filterNames: JSON.stringify(['hostName', 'agentName']),
+ },
+ });
+
+ expect(response.custom).not.toHaveBeenCalled();
+ expect(handlerMock).toHaveBeenCalledTimes(1);
+ expect(response.ok).toHaveBeenCalledTimes(1);
+
+ const params = handlerMock.mock.calls[0][0].params;
+
+ expect(params).toEqual({
+ query: {
+ bar: '',
+ _inspect: true,
+ filterNames: ['hostName', 'agentName'],
+ },
+ });
+
+ await simulateRequest({
+ method: 'get',
+ pathname: '/foo',
+ query: {
+ bar: '',
+ foo: '',
+ },
+ });
+
+ expect(response.custom).toHaveBeenCalledTimes(1);
+ });
+ });
+});
diff --git a/x-pack/plugins/apm/server/routes/register_routes/index.ts b/x-pack/plugins/apm/server/routes/register_routes/index.ts
new file mode 100644
index 00000000000000..3a88a496b923f5
--- /dev/null
+++ b/x-pack/plugins/apm/server/routes/register_routes/index.ts
@@ -0,0 +1,143 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import Boom from '@hapi/boom';
+import * as t from 'io-ts';
+import { KibanaRequest, RouteRegistrar } from 'src/core/server';
+import { RequestAbortedError } from '@elastic/elasticsearch/lib/errors';
+import agent from 'elastic-apm-node';
+import { ServerRouteRepository } from '@kbn/server-route-repository';
+import { merge } from 'lodash';
+import {
+ decodeRequestParams,
+ parseEndpoint,
+ routeValidationObject,
+} from '@kbn/server-route-repository';
+import { mergeRt, jsonRt } from '@kbn/io-ts-utils';
+import { pickKeys } from '../../../common/utils/pick_keys';
+import { APMRouteHandlerResources, InspectResponse } from '../typings';
+import type { ApmPluginRequestHandlerContext } from '../typings';
+
+const inspectRt = t.exact(
+ t.partial({
+ query: t.exact(t.partial({ _inspect: jsonRt.pipe(t.boolean) })),
+ })
+);
+
+export const inspectableEsQueriesMap = new WeakMap<
+ KibanaRequest,
+ InspectResponse
+>();
+
+export function registerRoutes({
+ core,
+ repository,
+ plugins,
+ logger,
+ config,
+}: {
+ core: APMRouteHandlerResources['core'];
+ plugins: APMRouteHandlerResources['plugins'];
+ logger: APMRouteHandlerResources['logger'];
+ repository: ServerRouteRepository;
+ config: APMRouteHandlerResources['config'];
+}) {
+ const routes = repository.getRoutes();
+
+ const router = core.setup.http.createRouter();
+
+ routes.forEach((route) => {
+ const { params, endpoint, options, handler } = route;
+
+ const { method, pathname } = parseEndpoint(endpoint);
+
+ (router[method] as RouteRegistrar<
+ typeof method,
+ ApmPluginRequestHandlerContext
+ >)(
+ {
+ path: pathname,
+ options,
+ validate: routeValidationObject,
+ },
+ async (context, request, response) => {
+ if (agent.isStarted()) {
+ agent.addLabels({
+ plugin: 'apm',
+ });
+ }
+
+ // init debug queries
+ inspectableEsQueriesMap.set(request, []);
+
+ try {
+ const runtimeType = params ? mergeRt(params, inspectRt) : inspectRt;
+
+ const validatedParams = decodeRequestParams(
+ pickKeys(request, 'params', 'body', 'query'),
+ runtimeType
+ );
+
+ const data: Record | undefined | null = (await handler({
+ request,
+ context,
+ config,
+ logger,
+ core,
+ plugins,
+ params: merge(
+ {
+ query: {
+ _inspect: false,
+ },
+ },
+ validatedParams
+ ),
+ })) as any;
+
+ if (Array.isArray(data)) {
+ throw new Error('Return type cannot be an array');
+ }
+
+ const body = validatedParams.query?._inspect
+ ? {
+ ...data,
+ _inspect: inspectableEsQueriesMap.get(request),
+ }
+ : { ...data };
+
+ // cleanup
+ inspectableEsQueriesMap.delete(request);
+
+ return response.ok({ body });
+ } catch (error) {
+ logger.error(error);
+ const opts = {
+ statusCode: 500,
+ body: {
+ message: error.message,
+ attributes: {
+ _inspect: inspectableEsQueriesMap.get(request),
+ },
+ },
+ };
+
+ if (Boom.isBoom(error)) {
+ opts.statusCode = error.output.statusCode;
+ }
+
+ if (error instanceof RequestAbortedError) {
+ opts.statusCode = 499;
+ opts.body.message = 'Client closed request';
+ }
+
+ return response.custom(opts);
+ }
+ }
+ );
+ });
+}
diff --git a/x-pack/plugins/apm/server/routes/rum_client.ts b/x-pack/plugins/apm/server/routes/rum_client.ts
index 3156acb469a72b..d7f91adc0d6830 100644
--- a/x-pack/plugins/apm/server/routes/rum_client.ts
+++ b/x-pack/plugins/apm/server/routes/rum_client.ts
@@ -6,7 +6,7 @@
*/
import * as t from 'io-ts';
-import { jsonRt } from '../../common/runtime_types/json_rt';
+import { jsonRt } from '@kbn/io-ts-utils';
import { LocalUIFilterName } from '../../common/ui_filter';
import {
Setup,
@@ -28,9 +28,10 @@ import { getLocalUIFilters } from '../lib/rum_client/ui_filters/local_ui_filters
import { localUIFilterNames } from '../lib/rum_client/ui_filters/local_ui_filters/config';
import { getRumPageLoadTransactionsProjection } from '../projections/rum_page_load_transactions';
import { Projection } from '../projections/typings';
-import { createRoute } from './create_route';
+import { createApmServerRoute } from './create_apm_server_route';
+import { createApmServerRouteRepository } from './create_apm_server_route_repository';
import { rangeRt } from './default_api_types';
-import { APMRequestHandlerContext } from './typings';
+import { APMRouteHandlerResources } from './typings';
export const percentileRangeRt = t.partial({
minPercentile: t.string,
@@ -45,18 +46,18 @@ const uxQueryRt = t.intersection([
t.partial({ urlQuery: t.string, percentile: t.string }),
]);
-export const rumClientMetricsRoute = createRoute({
+const rumClientMetricsRoute = createApmServerRoute({
endpoint: 'GET /api/apm/rum/client-metrics',
params: t.type({
query: uxQueryRt,
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
const {
query: { urlQuery, percentile },
- } = context.params;
+ } = resources.params;
return getClientMetrics({
setup,
@@ -66,18 +67,18 @@ export const rumClientMetricsRoute = createRoute({
},
});
-export const rumPageLoadDistributionRoute = createRoute({
+const rumPageLoadDistributionRoute = createApmServerRoute({
endpoint: 'GET /api/apm/rum-client/page-load-distribution',
params: t.type({
query: t.intersection([uxQueryRt, percentileRangeRt]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
const {
query: { minPercentile, maxPercentile, urlQuery },
- } = context.params;
+ } = resources.params;
const pageLoadDistribution = await getPageLoadDistribution({
setup,
@@ -90,7 +91,7 @@ export const rumPageLoadDistributionRoute = createRoute({
},
});
-export const rumPageLoadDistBreakdownRoute = createRoute({
+const rumPageLoadDistBreakdownRoute = createApmServerRoute({
endpoint: 'GET /api/apm/rum-client/page-load-distribution/breakdown',
params: t.type({
query: t.intersection([
@@ -100,12 +101,12 @@ export const rumPageLoadDistBreakdownRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
const {
query: { minPercentile, maxPercentile, breakdown, urlQuery },
- } = context.params;
+ } = resources.params;
const pageLoadDistBreakdown = await getPageLoadDistBreakdown({
setup,
@@ -119,18 +120,18 @@ export const rumPageLoadDistBreakdownRoute = createRoute({
},
});
-export const rumPageViewsTrendRoute = createRoute({
+const rumPageViewsTrendRoute = createApmServerRoute({
endpoint: 'GET /api/apm/rum-client/page-view-trends',
params: t.type({
query: t.intersection([uxQueryRt, t.partial({ breakdowns: t.string })]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
const {
query: { breakdowns, urlQuery },
- } = context.params;
+ } = resources.params;
return getPageViewTrends({
setup,
@@ -140,32 +141,32 @@ export const rumPageViewsTrendRoute = createRoute({
},
});
-export const rumServicesRoute = createRoute({
+const rumServicesRoute = createApmServerRoute({
endpoint: 'GET /api/apm/rum-client/services',
params: t.type({
query: t.intersection([uiFiltersRt, rangeRt]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
const rumServices = await getRumServices({ setup });
return { rumServices };
},
});
-export const rumVisitorsBreakdownRoute = createRoute({
+const rumVisitorsBreakdownRoute = createApmServerRoute({
endpoint: 'GET /api/apm/rum-client/visitor-breakdown',
params: t.type({
query: uxQueryRt,
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
const {
query: { urlQuery },
- } = context.params;
+ } = resources.params;
return getVisitorBreakdown({
setup,
@@ -174,18 +175,18 @@ export const rumVisitorsBreakdownRoute = createRoute({
},
});
-export const rumWebCoreVitals = createRoute({
+const rumWebCoreVitals = createApmServerRoute({
endpoint: 'GET /api/apm/rum-client/web-core-vitals',
params: t.type({
query: uxQueryRt,
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
const {
query: { urlQuery, percentile },
- } = context.params;
+ } = resources.params;
return getWebCoreVitals({
setup,
@@ -195,18 +196,18 @@ export const rumWebCoreVitals = createRoute({
},
});
-export const rumLongTaskMetrics = createRoute({
+const rumLongTaskMetrics = createApmServerRoute({
endpoint: 'GET /api/apm/rum-client/long-task-metrics',
params: t.type({
query: uxQueryRt,
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
const {
query: { urlQuery, percentile },
- } = context.params;
+ } = resources.params;
return getLongTaskMetrics({
setup,
@@ -216,24 +217,24 @@ export const rumLongTaskMetrics = createRoute({
},
});
-export const rumUrlSearch = createRoute({
+const rumUrlSearch = createApmServerRoute({
endpoint: 'GET /api/apm/rum-client/url-search',
params: t.type({
query: uxQueryRt,
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
const {
query: { urlQuery, percentile },
- } = context.params;
+ } = resources.params;
return getUrlSearch({ setup, urlQuery, percentile: Number(percentile) });
},
});
-export const rumJSErrors = createRoute({
+const rumJSErrors = createApmServerRoute({
endpoint: 'GET /api/apm/rum-client/js-errors',
params: t.type({
query: t.intersection([
@@ -244,12 +245,12 @@ export const rumJSErrors = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
const {
query: { pageSize, pageIndex, urlQuery },
- } = context.params;
+ } = resources.params;
return getJSErrors({
setup,
@@ -260,14 +261,14 @@ export const rumJSErrors = createRoute({
},
});
-export const rumHasDataRoute = createRoute({
+const rumHasDataRoute = createApmServerRoute({
endpoint: 'GET /api/apm/observability_overview/has_rum_data',
params: t.type({
query: t.intersection([uiFiltersRt, rangeRt]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
return await hasRumData({ setup });
},
});
@@ -309,21 +310,22 @@ function createLocalFiltersRoute<
>;
queryRt: TQueryRT;
}) {
- return createRoute({
+ return createApmServerRoute({
endpoint,
params: t.type({
query: t.intersection([localUiBaseQueryRt, queryRt]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
const { uiFilters } = setup;
- const { query } = context.params;
+
+ const { query } = resources.params;
const { filterNames } = query;
const projection = await getProjection({
query,
- context,
+ resources,
setup,
});
@@ -339,7 +341,7 @@ function createLocalFiltersRoute<
});
}
-export const rumOverviewLocalFiltersRoute = createLocalFiltersRoute({
+const rumOverviewLocalFiltersRoute = createLocalFiltersRoute({
endpoint: 'GET /api/apm/rum/local_filters',
getProjection: async ({ setup }) => {
return getRumPageLoadTransactionsProjection({
@@ -357,9 +359,23 @@ type GetProjection<
> = ({
query,
setup,
- context,
+ resources,
}: {
query: t.TypeOf;
setup: Setup & SetupTimeRange;
- context: APMRequestHandlerContext;
+ resources: APMRouteHandlerResources;
}) => Promise | TProjection;
+
+export const rumRouteRepository = createApmServerRouteRepository()
+ .add(rumClientMetricsRoute)
+ .add(rumPageLoadDistributionRoute)
+ .add(rumPageLoadDistBreakdownRoute)
+ .add(rumPageViewsTrendRoute)
+ .add(rumServicesRoute)
+ .add(rumVisitorsBreakdownRoute)
+ .add(rumWebCoreVitals)
+ .add(rumLongTaskMetrics)
+ .add(rumUrlSearch)
+ .add(rumJSErrors)
+ .add(rumHasDataRoute)
+ .add(rumOverviewLocalFiltersRoute);
diff --git a/x-pack/plugins/apm/server/routes/service_map.ts b/x-pack/plugins/apm/server/routes/service_map.ts
index 33943d6e05d01d..267479de4c102b 100644
--- a/x-pack/plugins/apm/server/routes/service_map.ts
+++ b/x-pack/plugins/apm/server/routes/service_map.ts
@@ -11,13 +11,14 @@ import { invalidLicenseMessage } from '../../common/service_map';
import { setupRequest } from '../lib/helpers/setup_request';
import { getServiceMap } from '../lib/service_map/get_service_map';
import { getServiceMapServiceNodeInfo } from '../lib/service_map/get_service_map_service_node_info';
-import { createRoute } from './create_route';
+import { createApmServerRoute } from './create_apm_server_route';
import { environmentRt, rangeRt } from './default_api_types';
import { notifyFeatureUsage } from '../feature';
import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions';
import { isActivePlatinumLicense } from '../../common/license_check';
+import { createApmServerRouteRepository } from './create_apm_server_route_repository';
-export const serviceMapRoute = createRoute({
+const serviceMapRoute = createApmServerRoute({
endpoint: 'GET /api/apm/service-map',
params: t.type({
query: t.intersection([
@@ -29,8 +30,9 @@ export const serviceMapRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- if (!context.config['xpack.apm.serviceMapEnabled']) {
+ handler: async (resources) => {
+ const { config, context, params, logger } = resources;
+ if (!config['xpack.apm.serviceMapEnabled']) {
throw Boom.notFound();
}
if (!isActivePlatinumLicense(context.licensing.license)) {
@@ -42,11 +44,10 @@ export const serviceMapRoute = createRoute({
featureName: 'serviceMaps',
});
- const logger = context.logger;
- const setup = await setupRequest(context, request);
+ const setup = await setupRequest(resources);
const {
query: { serviceName, environment },
- } = context.params;
+ } = params;
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
@@ -61,7 +62,7 @@ export const serviceMapRoute = createRoute({
},
});
-export const serviceMapServiceNodeRoute = createRoute({
+const serviceMapServiceNodeRoute = createApmServerRoute({
endpoint: 'GET /api/apm/service-map/service/{serviceName}',
params: t.type({
path: t.type({
@@ -70,19 +71,21 @@ export const serviceMapServiceNodeRoute = createRoute({
query: t.intersection([environmentRt, rangeRt]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- if (!context.config['xpack.apm.serviceMapEnabled']) {
+ handler: async (resources) => {
+ const { config, context, params } = resources;
+
+ if (!config['xpack.apm.serviceMapEnabled']) {
throw Boom.notFound();
}
if (!isActivePlatinumLicense(context.licensing.license)) {
throw Boom.forbidden(invalidLicenseMessage);
}
- const setup = await setupRequest(context, request);
+ const setup = await setupRequest(resources);
const {
path: { serviceName },
query: { environment },
- } = context.params;
+ } = params;
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
@@ -96,3 +99,7 @@ export const serviceMapServiceNodeRoute = createRoute({
});
},
});
+
+export const serviceMapRouteRepository = createApmServerRouteRepository()
+ .add(serviceMapRoute)
+ .add(serviceMapServiceNodeRoute);
diff --git a/x-pack/plugins/apm/server/routes/service_nodes.ts b/x-pack/plugins/apm/server/routes/service_nodes.ts
index e9060688c63a6e..a2eb12662cbca2 100644
--- a/x-pack/plugins/apm/server/routes/service_nodes.ts
+++ b/x-pack/plugins/apm/server/routes/service_nodes.ts
@@ -6,12 +6,13 @@
*/
import * as t from 'io-ts';
-import { createRoute } from './create_route';
+import { createApmServerRouteRepository } from './create_apm_server_route_repository';
+import { createApmServerRoute } from './create_apm_server_route';
import { setupRequest } from '../lib/helpers/setup_request';
import { getServiceNodes } from '../lib/service_nodes';
import { rangeRt, kueryRt } from './default_api_types';
-export const serviceNodesRoute = createRoute({
+const serviceNodesRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services/{serviceName}/serviceNodes',
params: t.type({
path: t.type({
@@ -20,9 +21,9 @@ export const serviceNodesRoute = createRoute({
query: t.intersection([kueryRt, rangeRt]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { params } = context;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
const { serviceName } = params.path;
const { kuery } = params.query;
@@ -30,3 +31,7 @@ export const serviceNodesRoute = createRoute({
return { serviceNodes };
},
});
+
+export const serviceNodeRouteRepository = createApmServerRouteRepository().add(
+ serviceNodesRoute
+);
diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts
index b4d25ca8b2a06e..800a5bdcc5d5ff 100644
--- a/x-pack/plugins/apm/server/routes/services.ts
+++ b/x-pack/plugins/apm/server/routes/services.ts
@@ -6,15 +6,12 @@
*/
import Boom from '@hapi/boom';
+import { jsonRt } from '@kbn/io-ts-utils';
import * as t from 'io-ts';
import { uniq } from 'lodash';
-import {
- LatencyAggregationType,
- latencyAggregationTypeRt,
-} from '../../common/latency_aggregation_types';
+import { latencyAggregationTypeRt } from '../../common/latency_aggregation_types';
import { ProfilingValueType } from '../../common/profiling';
import { isoToEpochRt } from '../../common/runtime_types/iso_to_epoch_rt';
-import { jsonRt } from '../../common/runtime_types/json_rt';
import { toNumberRt } from '../../common/runtime_types/to_number_rt';
import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions';
import { setupRequest } from '../lib/helpers/setup_request';
@@ -35,7 +32,8 @@ import { getServiceProfilingStatistics } from '../lib/services/profiling/get_ser
import { getServiceProfilingTimeline } from '../lib/services/profiling/get_service_profiling_timeline';
import { offsetPreviousPeriodCoordinates } from '../utils/offset_previous_period_coordinate';
import { withApmSpan } from '../utils/with_apm_span';
-import { createRoute } from './create_route';
+import { createApmServerRoute } from './create_apm_server_route';
+import { createApmServerRouteRepository } from './create_apm_server_route_repository';
import {
comparisonRangeRt,
environmentRt,
@@ -43,15 +41,16 @@ import {
rangeRt,
} from './default_api_types';
-export const servicesRoute = createRoute({
+const servicesRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services',
params: t.type({
query: t.intersection([environmentRt, kueryRt, rangeRt]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { environment, kuery } = context.params.query;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params, logger } = resources;
+ const { environment, kuery } = params.query;
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
);
@@ -61,21 +60,22 @@ export const servicesRoute = createRoute({
kuery,
setup,
searchAggregatedTransactions,
- logger: context.logger,
+ logger,
});
},
});
-export const serviceMetadataDetailsRoute = createRoute({
+const serviceMetadataDetailsRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services/{serviceName}/metadata/details',
params: t.type({
path: t.type({ serviceName: t.string }),
query: rangeRt,
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { serviceName } = context.params.path;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+ const { serviceName } = params.path;
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
@@ -89,16 +89,17 @@ export const serviceMetadataDetailsRoute = createRoute({
},
});
-export const serviceMetadataIconsRoute = createRoute({
+const serviceMetadataIconsRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services/{serviceName}/metadata/icons',
params: t.type({
path: t.type({ serviceName: t.string }),
query: rangeRt,
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { serviceName } = context.params.path;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+ const { serviceName } = params.path;
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
@@ -112,7 +113,7 @@ export const serviceMetadataIconsRoute = createRoute({
},
});
-export const serviceAgentNameRoute = createRoute({
+const serviceAgentNameRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services/{serviceName}/agent_name',
params: t.type({
path: t.type({
@@ -121,9 +122,10 @@ export const serviceAgentNameRoute = createRoute({
query: rangeRt,
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { serviceName } = context.params.path;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+ const { serviceName } = params.path;
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
);
@@ -136,7 +138,7 @@ export const serviceAgentNameRoute = createRoute({
},
});
-export const serviceTransactionTypesRoute = createRoute({
+const serviceTransactionTypesRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services/{serviceName}/transaction_types',
params: t.type({
path: t.type({
@@ -145,9 +147,11 @@ export const serviceTransactionTypesRoute = createRoute({
query: rangeRt,
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { serviceName } = context.params.path;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+ const { serviceName } = params.path;
+
return getServiceTransactionTypes({
serviceName,
setup,
@@ -158,7 +162,7 @@ export const serviceTransactionTypesRoute = createRoute({
},
});
-export const serviceNodeMetadataRoute = createRoute({
+const serviceNodeMetadataRoute = createApmServerRoute({
endpoint:
'GET /api/apm/services/{serviceName}/node/{serviceNodeName}/metadata',
params: t.type({
@@ -169,10 +173,11 @@ export const serviceNodeMetadataRoute = createRoute({
query: t.intersection([kueryRt, rangeRt]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { serviceName, serviceNodeName } = context.params.path;
- const { kuery } = context.params.query;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+ const { serviceName, serviceNodeName } = params.path;
+ const { kuery } = params.query;
return getServiceNodeMetadata({
kuery,
@@ -183,7 +188,7 @@ export const serviceNodeMetadataRoute = createRoute({
},
});
-export const serviceAnnotationsRoute = createRoute({
+const serviceAnnotationsRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services/{serviceName}/annotation/search',
params: t.type({
path: t.type({
@@ -192,12 +197,13 @@ export const serviceAnnotationsRoute = createRoute({
query: t.intersection([environmentRt, rangeRt]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { serviceName } = context.params.path;
- const { environment } = context.params.query;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params, plugins, context, request, logger } = resources;
+ const { serviceName } = params.path;
+ const { environment } = params.query;
- const { observability } = context.plugins;
+ const { observability } = plugins;
const [
annotationsClient,
@@ -205,7 +211,7 @@ export const serviceAnnotationsRoute = createRoute({
] = await Promise.all([
observability
? withApmSpan('get_scoped_annotations_client', () =>
- observability.getScopedAnnotationsClient(context, request)
+ observability.setup.getScopedAnnotationsClient(context, request)
)
: undefined,
getSearchAggregatedTransactions(setup),
@@ -218,12 +224,12 @@ export const serviceAnnotationsRoute = createRoute({
serviceName,
annotationsClient,
client: context.core.elasticsearch.client.asCurrentUser,
- logger: context.logger,
+ logger,
});
},
});
-export const serviceAnnotationsCreateRoute = createRoute({
+const serviceAnnotationsCreateRoute = createApmServerRoute({
endpoint: 'POST /api/apm/services/{serviceName}/annotation',
options: {
tags: ['access:apm', 'access:apm_write'],
@@ -250,12 +256,17 @@ export const serviceAnnotationsCreateRoute = createRoute({
}),
]),
}),
- handler: async ({ request, context }) => {
- const { observability } = context.plugins;
+ handler: async (resources) => {
+ const {
+ request,
+ context,
+ plugins: { observability },
+ params,
+ } = resources;
const annotationsClient = observability
? await withApmSpan('get_scoped_annotations_client', () =>
- observability.getScopedAnnotationsClient(context, request)
+ observability.setup.getScopedAnnotationsClient(context, request)
)
: undefined;
@@ -263,7 +274,7 @@ export const serviceAnnotationsCreateRoute = createRoute({
throw Boom.notFound();
}
- const { body, path } = context.params;
+ const { body, path } = params;
return withApmSpan('create_annotation', () =>
annotationsClient.create({
@@ -283,7 +294,7 @@ export const serviceAnnotationsCreateRoute = createRoute({
},
});
-export const serviceErrorGroupsPrimaryStatisticsRoute = createRoute({
+const serviceErrorGroupsPrimaryStatisticsRoute = createApmServerRoute({
endpoint:
'GET /api/apm/services/{serviceName}/error_groups/primary_statistics',
params: t.type({
@@ -300,13 +311,14 @@ export const serviceErrorGroupsPrimaryStatisticsRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
const {
path: { serviceName },
query: { kuery, transactionType, environment },
- } = context.params;
+ } = params;
return getServiceErrorGroupPrimaryStatistics({
kuery,
serviceName,
@@ -317,7 +329,7 @@ export const serviceErrorGroupsPrimaryStatisticsRoute = createRoute({
},
});
-export const serviceErrorGroupsComparisonStatisticsRoute = createRoute({
+const serviceErrorGroupsComparisonStatisticsRoute = createApmServerRoute({
endpoint:
'GET /api/apm/services/{serviceName}/error_groups/comparison_statistics',
params: t.type({
@@ -337,8 +349,9 @@ export const serviceErrorGroupsComparisonStatisticsRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
const {
path: { serviceName },
@@ -351,7 +364,7 @@ export const serviceErrorGroupsComparisonStatisticsRoute = createRoute({
comparisonStart,
comparisonEnd,
},
- } = context.params;
+ } = params;
return getServiceErrorGroupPeriods({
environment,
@@ -367,7 +380,7 @@ export const serviceErrorGroupsComparisonStatisticsRoute = createRoute({
},
});
-export const serviceThroughputRoute = createRoute({
+const serviceThroughputRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services/{serviceName}/throughput',
params: t.type({
path: t.type({
@@ -382,16 +395,17 @@ export const serviceThroughputRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { serviceName } = context.params.path;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+ const { serviceName } = params.path;
const {
environment,
kuery,
transactionType,
comparisonStart,
comparisonEnd,
- } = context.params.query;
+ } = params.query;
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
);
@@ -432,7 +446,7 @@ export const serviceThroughputRoute = createRoute({
},
});
-export const serviceInstancesPrimaryStatisticsRoute = createRoute({
+const serviceInstancesPrimaryStatisticsRoute = createApmServerRoute({
endpoint:
'GET /api/apm/services/{serviceName}/service_overview_instances/primary_statistics',
params: t.type({
@@ -450,12 +464,16 @@ export const serviceInstancesPrimaryStatisticsRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { serviceName } = context.params.path;
- const { environment, kuery, transactionType } = context.params.query;
- const latencyAggregationType = (context.params.query
- .latencyAggregationType as unknown) as LatencyAggregationType;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+ const { serviceName } = params.path;
+ const {
+ environment,
+ kuery,
+ transactionType,
+ latencyAggregationType,
+ } = params.query;
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
@@ -479,7 +497,7 @@ export const serviceInstancesPrimaryStatisticsRoute = createRoute({
},
});
-export const serviceInstancesComparisonStatisticsRoute = createRoute({
+const serviceInstancesComparisonStatisticsRoute = createApmServerRoute({
endpoint:
'GET /api/apm/services/{serviceName}/service_overview_instances/comparison_statistics',
params: t.type({
@@ -500,9 +518,10 @@ export const serviceInstancesComparisonStatisticsRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { serviceName } = context.params.path;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+ const { serviceName } = params.path;
const {
environment,
kuery,
@@ -511,9 +530,8 @@ export const serviceInstancesComparisonStatisticsRoute = createRoute({
comparisonEnd,
serviceNodeIds,
numBuckets,
- } = context.params.query;
- const latencyAggregationType = (context.params.query
- .latencyAggregationType as unknown) as LatencyAggregationType;
+ latencyAggregationType,
+ } = params.query;
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
@@ -535,7 +553,7 @@ export const serviceInstancesComparisonStatisticsRoute = createRoute({
},
});
-export const serviceDependenciesRoute = createRoute({
+const serviceDependenciesRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services/{serviceName}/dependencies',
params: t.type({
path: t.type({
@@ -552,11 +570,11 @@ export const serviceDependenciesRoute = createRoute({
options: {
tags: ['access:apm'],
},
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
-
- const { serviceName } = context.params.path;
- const { environment, numBuckets } = context.params.query;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+ const { serviceName } = params.path;
+ const { environment, numBuckets } = params.query;
const serviceDependencies = await getServiceDependencies({
serviceName,
@@ -569,7 +587,7 @@ export const serviceDependenciesRoute = createRoute({
},
});
-export const serviceProfilingTimelineRoute = createRoute({
+const serviceProfilingTimelineRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services/{serviceName}/profiling/timeline',
params: t.type({
path: t.type({
@@ -580,13 +598,13 @@ export const serviceProfilingTimelineRoute = createRoute({
options: {
tags: ['access:apm'],
},
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
-
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
const {
path: { serviceName },
query: { environment, kuery },
- } = context.params;
+ } = params;
const profilingTimeline = await getServiceProfilingTimeline({
kuery,
@@ -599,7 +617,7 @@ export const serviceProfilingTimelineRoute = createRoute({
},
});
-export const serviceProfilingStatisticsRoute = createRoute({
+const serviceProfilingStatisticsRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services/{serviceName}/profiling/statistics',
params: t.type({
path: t.type({
@@ -625,13 +643,15 @@ export const serviceProfilingStatisticsRoute = createRoute({
options: {
tags: ['access:apm'],
},
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+
+ const { params, logger } = resources;
const {
path: { serviceName },
query: { environment, kuery, valueType },
- } = context.params;
+ } = params;
return getServiceProfilingStatistics({
kuery,
@@ -639,7 +659,25 @@ export const serviceProfilingStatisticsRoute = createRoute({
environment,
valueType,
setup,
- logger: context.logger,
+ logger,
});
},
});
+
+export const serviceRouteRepository = createApmServerRouteRepository()
+ .add(servicesRoute)
+ .add(serviceMetadataDetailsRoute)
+ .add(serviceMetadataIconsRoute)
+ .add(serviceAgentNameRoute)
+ .add(serviceTransactionTypesRoute)
+ .add(serviceNodeMetadataRoute)
+ .add(serviceAnnotationsRoute)
+ .add(serviceAnnotationsCreateRoute)
+ .add(serviceErrorGroupsPrimaryStatisticsRoute)
+ .add(serviceErrorGroupsComparisonStatisticsRoute)
+ .add(serviceThroughputRoute)
+ .add(serviceInstancesPrimaryStatisticsRoute)
+ .add(serviceInstancesComparisonStatisticsRoute)
+ .add(serviceDependenciesRoute)
+ .add(serviceProfilingTimelineRoute)
+ .add(serviceProfilingStatisticsRoute);
diff --git a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts
index 31e8d6cc1e9f0d..111e0a18c8608a 100644
--- a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts
+++ b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts
@@ -16,7 +16,7 @@ import { findExactConfiguration } from '../../lib/settings/agent_configuration/f
import { listConfigurations } from '../../lib/settings/agent_configuration/list_configurations';
import { getEnvironments } from '../../lib/settings/agent_configuration/get_environments';
import { deleteConfiguration } from '../../lib/settings/agent_configuration/delete_configuration';
-import { createRoute } from '../create_route';
+import { createApmServerRoute } from '../create_apm_server_route';
import { getAgentNameByService } from '../../lib/settings/agent_configuration/get_agent_name_by_service';
import { markAppliedByAgent } from '../../lib/settings/agent_configuration/mark_applied_by_agent';
import {
@@ -24,34 +24,37 @@ import {
agentConfigurationIntakeRt,
} from '../../../common/agent_configuration/runtime_types/agent_configuration_intake_rt';
import { getSearchAggregatedTransactions } from '../../lib/helpers/aggregated_transactions';
+import { createApmServerRouteRepository } from '../create_apm_server_route_repository';
// get list of configurations
-export const agentConfigurationRoute = createRoute({
+const agentConfigurationRoute = createApmServerRoute({
endpoint: 'GET /api/apm/settings/agent-configuration',
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
const configurations = await listConfigurations({ setup });
return { configurations };
},
});
// get a single configuration
-export const getSingleAgentConfigurationRoute = createRoute({
+const getSingleAgentConfigurationRoute = createApmServerRoute({
endpoint: 'GET /api/apm/settings/agent-configuration/view',
params: t.partial({
query: serviceRt,
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { name, environment } = context.params.query;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params, logger } = resources;
+
+ const { name, environment } = params.query;
const service = { name, environment };
const config = await findExactConfiguration({ service, setup });
if (!config) {
- context.logger.info(
+ logger.info(
`Config was not found for ${service.name}/${service.environment}`
);
@@ -63,7 +66,7 @@ export const getSingleAgentConfigurationRoute = createRoute({
});
// delete configuration
-export const deleteAgentConfigurationRoute = createRoute({
+const deleteAgentConfigurationRoute = createApmServerRoute({
endpoint: 'DELETE /api/apm/settings/agent-configuration',
options: {
tags: ['access:apm', 'access:apm_write'],
@@ -73,20 +76,22 @@ export const deleteAgentConfigurationRoute = createRoute({
service: serviceRt,
}),
}),
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { service } = context.params.body;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params, logger } = resources;
+
+ const { service } = params.body;
const config = await findExactConfiguration({ service, setup });
if (!config) {
- context.logger.info(
+ logger.info(
`Config was not found for ${service.name}/${service.environment}`
);
throw Boom.notFound();
}
- context.logger.info(
+ logger.info(
`Deleting config ${service.name}/${service.environment} (${config._id})`
);
@@ -98,7 +103,7 @@ export const deleteAgentConfigurationRoute = createRoute({
});
// create/update configuration
-export const createOrUpdateAgentConfigurationRoute = createRoute({
+const createOrUpdateAgentConfigurationRoute = createApmServerRoute({
endpoint: 'PUT /api/apm/settings/agent-configuration',
options: {
tags: ['access:apm', 'access:apm_write'],
@@ -107,9 +112,10 @@ export const createOrUpdateAgentConfigurationRoute = createRoute({
t.partial({ query: t.partial({ overwrite: toBooleanRt }) }),
t.type({ body: agentConfigurationIntakeRt }),
]),
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { body, query } = context.params;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params, logger } = resources;
+ const { body, query } = params;
// if the config already exists, it is fetched and updated
// this is to avoid creating two configs with identical service params
@@ -125,13 +131,13 @@ export const createOrUpdateAgentConfigurationRoute = createRoute({
);
}
- context.logger.info(
+ logger.info(
`${config ? 'Updating' : 'Creating'} config ${body.service.name}/${
body.service.environment
}`
);
- return await createOrUpdateConfiguration({
+ await createOrUpdateConfiguration({
configurationId: config?._id,
configurationIntake: body,
setup,
@@ -147,35 +153,35 @@ const searchParamsRt = t.intersection([
export type AgentConfigSearchParams = t.TypeOf;
// Lookup single configuration (used by APM Server)
-export const agentConfigurationSearchRoute = createRoute({
+const agentConfigurationSearchRoute = createApmServerRoute({
endpoint: 'POST /api/apm/settings/agent-configuration/search',
params: t.type({
body: searchParamsRt,
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
+ handler: async (resources) => {
+ const { params, logger } = resources;
+
const {
service,
etag,
mark_as_applied_by_agent: markAsAppliedByAgent,
- } = context.params.body;
+ } = params.body;
- const setup = await setupRequest(context, request);
+ const setup = await setupRequest(resources);
const config = await searchConfigurations({
service,
setup,
});
if (!config) {
- context.logger.debug(
+ logger.debug(
`[Central configuration] Config was not found for ${service.name}/${service.environment}`
);
throw Boom.notFound();
}
- context.logger.info(
- `Config was found for ${service.name}/${service.environment}`
- );
+ logger.info(`Config was found for ${service.name}/${service.environment}`);
// update `applied_by_agent` field
// when `markAsAppliedByAgent` is true (Jaeger agent doesn't have etags)
@@ -197,11 +203,11 @@ export const agentConfigurationSearchRoute = createRoute({
*/
// get list of services
-export const listAgentConfigurationServicesRoute = createRoute({
+const listAgentConfigurationServicesRoute = createApmServerRoute({
endpoint: 'GET /api/apm/settings/agent-configuration/services',
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
);
@@ -215,15 +221,17 @@ export const listAgentConfigurationServicesRoute = createRoute({
});
// get environments for service
-export const listAgentConfigurationEnvironmentsRoute = createRoute({
+const listAgentConfigurationEnvironmentsRoute = createApmServerRoute({
endpoint: 'GET /api/apm/settings/agent-configuration/environments',
params: t.partial({
query: t.partial({ serviceName: t.string }),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { serviceName } = context.params.query;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+
+ const { serviceName } = params.query;
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
);
@@ -239,16 +247,27 @@ export const listAgentConfigurationEnvironmentsRoute = createRoute({
});
// get agentName for service
-export const agentConfigurationAgentNameRoute = createRoute({
+const agentConfigurationAgentNameRoute = createApmServerRoute({
endpoint: 'GET /api/apm/settings/agent-configuration/agent_name',
params: t.type({
query: t.type({ serviceName: t.string }),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { serviceName } = context.params.query;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+ const { serviceName } = params.query;
const agentName = await getAgentNameByService({ serviceName, setup });
return { agentName };
},
});
+
+export const agentConfigurationRouteRepository = createApmServerRouteRepository()
+ .add(agentConfigurationRoute)
+ .add(getSingleAgentConfigurationRoute)
+ .add(deleteAgentConfigurationRoute)
+ .add(createOrUpdateAgentConfigurationRoute)
+ .add(agentConfigurationSearchRoute)
+ .add(listAgentConfigurationServicesRoute)
+ .add(listAgentConfigurationEnvironmentsRoute)
+ .add(agentConfigurationAgentNameRoute);
diff --git a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts
index de7f35c4081bc3..98467e1a4a0ddd 100644
--- a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts
+++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts
@@ -9,7 +9,7 @@ import * as t from 'io-ts';
import Boom from '@hapi/boom';
import { isActivePlatinumLicense } from '../../../common/license_check';
import { ML_ERRORS } from '../../../common/anomaly_detection';
-import { createRoute } from '../create_route';
+import { createApmServerRoute } from '../create_apm_server_route';
import { getAnomalyDetectionJobs } from '../../lib/anomaly_detection/get_anomaly_detection_jobs';
import { createAnomalyDetectionJobs } from '../../lib/anomaly_detection/create_anomaly_detection_jobs';
import { setupRequest } from '../../lib/helpers/setup_request';
@@ -18,15 +18,17 @@ import { hasLegacyJobs } from '../../lib/anomaly_detection/has_legacy_jobs';
import { getSearchAggregatedTransactions } from '../../lib/helpers/aggregated_transactions';
import { notifyFeatureUsage } from '../../feature';
import { withApmSpan } from '../../utils/with_apm_span';
+import { createApmServerRouteRepository } from '../create_apm_server_route_repository';
// get ML anomaly detection jobs for each environment
-export const anomalyDetectionJobsRoute = createRoute({
+const anomalyDetectionJobsRoute = createApmServerRoute({
endpoint: 'GET /api/apm/settings/anomaly-detection/jobs',
options: {
tags: ['access:apm', 'access:ml:canGetJobs'],
},
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { context, logger } = resources;
if (!isActivePlatinumLicense(context.licensing.license)) {
throw Boom.forbidden(ML_ERRORS.INVALID_LICENSE);
@@ -34,7 +36,7 @@ export const anomalyDetectionJobsRoute = createRoute({
const [jobs, legacyJobs] = await withApmSpan('get_available_ml_jobs', () =>
Promise.all([
- getAnomalyDetectionJobs(setup, context.logger),
+ getAnomalyDetectionJobs(setup, logger),
hasLegacyJobs(setup),
])
);
@@ -47,7 +49,7 @@ export const anomalyDetectionJobsRoute = createRoute({
});
// create new ML anomaly detection jobs for each given environment
-export const createAnomalyDetectionJobsRoute = createRoute({
+const createAnomalyDetectionJobsRoute = createApmServerRoute({
endpoint: 'POST /api/apm/settings/anomaly-detection/jobs',
options: {
tags: ['access:apm', 'access:apm_write', 'access:ml:canCreateJob'],
@@ -57,15 +59,17 @@ export const createAnomalyDetectionJobsRoute = createRoute({
environments: t.array(t.string),
}),
}),
- handler: async ({ context, request }) => {
- const { environments } = context.params.body;
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const { params, context, logger } = resources;
+ const { environments } = params.body;
+
+ const setup = await setupRequest(resources);
if (!isActivePlatinumLicense(context.licensing.license)) {
throw Boom.forbidden(ML_ERRORS.INVALID_LICENSE);
}
- await createAnomalyDetectionJobs(setup, environments, context.logger);
+ await createAnomalyDetectionJobs(setup, environments, logger);
notifyFeatureUsage({
licensingPlugin: context.licensing,
@@ -77,11 +81,11 @@ export const createAnomalyDetectionJobsRoute = createRoute({
});
// get all available environments to create anomaly detection jobs for
-export const anomalyDetectionEnvironmentsRoute = createRoute({
+const anomalyDetectionEnvironmentsRoute = createApmServerRoute({
endpoint: 'GET /api/apm/settings/anomaly-detection/environments',
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
@@ -96,3 +100,8 @@ export const anomalyDetectionEnvironmentsRoute = createRoute({
return { environments };
},
});
+
+export const anomalyDetectionRouteRepository = createApmServerRouteRepository()
+ .add(anomalyDetectionJobsRoute)
+ .add(createAnomalyDetectionJobsRoute)
+ .add(anomalyDetectionEnvironmentsRoute);
diff --git a/x-pack/plugins/apm/server/routes/settings/apm_indices.ts b/x-pack/plugins/apm/server/routes/settings/apm_indices.ts
index 91057c97579e41..003471aa89f39d 100644
--- a/x-pack/plugins/apm/server/routes/settings/apm_indices.ts
+++ b/x-pack/plugins/apm/server/routes/settings/apm_indices.ts
@@ -6,7 +6,8 @@
*/
import * as t from 'io-ts';
-import { createRoute } from '../create_route';
+import { createApmServerRouteRepository } from '../create_apm_server_route_repository';
+import { createApmServerRoute } from '../create_apm_server_route';
import {
getApmIndices,
getApmIndexSettings,
@@ -14,29 +15,30 @@ import {
import { saveApmIndices } from '../../lib/settings/apm_indices/save_apm_indices';
// get list of apm indices and values
-export const apmIndexSettingsRoute = createRoute({
+const apmIndexSettingsRoute = createApmServerRoute({
endpoint: 'GET /api/apm/settings/apm-index-settings',
options: { tags: ['access:apm'] },
- handler: async ({ context }) => {
- const apmIndexSettings = await getApmIndexSettings({ context });
+ handler: async ({ config, context }) => {
+ const apmIndexSettings = await getApmIndexSettings({ config, context });
return { apmIndexSettings };
},
});
// get apm indices configuration object
-export const apmIndicesRoute = createRoute({
+const apmIndicesRoute = createApmServerRoute({
endpoint: 'GET /api/apm/settings/apm-indices',
options: { tags: ['access:apm'] },
- handler: async ({ context }) => {
+ handler: async (resources) => {
+ const { context, config } = resources;
return await getApmIndices({
savedObjectsClient: context.core.savedObjects.client,
- config: context.config,
+ config,
});
},
});
// save ui indices
-export const saveApmIndicesRoute = createRoute({
+const saveApmIndicesRoute = createApmServerRoute({
endpoint: 'POST /api/apm/settings/apm-indices/save',
options: {
tags: ['access:apm', 'access:apm_write'],
@@ -53,9 +55,15 @@ export const saveApmIndicesRoute = createRoute({
/* eslint-enable @typescript-eslint/naming-convention */
}),
}),
- handler: async ({ context }) => {
- const { body } = context.params;
+ handler: async (resources) => {
+ const { params, context } = resources;
+ const { body } = params;
const savedObjectsClient = context.core.savedObjects.client;
return await saveApmIndices(savedObjectsClient, body);
},
});
+
+export const apmIndicesRouteRepository = createApmServerRouteRepository()
+ .add(apmIndexSettingsRoute)
+ .add(apmIndicesRoute)
+ .add(saveApmIndicesRoute);
diff --git a/x-pack/plugins/apm/server/routes/settings/custom_link.ts b/x-pack/plugins/apm/server/routes/settings/custom_link.ts
index a6ab553f094199..c9c5d236c14f90 100644
--- a/x-pack/plugins/apm/server/routes/settings/custom_link.ts
+++ b/x-pack/plugins/apm/server/routes/settings/custom_link.ts
@@ -21,35 +21,40 @@ import {
import { deleteCustomLink } from '../../lib/settings/custom_link/delete_custom_link';
import { getTransaction } from '../../lib/settings/custom_link/get_transaction';
import { listCustomLinks } from '../../lib/settings/custom_link/list_custom_links';
-import { createRoute } from '../create_route';
+import { createApmServerRoute } from '../create_apm_server_route';
+import { createApmServerRouteRepository } from '../create_apm_server_route_repository';
-export const customLinkTransactionRoute = createRoute({
+const customLinkTransactionRoute = createApmServerRoute({
endpoint: 'GET /api/apm/settings/custom_links/transaction',
options: { tags: ['access:apm'] },
params: t.partial({
query: filterOptionsRt,
}),
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { query } = context.params;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+ const { query } = params;
// picks only the items listed in FILTER_OPTIONS
const filters = pick(query, FILTER_OPTIONS);
return await getTransaction({ setup, filters });
},
});
-export const listCustomLinksRoute = createRoute({
+const listCustomLinksRoute = createApmServerRoute({
endpoint: 'GET /api/apm/settings/custom_links',
options: { tags: ['access:apm'] },
params: t.partial({
query: filterOptionsRt,
}),
- handler: async ({ context, request }) => {
+ handler: async (resources) => {
+ const { context, params } = resources;
if (!isActiveGoldLicense(context.licensing.license)) {
throw Boom.forbidden(INVALID_LICENSE);
}
- const setup = await setupRequest(context, request);
- const { query } = context.params;
+ const setup = await setupRequest(resources);
+
+ const { query } = params;
+
// picks only the items listed in FILTER_OPTIONS
const filters = pick(query, FILTER_OPTIONS);
const customLinks = await listCustomLinks({ setup, filters });
@@ -57,29 +62,30 @@ export const listCustomLinksRoute = createRoute({
},
});
-export const createCustomLinkRoute = createRoute({
+const createCustomLinkRoute = createApmServerRoute({
endpoint: 'POST /api/apm/settings/custom_links',
params: t.type({
body: payloadRt,
}),
options: { tags: ['access:apm', 'access:apm_write'] },
- handler: async ({ context, request }) => {
+ handler: async (resources) => {
+ const { context, params } = resources;
if (!isActiveGoldLicense(context.licensing.license)) {
throw Boom.forbidden(INVALID_LICENSE);
}
- const setup = await setupRequest(context, request);
- const customLink = context.params.body;
- const res = await createOrUpdateCustomLink({ customLink, setup });
+ const setup = await setupRequest(resources);
+ const customLink = params.body;
notifyFeatureUsage({
licensingPlugin: context.licensing,
featureName: 'customLinks',
});
- return res;
+
+ await createOrUpdateCustomLink({ customLink, setup });
},
});
-export const updateCustomLinkRoute = createRoute({
+const updateCustomLinkRoute = createApmServerRoute({
endpoint: 'PUT /api/apm/settings/custom_links/{id}',
params: t.type({
path: t.type({
@@ -90,23 +96,26 @@ export const updateCustomLinkRoute = createRoute({
options: {
tags: ['access:apm', 'access:apm_write'],
},
- handler: async ({ context, request }) => {
+ handler: async (resources) => {
+ const { params, context } = resources;
+
if (!isActiveGoldLicense(context.licensing.license)) {
throw Boom.forbidden(INVALID_LICENSE);
}
- const setup = await setupRequest(context, request);
- const { id } = context.params.path;
- const customLink = context.params.body;
- const res = await createOrUpdateCustomLink({
+ const setup = await setupRequest(resources);
+
+ const { id } = params.path;
+ const customLink = params.body;
+
+ await createOrUpdateCustomLink({
customLinkId: id,
customLink,
setup,
});
- return res;
},
});
-export const deleteCustomLinkRoute = createRoute({
+const deleteCustomLinkRoute = createApmServerRoute({
endpoint: 'DELETE /api/apm/settings/custom_links/{id}',
params: t.type({
path: t.type({
@@ -116,12 +125,14 @@ export const deleteCustomLinkRoute = createRoute({
options: {
tags: ['access:apm', 'access:apm_write'],
},
- handler: async ({ context, request }) => {
+ handler: async (resources) => {
+ const { context, params } = resources;
+
if (!isActiveGoldLicense(context.licensing.license)) {
throw Boom.forbidden(INVALID_LICENSE);
}
- const setup = await setupRequest(context, request);
- const { id } = context.params.path;
+ const setup = await setupRequest(resources);
+ const { id } = params.path;
const res = await deleteCustomLink({
customLinkId: id,
setup,
@@ -129,3 +140,10 @@ export const deleteCustomLinkRoute = createRoute({
return res;
},
});
+
+export const customLinkRouteRepository = createApmServerRouteRepository()
+ .add(customLinkTransactionRoute)
+ .add(listCustomLinksRoute)
+ .add(createCustomLinkRoute)
+ .add(updateCustomLinkRoute)
+ .add(deleteCustomLinkRoute);
diff --git a/x-pack/plugins/apm/server/routes/traces.ts b/x-pack/plugins/apm/server/routes/traces.ts
index 6287ffbf0c7513..dd392982b02fd2 100644
--- a/x-pack/plugins/apm/server/routes/traces.ts
+++ b/x-pack/plugins/apm/server/routes/traces.ts
@@ -9,20 +9,22 @@ import * as t from 'io-ts';
import { setupRequest } from '../lib/helpers/setup_request';
import { getTrace } from '../lib/traces/get_trace';
import { getTransactionGroupList } from '../lib/transaction_groups';
-import { createRoute } from './create_route';
+import { createApmServerRoute } from './create_apm_server_route';
import { environmentRt, kueryRt, rangeRt } from './default_api_types';
import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions';
import { getRootTransactionByTraceId } from '../lib/transactions/get_transaction_by_trace';
+import { createApmServerRouteRepository } from './create_apm_server_route_repository';
-export const tracesRoute = createRoute({
+const tracesRoute = createApmServerRoute({
endpoint: 'GET /api/apm/traces',
params: t.type({
query: t.intersection([environmentRt, kueryRt, rangeRt]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { environment, kuery } = context.params.query;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+ const { environment, kuery } = params.query;
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
);
@@ -34,7 +36,7 @@ export const tracesRoute = createRoute({
},
});
-export const tracesByIdRoute = createRoute({
+const tracesByIdRoute = createApmServerRoute({
endpoint: 'GET /api/apm/traces/{traceId}',
params: t.type({
path: t.type({
@@ -43,13 +45,16 @@ export const tracesByIdRoute = createRoute({
query: rangeRt,
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- return getTrace(context.params.path.traceId, setup);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+
+ const { traceId } = params.path;
+ return getTrace(traceId, setup);
},
});
-export const rootTransactionByTraceIdRoute = createRoute({
+const rootTransactionByTraceIdRoute = createApmServerRoute({
endpoint: 'GET /api/apm/traces/{traceId}/root_transaction',
params: t.type({
path: t.type({
@@ -57,9 +62,15 @@ export const rootTransactionByTraceIdRoute = createRoute({
}),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const { traceId } = context.params.path;
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const { params } = resources;
+ const { traceId } = params.path;
+ const setup = await setupRequest(resources);
return getRootTransactionByTraceId(traceId, setup);
},
});
+
+export const traceRouteRepository = createApmServerRouteRepository()
+ .add(tracesByIdRoute)
+ .add(tracesRoute)
+ .add(rootTransactionByTraceIdRoute);
diff --git a/x-pack/plugins/apm/server/routes/transactions.ts b/x-pack/plugins/apm/server/routes/transactions.ts
index f3424a252e409e..ebca374db86d76 100644
--- a/x-pack/plugins/apm/server/routes/transactions.ts
+++ b/x-pack/plugins/apm/server/routes/transactions.ts
@@ -5,12 +5,12 @@
* 2.0.
*/
+import { jsonRt } from '@kbn/io-ts-utils';
import * as t from 'io-ts';
import {
LatencyAggregationType,
latencyAggregationTypeRt,
} from '../../common/latency_aggregation_types';
-import { jsonRt } from '../../common/runtime_types/json_rt';
import { toNumberRt } from '../../common/runtime_types/to_number_rt';
import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions';
import { setupRequest } from '../lib/helpers/setup_request';
@@ -23,7 +23,8 @@ import { getLatencyPeriods } from '../lib/transactions/get_latency_charts';
import { getThroughputCharts } from '../lib/transactions/get_throughput_charts';
import { getTransactionGroupList } from '../lib/transaction_groups';
import { getErrorRatePeriods } from '../lib/transaction_groups/get_error_rate';
-import { createRoute } from './create_route';
+import { createApmServerRoute } from './create_apm_server_route';
+import { createApmServerRouteRepository } from './create_apm_server_route_repository';
import {
comparisonRangeRt,
environmentRt,
@@ -35,7 +36,7 @@ import {
* Returns a list of transactions grouped by name
* //TODO: delete this once we moved away from the old table in the transaction overview page. It should be replaced by /transactions/groups/primary_statistics/
*/
-export const transactionGroupsRoute = createRoute({
+const transactionGroupsRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services/{serviceName}/transactions/groups',
params: t.type({
path: t.type({
@@ -49,10 +50,11 @@ export const transactionGroupsRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { serviceName } = context.params.path;
- const { environment, kuery, transactionType } = context.params.query;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+ const { serviceName } = params.path;
+ const { environment, kuery, transactionType } = params.query;
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
@@ -72,7 +74,7 @@ export const transactionGroupsRoute = createRoute({
},
});
-export const transactionGroupsPrimaryStatisticsRoute = createRoute({
+const transactionGroupsPrimaryStatisticsRoute = createApmServerRoute({
endpoint:
'GET /api/apm/services/{serviceName}/transactions/groups/primary_statistics',
params: t.type({
@@ -90,8 +92,9 @@ export const transactionGroupsPrimaryStatisticsRoute = createRoute({
options: {
tags: ['access:apm'],
},
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const { params } = resources;
+ const setup = await setupRequest(resources);
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
@@ -100,7 +103,7 @@ export const transactionGroupsPrimaryStatisticsRoute = createRoute({
const {
path: { serviceName },
query: { environment, kuery, latencyAggregationType, transactionType },
- } = context.params;
+ } = params;
return getServiceTransactionGroups({
environment,
@@ -109,12 +112,12 @@ export const transactionGroupsPrimaryStatisticsRoute = createRoute({
serviceName,
searchAggregatedTransactions,
transactionType,
- latencyAggregationType: latencyAggregationType as LatencyAggregationType,
+ latencyAggregationType,
});
},
});
-export const transactionGroupsComparisonStatisticsRoute = createRoute({
+const transactionGroupsComparisonStatisticsRoute = createApmServerRoute({
endpoint:
'GET /api/apm/services/{serviceName}/transactions/groups/comparison_statistics',
params: t.type({
@@ -135,13 +138,15 @@ export const transactionGroupsComparisonStatisticsRoute = createRoute({
options: {
tags: ['access:apm'],
},
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
);
+ const { params } = resources;
+
const {
path: { serviceName },
query: {
@@ -154,7 +159,7 @@ export const transactionGroupsComparisonStatisticsRoute = createRoute({
comparisonStart,
comparisonEnd,
},
- } = context.params;
+ } = params;
return await getServiceTransactionGroupComparisonStatisticsPeriods({
environment,
@@ -165,14 +170,14 @@ export const transactionGroupsComparisonStatisticsRoute = createRoute({
searchAggregatedTransactions,
transactionType,
numBuckets,
- latencyAggregationType: latencyAggregationType as LatencyAggregationType,
+ latencyAggregationType,
comparisonStart,
comparisonEnd,
});
},
});
-export const transactionLatencyChartsRoute = createRoute({
+const transactionLatencyChartsRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services/{serviceName}/transactions/charts/latency',
params: t.type({
path: t.type({
@@ -188,10 +193,11 @@ export const transactionLatencyChartsRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const logger = context.logger;
- const { serviceName } = context.params.path;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params, logger } = resources;
+
+ const { serviceName } = params.path;
const {
environment,
kuery,
@@ -200,7 +206,7 @@ export const transactionLatencyChartsRoute = createRoute({
latencyAggregationType,
comparisonStart,
comparisonEnd,
- } = context.params.query;
+ } = params.query;
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
@@ -242,7 +248,7 @@ export const transactionLatencyChartsRoute = createRoute({
},
});
-export const transactionThroughputChartsRoute = createRoute({
+const transactionThroughputChartsRoute = createApmServerRoute({
endpoint:
'GET /api/apm/services/{serviceName}/transactions/charts/throughput',
params: t.type({
@@ -258,15 +264,17 @@ export const transactionThroughputChartsRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { serviceName } = context.params.path;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+
+ const { serviceName } = params.path;
const {
environment,
kuery,
transactionType,
transactionName,
- } = context.params.query;
+ } = params.query;
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
@@ -284,7 +292,7 @@ export const transactionThroughputChartsRoute = createRoute({
},
});
-export const transactionChartsDistributionRoute = createRoute({
+const transactionChartsDistributionRoute = createApmServerRoute({
endpoint:
'GET /api/apm/services/{serviceName}/transactions/charts/distribution',
params: t.type({
@@ -306,9 +314,10 @@ export const transactionChartsDistributionRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { serviceName } = context.params.path;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+ const { serviceName } = params.path;
const {
environment,
kuery,
@@ -316,7 +325,7 @@ export const transactionChartsDistributionRoute = createRoute({
transactionName,
transactionId = '',
traceId = '',
- } = context.params.query;
+ } = params.query;
const searchAggregatedTransactions = await getSearchAggregatedTransactions(
setup
@@ -336,7 +345,7 @@ export const transactionChartsDistributionRoute = createRoute({
},
});
-export const transactionChartsBreakdownRoute = createRoute({
+const transactionChartsBreakdownRoute = createApmServerRoute({
endpoint: 'GET /api/apm/services/{serviceName}/transaction/charts/breakdown',
params: t.type({
path: t.type({
@@ -351,15 +360,17 @@ export const transactionChartsBreakdownRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { serviceName } = context.params.path;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+ const { params } = resources;
+
+ const { serviceName } = params.path;
const {
environment,
kuery,
transactionName,
transactionType,
- } = context.params.query;
+ } = params.query;
return getTransactionBreakdown({
environment,
@@ -372,7 +383,7 @@ export const transactionChartsBreakdownRoute = createRoute({
},
});
-export const transactionChartsErrorRateRoute = createRoute({
+const transactionChartsErrorRateRoute = createApmServerRoute({
endpoint:
'GET /api/apm/services/{serviceName}/transactions/charts/error_rate',
params: t.type({
@@ -386,9 +397,10 @@ export const transactionChartsErrorRateRoute = createRoute({
]),
}),
options: { tags: ['access:apm'] },
- handler: async ({ context, request }) => {
- const setup = await setupRequest(context, request);
- const { params } = context;
+ handler: async (resources) => {
+ const setup = await setupRequest(resources);
+
+ const { params } = resources;
const { serviceName } = params.path;
const {
environment,
@@ -416,3 +428,13 @@ export const transactionChartsErrorRateRoute = createRoute({
});
},
});
+
+export const transactionRouteRepository = createApmServerRouteRepository()
+ .add(transactionGroupsRoute)
+ .add(transactionGroupsPrimaryStatisticsRoute)
+ .add(transactionGroupsComparisonStatisticsRoute)
+ .add(transactionLatencyChartsRoute)
+ .add(transactionThroughputChartsRoute)
+ .add(transactionChartsDistributionRoute)
+ .add(transactionChartsBreakdownRoute)
+ .add(transactionChartsErrorRateRoute);
diff --git a/x-pack/plugins/apm/server/routes/typings.ts b/x-pack/plugins/apm/server/routes/typings.ts
index 3ba24b4ed52689..0fec88a4326c34 100644
--- a/x-pack/plugins/apm/server/routes/typings.ts
+++ b/x-pack/plugins/apm/server/routes/typings.ts
@@ -5,27 +5,19 @@
* 2.0.
*/
-import t, { Encode, Encoder } from 'io-ts';
import {
CoreSetup,
- KibanaRequest,
RequestHandlerContext,
Logger,
+ KibanaRequest,
+ CoreStart,
} from 'src/core/server';
-import { Observable } from 'rxjs';
-import { RequiredKeys, DeepPartial } from 'utility-types';
-import { SpacesPluginStart } from '../../../spaces/server';
-import { ObservabilityPluginSetup } from '../../../observability/server';
import { LicensingApiRequestHandlerContext } from '../../../licensing/server';
-import { SecurityPluginSetup } from '../../../security/server';
-import { MlPluginSetup } from '../../../ml/server';
-import { FetchOptions } from '../../common/fetch_options';
import { APMConfig } from '..';
+import { APMPluginDependencies } from '../types';
-export type HandlerReturn = Record;
-
-interface InspectQueryParam {
- query: { _inspect: boolean };
+export interface ApmPluginRequestHandlerContext extends RequestHandlerContext {
+ licensing: LicensingApiRequestHandlerContext;
}
export type InspectResponse = Array<{
@@ -36,141 +28,53 @@ export type InspectResponse = Array<{
esError: Error;
}>;
-export interface RouteParams {
- path?: Record;
- query?: Record;
- body?: any;
+export interface APMRouteCreateOptions {
+ options: {
+ tags: Array<
+ | 'access:apm'
+ | 'access:apm_write'
+ | 'access:ml:canGetJobs'
+ | 'access:ml:canCreateJob'
+ >;
+ };
}
-type WithoutIncompatibleMethods = Omit<
- T,
- 'encode' | 'asEncoder'
-> & { encode: Encode; asEncoder: () => Encoder };
-
-export type RouteParamsRT = WithoutIncompatibleMethods>;
-
-export type RouteHandler<
- TParamsRT extends RouteParamsRT | undefined,
- TReturn extends HandlerReturn
-> = (kibanaContext: {
- context: APMRequestHandlerContext<
- (TParamsRT extends RouteParamsRT ? t.TypeOf : {}) &
- InspectQueryParam
- >;
+export interface APMRouteHandlerResources {
request: KibanaRequest;
-}) => Promise;
-
-interface RouteOptions {
- tags: Array<
- | 'access:apm'
- | 'access:apm_write'
- | 'access:ml:canGetJobs'
- | 'access:ml:canCreateJob'
- >;
-}
-
-export interface Route<
- TEndpoint extends string,
- TRouteParamsRT extends RouteParamsRT | undefined,
- TReturn extends HandlerReturn
-> {
- endpoint: TEndpoint;
- options: RouteOptions;
- params?: TRouteParamsRT;
- handler: RouteHandler;
-}
-
-/**
- * @internal
- */
-export interface ApmPluginRequestHandlerContext extends RequestHandlerContext {
- licensing: LicensingApiRequestHandlerContext;
-}
-
-export type APMRequestHandlerContext<
- TRouteParams = {}
-> = ApmPluginRequestHandlerContext & {
- params: TRouteParams & InspectQueryParam;
+ context: ApmPluginRequestHandlerContext;
+ params: {
+ query: {
+ _inspect: boolean;
+ };
+ };
config: APMConfig;
logger: Logger;
- plugins: {
- spaces?: SpacesPluginStart;
- observability?: ObservabilityPluginSetup;
- security?: SecurityPluginSetup;
- ml?: MlPluginSetup;
+ core: {
+ setup: CoreSetup;
+ start: () => Promise;
};
-};
-
-export interface RouteState {
- [endpoint: string]: {
- params?: RouteParams;
- ret: any;
+ plugins: {
+ [key in keyof APMPluginDependencies]: {
+ setup: Required[key]['setup'];
+ start: () => Promise[key]['start']>;
+ };
};
}
-export interface ServerAPI {
- _S: TRouteState;
- add<
- TEndpoint extends string,
- TReturn extends HandlerReturn,
- TRouteParamsRT extends RouteParamsRT | undefined = undefined
- >(
- route:
- | Route
- | ((core: CoreSetup) => Route)
- ): ServerAPI<
- TRouteState &
- {
- [key in TEndpoint]: {
- params: TRouteParamsRT;
- ret: TReturn & { _inspect?: InspectResponse };
- };
- }
- >;
- init: (
- core: CoreSetup,
- context: {
- config$: Observable;
- logger: Logger;
- plugins: {
- observability?: ObservabilityPluginSetup;
- security?: SecurityPluginSetup;
- ml?: MlPluginSetup;
- };
- }
- ) => void;
-}
-
-type MaybeOptional }> = RequiredKeys<
- T['params']
-> extends never
- ? { params?: T['params'] }
- : { params: T['params'] };
-
-export type MaybeParams<
- TRouteState,
- TEndpoint extends keyof TRouteState & string
-> = TRouteState[TEndpoint] extends { params: t.Any }
- ? MaybeOptional<{
- params: t.OutputOf &
- DeepPartial;
- }>
- : {};
-
-export type Client<
- TRouteState,
- TOptions extends { abortable: boolean } = { abortable: true }
-> = (
- options: Omit<
- FetchOptions,
- 'query' | 'body' | 'pathname' | 'method' | 'signal'
- > & {
- forceCache?: boolean;
- endpoint: TEndpoint;
- } & MaybeParams &
- (TOptions extends { abortable: true } ? { signal: AbortSignal | null } : {})
-) => Promise<
- TRouteState[TEndpoint] extends { ret: any }
- ? TRouteState[TEndpoint]['ret']
- : unknown
->;
+// export type Client<
+// TRouteState,
+// TOptions extends { abortable: boolean } = { abortable: true }
+// > = (
+// options: Omit<
+// FetchOptions,
+// 'query' | 'body' | 'pathname' | 'method' | 'signal'
+// > & {
+// forceCache?: boolean;
+// endpoint: TEndpoint;
+// } & MaybeParams &
+// (TOptions extends { abortable: true } ? { signal: AbortSignal | null } : {})
+// ) => Promise<
+// TRouteState[TEndpoint] extends { ret: any }
+// ? TRouteState[TEndpoint]['ret']
+// : unknown
+// >;
diff --git a/x-pack/plugins/apm/server/types.ts b/x-pack/plugins/apm/server/types.ts
new file mode 100644
index 00000000000000..cef9eaf2f4fc05
--- /dev/null
+++ b/x-pack/plugins/apm/server/types.ts
@@ -0,0 +1,164 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { ValuesType } from 'utility-types';
+import { Observable } from 'rxjs';
+import { CoreSetup, CoreStart, KibanaRequest } from 'kibana/server';
+import {
+ PluginSetup as DataPluginSetup,
+ PluginStart as DataPluginStart,
+} from '../../../../src/plugins/data/server';
+import { SpacesPluginSetup, SpacesPluginStart } from '../../spaces/server';
+import { APMOSSPluginSetup } from '../../../../src/plugins/apm_oss/server';
+import {
+ HomeServerPluginSetup,
+ HomeServerPluginStart,
+} from '../../../../src/plugins/home/server';
+import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server';
+import { ActionsPlugin } from '../../actions/server';
+import { AlertingPlugin } from '../../alerting/server';
+import { CloudSetup } from '../../cloud/server';
+import {
+ PluginSetupContract as FeaturesPluginSetup,
+ PluginStartContract as FeaturesPluginStart,
+} from '../../features/server';
+import {
+ LicensingPluginSetup,
+ LicensingPluginStart,
+} from '../../licensing/server';
+import { MlPluginSetup, MlPluginStart } from '../../ml/server';
+import { ObservabilityPluginSetup } from '../../observability/server';
+import {
+ SecurityPluginSetup,
+ SecurityPluginStart,
+} from '../../security/server';
+import {
+ TaskManagerSetupContract,
+ TaskManagerStartContract,
+} from '../../task_manager/server';
+import { APMConfig } from '.';
+import { getApmIndices } from './lib/settings/apm_indices/get_apm_indices';
+import { createApmEventClient } from './lib/helpers/create_es_client/create_apm_event_client';
+import { ApmPluginRequestHandlerContext } from './routes/typings';
+
+export interface APMPluginSetup {
+ config$: Observable;
+ getApmIndices: () => ReturnType;
+ createApmEventClient: (params: {
+ debug?: boolean;
+ request: KibanaRequest;
+ context: ApmPluginRequestHandlerContext;
+ }) => Promise>;
+}
+
+interface DependencyMap {
+ core: {
+ setup: CoreSetup;
+ start: CoreStart;
+ };
+ spaces: {
+ setup: SpacesPluginSetup;
+ start: SpacesPluginStart;
+ };
+ apmOss: {
+ setup: APMOSSPluginSetup;
+ start: undefined;
+ };
+ home: {
+ setup: HomeServerPluginSetup;
+ start: HomeServerPluginStart;
+ };
+ licensing: {
+ setup: LicensingPluginSetup;
+ start: LicensingPluginStart;
+ };
+ cloud: {
+ setup: CloudSetup;
+ start: undefined;
+ };
+ usageCollection: {
+ setup: UsageCollectionSetup;
+ start: undefined;
+ };
+ taskManager: {
+ setup: TaskManagerSetupContract;
+ start: TaskManagerStartContract;
+ };
+ alerting: {
+ setup: AlertingPlugin['setup'];
+ start: AlertingPlugin['start'];
+ };
+ actions: {
+ setup: ActionsPlugin['setup'];
+ start: ActionsPlugin['start'];
+ };
+ observability: {
+ setup: ObservabilityPluginSetup;
+ start: undefined;
+ };
+ features: {
+ setup: FeaturesPluginSetup;
+ start: FeaturesPluginStart;
+ };
+ security: {
+ setup: SecurityPluginSetup;
+ start: SecurityPluginStart;
+ };
+ ml: {
+ setup: MlPluginSetup;
+ start: MlPluginStart;
+ };
+ data: {
+ setup: DataPluginSetup;
+ start: DataPluginStart;
+ };
+}
+
+const requiredDependencies = [
+ 'features',
+ 'apmOss',
+ 'data',
+ 'licensing',
+ 'triggersActionsUi',
+ 'embeddable',
+ 'infra',
+] as const;
+
+const optionalDependencies = [
+ 'spaces',
+ 'cloud',
+ 'usageCollection',
+ 'taskManager',
+ 'actions',
+ 'alerting',
+ 'observability',
+ 'security',
+ 'ml',
+ 'home',
+ 'maps',
+] as const;
+
+type RequiredDependencies = Pick<
+ DependencyMap,
+ ValuesType & keyof DependencyMap
+>;
+
+type OptionalDependencies = Partial<
+ Pick<
+ DependencyMap,
+ ValuesType & keyof DependencyMap
+ >
+>;
+
+export type APMPluginDependencies = RequiredDependencies & OptionalDependencies;
+
+export type APMPluginSetupDependencies = {
+ [key in keyof APMPluginDependencies]: Required[key]['setup'];
+};
+
+export type APMPluginStartDependencies = {
+ [key in keyof APMPluginDependencies]: Required[key]['start'];
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx
index fb3b771850a31f..df87f2e5230db9 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx
@@ -26,7 +26,7 @@ export const EnginesOverviewHeader: React.FC = () => {
rightSideItems={[
// eslint-disable-next-line @elastic/eui/href-or-on-click
{
{canManageEngines && (
@@ -108,6 +109,7 @@ export const EnginesOverview: React.FC = () => {
+
{
return (