diff --git a/NOTICE.txt b/NOTICE.txt
index 2341a478cbda9..4eec329b7a603 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/docs/developer/advanced/upgrading-nodejs.asciidoc b/docs/developer/advanced/upgrading-nodejs.asciidoc
index c1e727b1eac65..3827cb6e9aa7d 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.kbn_field_types.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.kbn_field_types.md
index 4d75dda61d5c9..521ceeb1e37f2 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/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 be4c3705bd8de..40fa872ff0fc6 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 5201444e69867..290dc10662569 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/getting-started/quick-start-guide.asciidoc b/docs/getting-started/quick-start-guide.asciidoc
index 1bdc9b9dea859..5e6a60f019bea 100644
--- a/docs/getting-started/quick-start-guide.asciidoc
+++ b/docs/getting-started/quick-start-guide.asciidoc
@@ -12,7 +12,7 @@ When you've finished, you'll know how to:
[float]
=== Required privileges
When security is enabled, you must have `read`, `write`, and `manage` privileges on the `kibana_sample_data_*` indices.
-For more information, refer to {ref}/security-privileges.html[Security privileges].
+Learn how to <>, or refer to {ref}/security-privileges.html[Security privileges] for more information.
[float]
[[set-up-on-cloud]]
@@ -141,3 +141,5 @@ For more information, refer to <>.
If you are you ready to add your own data, refer to <>.
If you want to ingest your data, refer to {fleet-guide}/fleet-quick-start.html[Quick start: Get logs and metrics into the Elastic Stack].
+
+If you want to secure access to your data, refer to our guide on <>
diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc
index e4d2b53a2d8d6..5d0242ae31950 100644
--- a/docs/redirects.asciidoc
+++ b/docs/redirects.asciidoc
@@ -286,3 +286,9 @@ This content has moved. See {ref}/ingest.html[Ingest pipelines].
== Timelion
This content has moved. refer to <>.
+
+
+[role="exclude",id="space-rbac-tutorial"]
+== Tutorial: Use role-based access control to customize Kibana spaces
+
+This content has moved. refer to <>.
diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc
index 643718b961650..90e813afad6f4 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/security/images/role-index-privilege.png b/docs/user/security/images/role-index-privilege.png
deleted file mode 100644
index 1dc1ae640e3ba..0000000000000
Binary files a/docs/user/security/images/role-index-privilege.png and /dev/null differ
diff --git a/docs/user/security/images/role-management.png b/docs/user/security/images/role-management.png
deleted file mode 100644
index 29efdd85c4df3..0000000000000
Binary files a/docs/user/security/images/role-management.png and /dev/null differ
diff --git a/docs/user/security/images/role-new-user.png b/docs/user/security/images/role-new-user.png
deleted file mode 100644
index c882eeea42d60..0000000000000
Binary files a/docs/user/security/images/role-new-user.png and /dev/null differ
diff --git a/docs/user/security/images/role-space-visualization.png b/docs/user/security/images/role-space-visualization.png
deleted file mode 100644
index 36f83f09f064b..0000000000000
Binary files a/docs/user/security/images/role-space-visualization.png and /dev/null differ
diff --git a/docs/user/security/images/tutorial-secure-access-example-1-role.png b/docs/user/security/images/tutorial-secure-access-example-1-role.png
new file mode 100644
index 0000000000000..53540da7170ea
Binary files /dev/null and b/docs/user/security/images/tutorial-secure-access-example-1-role.png differ
diff --git a/docs/user/security/images/tutorial-secure-access-example-1-space.png b/docs/user/security/images/tutorial-secure-access-example-1-space.png
new file mode 100644
index 0000000000000..a48fdeaa6efa1
Binary files /dev/null and b/docs/user/security/images/tutorial-secure-access-example-1-space.png differ
diff --git a/docs/user/security/images/tutorial-secure-access-example-1-test.png b/docs/user/security/images/tutorial-secure-access-example-1-test.png
new file mode 100644
index 0000000000000..305b97017a9d8
Binary files /dev/null and b/docs/user/security/images/tutorial-secure-access-example-1-test.png differ
diff --git a/docs/user/security/images/tutorial-secure-access-example-1-user.png b/docs/user/security/images/tutorial-secure-access-example-1-user.png
new file mode 100644
index 0000000000000..8df26cf28ef16
Binary files /dev/null and b/docs/user/security/images/tutorial-secure-access-example-1-user.png differ
diff --git a/docs/user/security/index.asciidoc b/docs/user/security/index.asciidoc
index 6a5c4a83aa3ad..71c5bd268a67d 100644
--- a/docs/user/security/index.asciidoc
+++ b/docs/user/security/index.asciidoc
@@ -47,4 +47,3 @@ include::authorization/kibana-privileges.asciidoc[]
include::api-keys/index.asciidoc[]
include::encryption-keys/index.asciidoc[]
include::role-mappings/index.asciidoc[]
-include::rbac_tutorial.asciidoc[]
diff --git a/docs/user/security/rbac_tutorial.asciidoc b/docs/user/security/rbac_tutorial.asciidoc
deleted file mode 100644
index 6324539c3c10a..0000000000000
--- a/docs/user/security/rbac_tutorial.asciidoc
+++ /dev/null
@@ -1,105 +0,0 @@
-[[space-rbac-tutorial]]
-=== Tutorial: Use role-based access control to customize Kibana spaces
-
-With role-based access control (RBAC), you can provide users access to data, tools,
-and Kibana spaces. In this tutorial, you will learn how to configure roles
-that provide the right users with the right access to the data, tools, and
-Kibana spaces.
-
-[float]
-==== Scenario
-
-Our user is a web developer working on a bank's
-online mortgage service. The web developer has these
-three requirements:
-
-* Have access to the data for that service
-* Build visualizations and dashboards
-* Monitor the performance of the system
-
-You'll provide the web developer with the access and privileges to get the job done.
-
-[float]
-==== Prerequisites
-
-To complete this tutorial, you'll need the following:
-
-* **Administrative privileges**: You must have a role that grants privileges to create a space, role, and user. This is any role which grants the `manage_security` cluster privilege. By default, the `superuser` role provides this access. See the {ref}/built-in-roles.html[built-in] roles.
-* **A space**: In this tutorial, use `Dev Mortgage` as the space
-name. See <> for
-details on creating a space.
-* **Data**: You can use <> or
-live data. In the following steps, Filebeat and Metricbeat data are used.
-
-[float]
-==== Steps
-
-With the requirements in mind, here are the steps that you will work
-through in this tutorial:
-
-* Create a role named `mortgage-developer`
-* Give the role permission to access the data in the relevant indices
-* Give the role permission to create visualizations and dashboards
-* Create the web developer's user account with the proper roles
-
-[float]
-==== Create a role
-
-Open the main menu, then click *Stack Management > Roles*
-for an overview of your roles. This view provides actions
-for you to create, edit, and delete roles.
-
-[role="screenshot"]
-image::security/images/role-management.png["Role management"]
-
-
-You can create as many roles as you like. Click *Create role* and
-provide a name. Use `dev-mortgage` because this role is for a developer
-working on the bank's mortgage application.
-
-
-[float]
-==== Give the role permission to access the data
-
-Access to data in indices is an index-level privilege, so in
-*Index privileges*, add lines for the indices that contain the
-data for this role. Two privileges are required: `read` and
-`view_index_metadata`. All privileges are detailed in the
-https://www.elastic.co/guide/en/elasticsearch/reference/current/security-privileges.html[security privileges] documentation.
-
-In the screenshots, Filebeat and Metricbeat data is used, but you
-should use the index patterns for your indices.
-
-[role="screenshot"]
-image::security/images/role-index-privilege.png["Index privilege"]
-
-[float]
-==== Give the role permissions to {kib} apps
-
-To enable users to create dashboards, visualizations, and saved searches, add {kib} privileges to the `dev-mortgage` role.
-
-. On the *{kib} privileges* window, select *Dev Mortgage* from the *Space* dropdown.
-
-. Click **Add space privilege**.
-
-. For *Dashboard*, *Visualize Library*, and *Discover*, click *All*.
-+
-It is common to create saved searches in *Discover* while creating visualizations.
-+
-[role="screenshot"]
-image::security/images/role-space-visualization.png["Associate space"]
-
-[float]
-==== Create the developer user account with the proper roles
-
-. Open the main menu, then click *Stack Management > Users*.
-. Click **Create user**, then give the user the `dev-mortgage`
-and `monitoring-user` roles, which are required for *Stack Monitoring* users.
-
-[role="screenshot"]
-image::security/images/role-new-user.png["Developer user"]
-
-Finally, have the developer log in and access the Dev Mortgage space
-and create a new visualization.
-
-NOTE: If the user is assigned to only one space, they will automatically enter that space on login.
diff --git a/docs/user/security/tutorials/how-to-secure-access-to-kibana.asciidoc b/docs/user/security/tutorials/how-to-secure-access-to-kibana.asciidoc
new file mode 100644
index 0000000000000..63b83712e3e6e
--- /dev/null
+++ b/docs/user/security/tutorials/how-to-secure-access-to-kibana.asciidoc
@@ -0,0 +1,136 @@
+[[tutorial-secure-access-to-kibana]]
+== Securing access to {kib}
+
+
+{kib} is home to an ever-growing suite of powerful features, which help you get the most out of your data. Your data is important, and should be protected. {kib} allows you to secure access to your data and control how users are able to interact with your data.
+
+For example, some users might only need to view your stunning dashboards, while others might need to manage your fleet of Elastic agents and run machine learning jobs to detect anomalous behavior in your network.
+
+This guide introduces you to three of {kib}'s security features: spaces, roles, and users. By the end of this tutorial, you will learn how to manage these entities, and how you can leverage them to secure access to both {kib} and your data.
+
+[float]
+=== Spaces
+
+Do you have multiple teams using {kib}? Do you want a “playground” to experiment with new visualizations or alerts? If so, then <> can help.
+
+Think of a space as another instance of {kib}. A space allows you to organize your <>, <>, <>, and much more into their own categories. For example, you might have a Marketing space for your marketeers to track the results of their campaigns, and an Engineering space for your developers to {apm-get-started-ref}/overview.html[monitor application performance].
+
+The assets you create in one space are isolated from other spaces, so when you enter a space, you only see the assets that belong to that space.
+
+Refer to the <> for more information.
+
+[float]
+=== Roles
+
+Once your spaces are setup, the next step to securing access is to provision your roles. Roles are a collection of privileges that allow you to perform actions in {kib} and Elasticsearch. Roles are assigned to users, and to {ref}/built-in-users.html[system accounts] that power the Elastic Stack.
+
+You can create your own roles, or use any of the {ref}/built-in-roles.html[built-in roles]. Some built-in roles are intended for Elastic Stack components and should not be assigned to end users directly.
+
+One of the more useful built-in roles is `kibana_admin`. Assigning this role to your users will grant access to all of {kib}'s features. This includes the ability to manage Spaces.
+
+The built-in roles are great for getting started with the Elastic Stack, and for system administrators who do not need more restrictive access. With so many features, it’s not possible to ship more granular roles to accommodate everyone’s needs. This is where custom roles come in.
+
+As an administrator, you have the ability to create your own roles to describe exactly the kind of access your users should have. For example, you might create a `marketing_user` role, which you then assign to all users in your marketing department. This role would grant access to all of the necessary data and features for this team to be successful, without granting them access they don’t require.
+
+
+[float]
+=== Users
+
+Once your roles are setup, the next step to securing access is to create your users, and assign them one or more roles. {kib}'s user management allows you to provision accounts for each of your users.
+
+TIP: Want Single Sign-on? {kib} supports a wide range of SSO implementations, including SAML, OIDC, LDAP/AD, and Kerberos. <>.
+
+
+[float]
+[[tutorial-secure-kibana-dashboards-only]]
+=== Example: Create a user with access only to dashboards
+
+Let’s work through an example together. Consider a marketing analyst who wants to monitor the effectiveness of their campaigns. They should be able to see their team’s dashboards, but not be allowed to view or manage anything else in {kib}. All of the team’s dashboards are located in the Marketing space.
+
+[float]
+==== Create a space
+
+Create a Marketing space for your marketing analysts to use.
+
+. Open the main menu, and select **Stack Management**.
+. Under **{kib}**, select **Spaces**.
+. Click **Create a space**.
+. Give this space a unique name. For example: `Marketing`.
+. Click **Create space**.
++
+If you’ve followed the example above, you should end up with a space that looks like this:
++
+[role="screenshot"]
+image::user/security/images/tutorial-secure-access-example-1-space.png[Create space UI]
+
+
+[float]
+==== Create a role
+
+To effectively use dashboards, create a role that describes the privileges you want to grant.
+In this example, a marketing analyst will need:
+
+* Access to **read** the data that powers the dashboards
+* Access to **read** the dashboards within the `Marketing` space
+
+To create the role:
+
+. Open the main menu, and select **Stack Management**.
+. Under **Security**, select **Roles**.
+. Click **Create role**.
+. Give this role a unique name. For example: `marketing_dashboards_role`.
+. For this example, you want to store all marketing data in the `acme-marketing-*` set of indices. To grant this access, locate the **Index privileges** section and enter:
+.. `acme-marketing-*` in the **Indices** field.
+.. `read` and `view_index_metadata` in the **Privileges** field.
++
+TIP: You can add multiple patterns of indices, and grant different access levels to each. Click **Add index privilege** to grant additional access.
+. To grant access to dashboards in the `Marketing` space, locate the {kib} section, and click **Add {kib} privilege**:
+.. From the **Spaces** dropdown, select the `Marketing` space.
+.. Expand the **Analytics** section, and select the **Read** privilege for **Dashboard**.
+.. Click **Add Kibana privilege**.
+. Click **Create role**.
++
+If you’ve followed the example above, you should end up with a role that looks like this:
++
+[role="screenshot"]
+image::user/security/images/tutorial-secure-access-example-1-role.png[Create role UI]
+
+
+[float]
+==== Create a user
+
+Now that you created a role, create a user account.
+
+. Navigate to *Stack Management*, and under *Security*, select *Users*.
+. Click *Create user*.
+. Give this user a descriptive username, and choose a secure password.
+. Assign the *marketing_dashboards_role* that you previously created to this new user.
+. Click *Create user*.
+
+[role="screenshot"]
+image::user/security/images/tutorial-secure-access-example-1-user.png[Create user UI]
+
+[float]
+==== Verify
+
+Verify that the user and role are working correctly.
+
+. Logout of {kib} if you are already logged in.
+. In the login screen, enter the username and password for the account you created.
++
+You’re taken into the `Marketing` space, and the main navigation shows only the *Dashboard* application.
++
+[role="screenshot"]
+image::user/security/images/tutorial-secure-access-example-1-test.png[Verifying access to dashboards]
+
+
+[float]
+=== What's next?
+
+This guide is an introduction to {kib}'s security features. Check out these additional resources to learn more about authenticating and authorizing your users.
+
+* View the <> to learn more about single-sign on and other login features.
+
+* View the <> to learn more about authorizing access to {kib}'s features.
+
+Still have questions? Ask on our https://discuss.elastic.co/c/kibana[Kibana discuss forum] and a fellow community member or Elastic engineer will help out.
diff --git a/docs/user/setup.asciidoc b/docs/user/setup.asciidoc
index a38bf699c1db8..bea13c1ef49b2 100644
--- a/docs/user/setup.asciidoc
+++ b/docs/user/setup.asciidoc
@@ -54,6 +54,8 @@ include::{kib-repo-dir}/setup/start-stop.asciidoc[]
include::{kib-repo-dir}/setup/access.asciidoc[]
+include::security/tutorials/how-to-secure-access-to-kibana.asciidoc[]
+
include::{kib-repo-dir}/setup/connect-to-elasticsearch.asciidoc[]
include::{kib-repo-dir}/setup/upgrade.asciidoc[]
diff --git a/package.json b/package.json
index a1acf73ea26f0..9bddca4665467 100644
--- a/package.json
+++ b/package.json
@@ -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,7 +208,6 @@
"content-disposition": "0.5.3",
"copy-to-clipboard": "^3.0.8",
"core-js": "^3.6.5",
- "custom-event-polyfill": "^0.3.0",
"cytoscape": "^3.10.0",
"cytoscape-dagre": "^2.2.2",
"d3": "3.5.17",
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 0000000000000..1a71166fae843
--- /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 0000000000000..4d6f02d3f85a6
--- /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 0000000000000..2032127b1eb91
--- /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 d6c286c672d90..1220639fc7bef 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 0207145a17be7..bc596d53db54c 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 af5a0221662d5..b25d4451895f2 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 451edf678aabe..c582767fb5101 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 4212e0430ff5f..ab20ca42a283e 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 e90ccf7eb8d31..56afdf54463f7 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 0000000000000..6c67518e21073
--- /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 0000000000000..e22205540ef31
--- /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 0000000000000..7449bb7cd3860
--- /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 0000000000000..ce1ca02d0c4f6
--- /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 0000000000000..edf9bd657f995
--- /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 0000000000000..5ac89ebcac77f
--- /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 0000000000000..08ef303ad0b3a
--- /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 0000000000000..00492d69b8ac5
--- /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 0000000000000..49004a78ce0e0
--- /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 0000000000000..23621c5b213bc
--- /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 0000000000000..fd40489b0f4a5
--- /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 0000000000000..550be8d20d446
--- /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 0000000000000..c9015e19b82f8
--- /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 0000000000000..c27f67c71e88b
--- /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 0000000000000..8f1e72172c675
--- /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-ui-shared-deps/polyfills.js b/packages/kbn-ui-shared-deps/polyfills.js
index abbf911cfc8fc..a9ec32023f2bf 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 d8818fe809ccb..0000000000000
--- 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/plugins/data/common/kbn_field_types/types.ts b/src/plugins/data/common/kbn_field_types/types.ts
index c46e5c5266f55..e6f815e058ce3 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 297af560081b1..3ce528e6ed893 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 4e278d5872a3e..56e720d237c45 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 bb34d7ede453c..09dfbb28a4e53 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 2d3ff8f5fdba8..62dac9831211a 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/public.api.md b/src/plugins/data/public/public.api.md
index db7f814a83f79..05925f097de24 100644
--- a/src/plugins/data/public/public.api.md
+++ b/src/plugins/data/public/public.api.md
@@ -1786,6 +1786,8 @@ export enum KBN_FIELD_TYPES {
// (undocumented)
IP_RANGE = "ip_range",
// (undocumented)
+ MISSING = "missing",
+ // (undocumented)
MURMUR3 = "murmur3",
// (undocumented)
NESTED = "nested",
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 466cc8c3de0b7..4e12f11668734 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 900a4ab7d7eb7..0f660f87266fd 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 {
>
> {
+ 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 b9719542adc81..3f0907acabdfa 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/kibana_utils/common/errors/errors.ts b/src/plugins/kibana_utils/common/errors/errors.ts
index 7a9495cc8f413..7f3efc6d9571f 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/vis_default_editor/public/_default.scss b/src/plugins/vis_default_editor/public/_default.scss
index c412b9d915e55..56c6a0f0f63f6 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 94f767510c4bd..277804567c2b7 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 95843dc6ae3a8..f8db2d89888a2 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 efb166c8975bb..3bb52eb15758a 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();
}
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 cc0f3ce2afae5..9eda709e58c3e 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 64d61996495d7..965951bfbd88d 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 0e529507f97e3..c906ff5304c90 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 db9c996147c94..f6131679874db 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(
? 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/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 6a6db40892e10..407f460f25ad3 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
@@ -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/index.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx
index 77835afef863a..7d119b8c406da 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/service_overview/service_overview.test.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx
index b30faac7a65af..c6ed4e640693f 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/services/rest/apm_observability_overview_fetchers.test.ts b/x-pack/plugins/apm/public/services/rest/apm_observability_overview_fetchers.test.ts
index 29fabc51fd582..00447607cf787 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 0000000000000..ba9f740e06d0d
--- /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 b0cce3296fe21..0e82d70faf1e1 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 00910353ac278..9ab56c1a303ea 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 1f0aa401bcab0..989297544c78f 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 45e17c1678518..9d7434d127ead 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 c0707d0286180..c0ff0cab88f47 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 fff661250c6df..40836cb6635e3 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 19163da449b90..a5340c1220b44 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 b91fb8342a212..e627e9ed1d6cf 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 41abe82de8ff2..faec64c798c7d 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 5d5e6eebb4c9f..8bbc22fbf289d 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
@@ -9,7 +9,7 @@ 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 {
@@ -20,12 +20,12 @@ export interface IndexPatternTitleAndFields {
// 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 indexPatternTitle = config['apm_oss.indexPattern'];
const indexPatternsFetcher = new IndexPatternsFetcher(
context.core.elasticsearch.client.asCurrentUser
@@ -50,7 +50,7 @@ export const getDynamicIndexPattern = ({
} catch (e) {
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 a1587611b0a2a..d8dbc242986a6 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 db96794627519..074df7eaafd3c 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 3bebcd49ec34a..0175860e93d35 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 c7c69e0774822..4728aa2e8d3f6 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 9958b8dec0124..0000000000000
--- 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 87bc97d346984..0000000000000
--- 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 5b74aa4347f14..0000000000000
--- 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 0000000000000..86330a87a8c55
--- /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 0000000000000..b7cbe890c57db
--- /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 d74aac0992eb4..0000000000000
--- 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 4aa7d7e6d412f..e06fbdf7fb6d4 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 f69d3fc9631d1..d6bb1d4bcbaae 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 0000000000000..c151752b4b6e0
--- /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 3b800c23135ce..aa70cde4f96ae 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 c7e82e13d07b8..9fa2346eb72fb 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 1aac2c09d01c5..d459570cf7337 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 0000000000000..82b73d46da5c1
--- /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 0000000000000..3a88a496b923f
--- /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 3156acb469a72..d7f91adc0d683 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 33943d6e05d01..267479de4c102 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 e9060688c63a6..a2eb12662cbca 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 b4d25ca8b2a06..800a5bdcc5d5f 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 31e8d6cc1e9f0..111e0a18c8608 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 de7f35c4081bc..98467e1a4a0dd 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 91057c97579e4..003471aa89f39 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 a6ab553f09419..c9c5d236c14f9 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 6287ffbf0c751..dd392982b02fd 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 f3424a252e409..ebca374db86d7 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 3ba24b4ed5268..0fec88a4326c3 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 0000000000000..cef9eaf2f4fc0
--- /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/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts
index 846e20b48ddca..aa176fe3b188f 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts
@@ -481,21 +481,84 @@ describe(' serialization', () => {
});
});
- test('delete phase', async () => {
- const { actions } = testBed;
- await actions.delete.enable(true);
- await actions.setWaitForSnapshotPolicy('test');
- await actions.savePolicy();
- const latestRequest = server.requests[server.requests.length - 1];
- const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body);
- expect(entirePolicy.phases.delete).toEqual({
- min_age: '365d',
- actions: {
- delete: {},
- wait_for_snapshot: {
- policy: 'test',
+ describe('frozen phase', () => {
+ test('default value', async () => {
+ const { actions } = testBed;
+ await actions.frozen.enable(true);
+ await actions.frozen.setSearchableSnapshot('myRepo');
+
+ await actions.savePolicy();
+
+ const latestRequest = server.requests[server.requests.length - 1];
+ const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body);
+ expect(entirePolicy.phases.frozen).toEqual({
+ min_age: '0d',
+ actions: {
+ searchable_snapshot: { snapshot_repository: 'myRepo' },
},
- },
+ });
+ });
+
+ describe('deserialization', () => {
+ beforeEach(async () => {
+ const policyToEdit = getDefaultHotPhasePolicy('my_policy');
+ policyToEdit.policy.phases.frozen = {
+ min_age: '1234m',
+ actions: { searchable_snapshot: { snapshot_repository: 'myRepo' } },
+ };
+
+ httpRequestsMockHelpers.setLoadPolicies([policyToEdit]);
+ httpRequestsMockHelpers.setLoadSnapshotPolicies([]);
+ httpRequestsMockHelpers.setListNodes({
+ nodesByRoles: {},
+ nodesByAttributes: { test: ['123'] },
+ isUsingDeprecatedDataRoleConfig: false,
+ });
+
+ await act(async () => {
+ testBed = await setup();
+ });
+
+ const { component } = testBed;
+ component.update();
+ });
+
+ test('default value', async () => {
+ const { actions } = testBed;
+
+ await actions.savePolicy();
+
+ const latestRequest = server.requests[server.requests.length - 1];
+ const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body);
+ expect(entirePolicy.phases.frozen).toEqual({
+ min_age: '1234m',
+ actions: {
+ searchable_snapshot: {
+ snapshot_repository: 'myRepo',
+ },
+ },
+ });
+ });
+ });
+ });
+
+ describe('delete phase', () => {
+ test('default value', async () => {
+ const { actions } = testBed;
+ await actions.delete.enable(true);
+ await actions.setWaitForSnapshotPolicy('test');
+ await actions.savePolicy();
+ const latestRequest = server.requests[server.requests.length - 1];
+ const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body);
+ expect(entirePolicy.phases.delete).toEqual({
+ min_age: '365d',
+ actions: {
+ delete: {},
+ wait_for_snapshot: {
+ policy: 'test',
+ },
+ },
+ });
});
});
});
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts
index 73ecb0d73b7a7..af571d16ca8c5 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts
@@ -114,6 +114,14 @@ export const createDeserializer = (isCloudEnabled: boolean) => (
}
}
+ if (draft.phases.frozen) {
+ if (draft.phases.frozen.min_age) {
+ const minAge = splitSizeAndUnits(draft.phases.frozen.min_age);
+ draft.phases.frozen.min_age = minAge.size;
+ draft._meta.frozen.minAgeUnit = minAge.units;
+ }
+ }
+
if (draft.phases.delete) {
if (draft.phases.delete.min_age) {
const minAge = splitSizeAndUnits(draft.phases.delete.min_age);
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts
index 24dafa6cca237..0b1db784469a9 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts
@@ -267,6 +267,13 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => (
draft.phases.frozen!.actions = draft.phases.frozen?.actions ?? {};
const frozenPhase = draft.phases.frozen!;
+ /**
+ * FROZEN PHASE MIN AGE
+ */
+ if (updatedPolicy.phases.frozen?.min_age) {
+ frozenPhase.min_age = `${updatedPolicy.phases.frozen!.min_age}${_meta.frozen.minAgeUnit}`;
+ }
+
/**
* FROZEN PHASE SEARCHABLE SNAPSHOT
*/
diff --git a/x-pack/plugins/infra/server/kibana.index.ts b/x-pack/plugins/infra/server/kibana.index.ts
deleted file mode 100644
index 35d2f845ac4c1..0000000000000
--- a/x-pack/plugins/infra/server/kibana.index.ts
+++ /dev/null
@@ -1,45 +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 { Server } from '@hapi/hapi';
-import JoiNamespace from 'joi';
-
-export interface KbnServer extends Server {
- usage: any;
-}
-
-// NP_TODO: this is only used in the root index file AFAICT, can remove after migrating to NP
-export const getConfigSchema = (Joi: typeof JoiNamespace) => {
- const InfraDefaultSourceConfigSchema = Joi.object({
- metricAlias: Joi.string(),
- logAlias: Joi.string(),
- fields: Joi.object({
- container: Joi.string(),
- host: Joi.string(),
- message: Joi.array().items(Joi.string()).single(),
- pod: Joi.string(),
- tiebreaker: Joi.string(),
- timestamp: Joi.string(),
- }),
- });
-
- // NP_TODO: make sure this is all represented in the NP config schema
- const InfraRootConfigSchema = Joi.object({
- enabled: Joi.boolean().default(true),
- query: Joi.object({
- partitionSize: Joi.number(),
- partitionFactor: Joi.number(),
- }).default(),
- sources: Joi.object()
- .keys({
- default: InfraDefaultSourceConfigSchema,
- })
- .default(),
- }).default();
-
- return InfraRootConfigSchema;
-};
diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts
index 5244b8a81e75f..8cee4ea588722 100644
--- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts
@@ -33,15 +33,25 @@ type ConditionResult = InventoryMetricConditions & {
isError: boolean;
};
-export const evaluateCondition = async (
- condition: InventoryMetricConditions,
- nodeType: InventoryItemType,
- source: InfraSource,
- logQueryFields: LogQueryFields,
- esClient: ElasticsearchClient,
- filterQuery?: string,
- lookbackSize?: number
-): Promise> => {
+export const evaluateCondition = async ({
+ condition,
+ nodeType,
+ source,
+ logQueryFields,
+ esClient,
+ compositeSize,
+ filterQuery,
+ lookbackSize,
+}: {
+ condition: InventoryMetricConditions;
+ nodeType: InventoryItemType;
+ source: InfraSource;
+ logQueryFields: LogQueryFields;
+ esClient: ElasticsearchClient;
+ compositeSize: number;
+ filterQuery?: string;
+ lookbackSize?: number;
+}): Promise> => {
const { comparator, warningComparator, metric, customMetric } = condition;
let { threshold, warningThreshold } = condition;
@@ -61,6 +71,7 @@ export const evaluateCondition = async (
timerange,
source,
logQueryFields,
+ compositeSize,
filterQuery,
customMetric
);
@@ -105,6 +116,7 @@ const getData = async (
timerange: InfraTimerangeInput,
source: InfraSource,
logQueryFields: LogQueryFields,
+ compositeSize: number,
filterQuery?: string,
customMetric?: SnapshotCustomMetricInput
) => {
@@ -128,7 +140,13 @@ const getData = async (
includeTimeseries: Boolean(timerange.lookbackSize),
};
try {
- const { nodes } = await getNodes(client, snapshotRequest, source, logQueryFields);
+ const { nodes } = await getNodes(
+ client,
+ snapshotRequest,
+ source,
+ logQueryFields,
+ compositeSize
+ );
if (!nodes.length) return { [UNGROUPED_FACTORY_KEY]: null }; // No Data state
diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts
index d775a503d1d32..8fb8ee54d22ab 100644
--- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts
@@ -73,16 +73,19 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
services.savedObjectsClient
);
+ const compositeSize = libs.configuration.inventory.compositeSize;
+
const results = await Promise.all(
- criteria.map((c) =>
- evaluateCondition(
- c,
+ criteria.map((condition) =>
+ evaluateCondition({
+ condition,
nodeType,
source,
logQueryFields,
- services.scopedClusterClient.asCurrentUser,
- filterQuery
- )
+ esClient: services.scopedClusterClient.asCurrentUser,
+ compositeSize,
+ filterQuery,
+ })
)
);
diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts
index f254f1e68ae46..00d01b15750d1 100644
--- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts
@@ -32,6 +32,7 @@ interface PreviewInventoryMetricThresholdAlertParams {
params: InventoryMetricThresholdParams;
source: InfraSource;
logQueryFields: LogQueryFields;
+ compositeSize: number;
lookback: Unit;
alertInterval: string;
alertThrottle: string;
@@ -46,6 +47,7 @@ export const previewInventoryMetricThresholdAlert: (
params,
source,
logQueryFields,
+ compositeSize,
lookback,
alertInterval,
alertThrottle,
@@ -70,8 +72,17 @@ export const previewInventoryMetricThresholdAlert: (
try {
const results = await Promise.all(
- criteria.map((c) =>
- evaluateCondition(c, nodeType, source, logQueryFields, esClient, filterQuery, lookbackSize)
+ criteria.map((condition) =>
+ evaluateCondition({
+ condition,
+ nodeType,
+ source,
+ logQueryFields,
+ esClient,
+ compositeSize,
+ filterQuery,
+ lookbackSize,
+ })
)
);
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts
index 9086d6436c2a2..44b2695ba4e3b 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts
@@ -424,9 +424,8 @@ describe('The metric threshold alert type', () => {
const createMockStaticConfiguration = (sources: any) => ({
enabled: true,
- query: {
- partitionSize: 1,
- partitionFactor: 1,
+ inventory: {
+ compositeSize: 2000,
},
sources,
});
diff --git a/x-pack/plugins/infra/server/lib/infra_types.ts b/x-pack/plugins/infra/server/lib/infra_types.ts
index 08e42279e4939..f338d7957a343 100644
--- a/x-pack/plugins/infra/server/lib/infra_types.ts
+++ b/x-pack/plugins/infra/server/lib/infra_types.ts
@@ -5,7 +5,6 @@
* 2.0.
*/
-import { InfraSourceConfiguration } from '../../common/source_configuration/source_configuration';
import { InfraFieldsDomain } from './domains/fields_domain';
import { InfraLogEntriesDomain } from './domains/log_entries_domain';
import { InfraMetricsDomain } from './domains/metrics_domain';
@@ -28,14 +27,3 @@ export interface InfraBackendLibs extends InfraDomainLibs {
sourceStatus: InfraSourceStatus;
getLogQueryFields: GetLogQueryFields;
}
-
-export interface InfraConfiguration {
- enabled: boolean;
- query: {
- partitionSize: number;
- partitionFactor: number;
- };
- sources: {
- default: InfraSourceConfiguration;
- };
-}
diff --git a/x-pack/plugins/infra/server/lib/sources/sources.test.ts b/x-pack/plugins/infra/server/lib/sources/sources.test.ts
index aaeb44bb03aa7..0786722e5a479 100644
--- a/x-pack/plugins/infra/server/lib/sources/sources.test.ts
+++ b/x-pack/plugins/infra/server/lib/sources/sources.test.ts
@@ -134,9 +134,8 @@ describe('the InfraSources lib', () => {
const createMockStaticConfiguration = (sources: any) => ({
enabled: true,
- query: {
- partitionSize: 1,
- partitionFactor: 1,
+ inventory: {
+ compositeSize: 2000,
},
sources,
});
diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts
index 50fec38b9f2df..f818776fdf82c 100644
--- a/x-pack/plugins/infra/server/plugin.ts
+++ b/x-pack/plugins/infra/server/plugin.ts
@@ -35,9 +35,8 @@ import { createGetLogQueryFields } from './services/log_queries/get_log_query_fi
export const config = {
schema: schema.object({
enabled: schema.boolean({ defaultValue: true }),
- query: schema.object({
- partitionSize: schema.number({ defaultValue: 75 }),
- partitionFactor: schema.number({ defaultValue: 1.2 }),
+ inventory: schema.object({
+ compositeSize: schema.number({ defaultValue: 2000 }),
}),
sources: schema.maybe(
schema.object({
diff --git a/x-pack/plugins/infra/server/routes/alerting/preview.ts b/x-pack/plugins/infra/server/routes/alerting/preview.ts
index 4d980834d3a70..3008504f3b06c 100644
--- a/x-pack/plugins/infra/server/routes/alerting/preview.ts
+++ b/x-pack/plugins/infra/server/routes/alerting/preview.ts
@@ -29,6 +29,7 @@ export const initAlertPreviewRoute = ({
framework,
sources,
getLogQueryFields,
+ configuration,
}: InfraBackendLibs) => {
framework.registerRoute(
{
@@ -56,6 +57,8 @@ export const initAlertPreviewRoute = ({
sourceId || 'default'
);
+ const compositeSize = configuration.inventory.compositeSize;
+
try {
switch (alertType) {
case METRIC_THRESHOLD_ALERT_TYPE_ID: {
@@ -96,6 +99,7 @@ export const initAlertPreviewRoute = ({
lookback,
source,
logQueryFields,
+ compositeSize,
alertInterval,
alertThrottle,
alertNotifyWhen,
diff --git a/x-pack/plugins/infra/server/routes/snapshot/index.ts b/x-pack/plugins/infra/server/routes/snapshot/index.ts
index cbadd26ccd4bf..c5394a02c1d04 100644
--- a/x-pack/plugins/infra/server/routes/snapshot/index.ts
+++ b/x-pack/plugins/infra/server/routes/snapshot/index.ts
@@ -40,7 +40,7 @@ export const initSnapshotRoute = (libs: InfraBackendLibs) => {
requestContext.core.savedObjects.client,
snapshotRequest.sourceId
);
-
+ const compositeSize = libs.configuration.inventory.compositeSize;
const logQueryFields = await libs.getLogQueryFields(
snapshotRequest.sourceId,
requestContext.core.savedObjects.client
@@ -49,7 +49,13 @@ export const initSnapshotRoute = (libs: InfraBackendLibs) => {
UsageCollector.countNode(snapshotRequest.nodeType);
const client = createSearchClient(requestContext, framework);
- const snapshotResponse = await getNodes(client, snapshotRequest, source, logQueryFields);
+ const snapshotResponse = await getNodes(
+ client,
+ snapshotRequest,
+ source,
+ logQueryFields,
+ compositeSize
+ );
return response.ok({
body: SnapshotNodeResponseRT.encode(snapshotResponse),
diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts
index ff3cf048b99de..21420095a3ae5 100644
--- a/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts
+++ b/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts
@@ -19,18 +19,26 @@ export interface SourceOverrides {
timestamp: string;
}
-const transformAndQueryData = async (
- client: ESSearchClient,
- snapshotRequest: SnapshotRequest,
- source: InfraSource,
- sourceOverrides?: SourceOverrides
-) => {
- const metricsApiRequest = await transformRequestToMetricsAPIRequest(
+const transformAndQueryData = async ({
+ client,
+ snapshotRequest,
+ source,
+ compositeSize,
+ sourceOverrides,
+}: {
+ client: ESSearchClient;
+ snapshotRequest: SnapshotRequest;
+ source: InfraSource;
+ compositeSize: number;
+ sourceOverrides?: SourceOverrides;
+}) => {
+ const metricsApiRequest = await transformRequestToMetricsAPIRequest({
client,
source,
snapshotRequest,
- sourceOverrides
- );
+ compositeSize,
+ sourceOverrides,
+ });
const metricsApiResponse = await queryAllData(client, metricsApiRequest);
const snapshotResponse = transformMetricsApiResponseToSnapshotResponse(
metricsApiRequest,
@@ -45,30 +53,39 @@ export const getNodes = async (
client: ESSearchClient,
snapshotRequest: SnapshotRequest,
source: InfraSource,
- logQueryFields: LogQueryFields
+ logQueryFields: LogQueryFields,
+ compositeSize: number
) => {
let nodes;
if (snapshotRequest.metrics.find((metric) => metric.type === 'logRate')) {
// *Only* the log rate metric has been requested
if (snapshotRequest.metrics.length === 1) {
- nodes = await transformAndQueryData(client, snapshotRequest, source, logQueryFields);
+ nodes = await transformAndQueryData({
+ client,
+ snapshotRequest,
+ source,
+ compositeSize,
+ sourceOverrides: logQueryFields,
+ });
} else {
// A scenario whereby a single host might be shipping metrics and logs.
const metricsWithoutLogsMetrics = snapshotRequest.metrics.filter(
(metric) => metric.type !== 'logRate'
);
- const nodesWithoutLogsMetrics = await transformAndQueryData(
+ const nodesWithoutLogsMetrics = await transformAndQueryData({
client,
- { ...snapshotRequest, metrics: metricsWithoutLogsMetrics },
- source
- );
- const logRateNodes = await transformAndQueryData(
+ snapshotRequest: { ...snapshotRequest, metrics: metricsWithoutLogsMetrics },
+ source,
+ compositeSize,
+ });
+ const logRateNodes = await transformAndQueryData({
client,
- { ...snapshotRequest, metrics: [{ type: 'logRate' }] },
+ snapshotRequest: { ...snapshotRequest, metrics: [{ type: 'logRate' }] },
source,
- logQueryFields
- );
+ compositeSize,
+ sourceOverrides: logQueryFields,
+ });
// Merge nodes where possible - e.g. a single host is shipping metrics and logs
const mergedNodes = nodesWithoutLogsMetrics.nodes.map((node) => {
const logRateNode = logRateNodes.nodes.find(
@@ -91,7 +108,7 @@ export const getNodes = async (
};
}
} else {
- nodes = await transformAndQueryData(client, snapshotRequest, source);
+ nodes = await transformAndQueryData({ client, snapshotRequest, source, compositeSize });
}
return nodes;
diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.test.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.test.ts
new file mode 100644
index 0000000000000..1e1c202b7e602
--- /dev/null
+++ b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.test.ts
@@ -0,0 +1,115 @@
+/*
+ * 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 { transformRequestToMetricsAPIRequest } from './transform_request_to_metrics_api_request';
+import { ESSearchClient } from '../../../lib/metrics/types';
+import { InfraSource } from '../../../lib/sources';
+import { SnapshotRequest } from '../../../../common/http_api';
+
+jest.mock('./create_timerange_with_interval', () => {
+ return {
+ createTimeRangeWithInterval: () => ({
+ interval: '60s',
+ from: 1605705900000,
+ to: 1605706200000,
+ }),
+ };
+});
+
+describe('transformRequestToMetricsAPIRequest', () => {
+ test('returns a MetricsApiRequest given parameters', async () => {
+ const compositeSize = 3000;
+ const result = await transformRequestToMetricsAPIRequest({
+ client: {} as ESSearchClient,
+ source,
+ snapshotRequest,
+ compositeSize,
+ });
+ expect(result).toEqual(metricsApiRequest);
+ });
+});
+
+const source: InfraSource = {
+ id: 'default',
+ version: 'WzkzNjk5LDVd',
+ updatedAt: 1617384456384,
+ origin: 'stored',
+ configuration: {
+ name: 'Default',
+ description: '',
+ metricAlias: 'metrics-*,metricbeat-*',
+ logAlias: 'logs-*,filebeat-*,kibana_sample_data_logs*',
+ fields: {
+ container: 'container.id',
+ host: 'host.name',
+ message: ['message', '@message'],
+ pod: 'kubernetes.pod.uid',
+ tiebreaker: '_doc',
+ timestamp: '@timestamp',
+ },
+ inventoryDefaultView: '0',
+ metricsExplorerDefaultView: '0',
+ logColumns: [
+ { timestampColumn: { id: '5e7f964a-be8a-40d8-88d2-fbcfbdca0e2f' } },
+ { fieldColumn: { id: ' eb9777a8-fcd3-420e-ba7d-172fff6da7a2', field: 'event.dataset' } },
+ { messageColumn: { id: 'b645d6da-824b-4723-9a2a-e8cece1645c0' } },
+ { fieldColumn: { id: '906175e0-a293-42b2-929f-87a203e6fbec', field: 'agent.name' } },
+ ],
+ anomalyThreshold: 50,
+ },
+};
+
+const snapshotRequest: SnapshotRequest = {
+ metrics: [{ type: 'cpu' }],
+ groupBy: [],
+ nodeType: 'pod',
+ timerange: { interval: '1m', to: 1605706200000, from: 1605705000000, lookbackSize: 5 },
+ filterQuery: '',
+ sourceId: 'default',
+ accountId: '',
+ region: '',
+ includeTimeseries: true,
+};
+
+const metricsApiRequest = {
+ indexPattern: 'metrics-*,metricbeat-*',
+ timerange: { field: '@timestamp', from: 1605705900000, to: 1605706200000, interval: '60s' },
+ metrics: [
+ {
+ id: 'cpu',
+ aggregations: {
+ cpu_with_limit: { avg: { field: 'kubernetes.pod.cpu.usage.limit.pct' } },
+ cpu_without_limit: { avg: { field: 'kubernetes.pod.cpu.usage.node.pct' } },
+ cpu: {
+ bucket_script: {
+ buckets_path: { with_limit: 'cpu_with_limit', without_limit: 'cpu_without_limit' },
+ script: {
+ source: 'params.with_limit > 0.0 ? params.with_limit : params.without_limit',
+ lang: 'painless',
+ },
+ gap_policy: 'skip',
+ },
+ },
+ },
+ },
+ {
+ id: '__metadata__',
+ aggregations: {
+ __metadata__: {
+ top_metrics: {
+ metrics: [{ field: 'kubernetes.pod.name' }, { field: 'kubernetes.pod.ip' }],
+ size: 1,
+ sort: { '@timestamp': 'desc' },
+ },
+ },
+ },
+ },
+ ],
+ limit: 3000,
+ alignDataToEnd: true,
+ groupBy: ['kubernetes.pod.uid'],
+};
diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts
index a71e1fb1f1f14..811b0da952456 100644
--- a/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts
+++ b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts
@@ -15,12 +15,19 @@ import { transformSnapshotMetricsToMetricsAPIMetrics } from './transform_snapsho
import { META_KEY } from './constants';
import { SourceOverrides } from './get_nodes';
-export const transformRequestToMetricsAPIRequest = async (
- client: ESSearchClient,
- source: InfraSource,
- snapshotRequest: SnapshotRequest,
- sourceOverrides?: SourceOverrides
-): Promise => {
+export const transformRequestToMetricsAPIRequest = async ({
+ client,
+ source,
+ snapshotRequest,
+ compositeSize,
+ sourceOverrides,
+}: {
+ client: ESSearchClient;
+ source: InfraSource;
+ snapshotRequest: SnapshotRequest;
+ compositeSize: number;
+ sourceOverrides?: SourceOverrides;
+}): Promise => {
const timeRangeWithIntervalApplied = await createTimeRangeWithInterval(client, {
...snapshotRequest,
filterQuery: parseFilterQuery(snapshotRequest.filterQuery),
@@ -36,7 +43,9 @@ export const transformRequestToMetricsAPIRequest = async (
interval: timeRangeWithIntervalApplied.interval,
},
metrics: transformSnapshotMetricsToMetricsAPIMetrics(snapshotRequest),
- limit: snapshotRequest.overrideCompositeSize ? snapshotRequest.overrideCompositeSize : 5,
+ limit: snapshotRequest.overrideCompositeSize
+ ? snapshotRequest.overrideCompositeSize
+ : compositeSize,
alignDataToEnd: true,
};
diff --git a/x-pack/plugins/ml/common/index.ts b/x-pack/plugins/ml/common/index.ts
index c15aa8f414fb1..a64a0c0ae09fe 100644
--- a/x-pack/plugins/ml/common/index.ts
+++ b/x-pack/plugins/ml/common/index.ts
@@ -10,6 +10,7 @@ export { ChartData } from './types/field_histograms';
export { ANOMALY_SEVERITY, ANOMALY_THRESHOLD, SEVERITY_COLORS } from './constants/anomalies';
export { getSeverityColor, getSeverityType } from './util/anomaly_utils';
export { isPopulatedObject } from './util/object_utils';
-export { isRuntimeMappings } from './util/runtime_field_utils';
export { composeValidators, patternValidator } from './util/validators';
+export { isRuntimeMappings, isRuntimeField } from './util/runtime_field_utils';
export { extractErrorMessage } from './util/errors';
+export type { RuntimeMappings } from './types/fields';
diff --git a/x-pack/plugins/ml/common/types/fields.ts b/x-pack/plugins/ml/common/types/fields.ts
index 8dfe9d111ed38..45fcfac7e930c 100644
--- a/x-pack/plugins/ml/common/types/fields.ts
+++ b/x-pack/plugins/ml/common/types/fields.ts
@@ -28,7 +28,7 @@ export interface Field {
aggregatable?: boolean;
aggIds?: AggId[];
aggs?: Aggregation[];
- runtimeField?: RuntimeField;
+ runtimeField?: estypes.RuntimeField;
}
export interface Aggregation {
@@ -108,17 +108,4 @@ export interface AggCardinality {
export type RollupFields = Record]>;
-// Replace this with import once #88995 is merged
-export const RUNTIME_FIELD_TYPES = ['keyword', 'long', 'double', 'date', 'ip', 'boolean'] as const;
-export type RuntimeType = typeof RUNTIME_FIELD_TYPES[number];
-
-export interface RuntimeField {
- type: RuntimeType;
- script?:
- | string
- | {
- source: string;
- };
-}
-
export type RuntimeMappings = estypes.RuntimeFields;
diff --git a/x-pack/plugins/ml/common/util/runtime_field_utils.ts b/x-pack/plugins/ml/common/util/runtime_field_utils.ts
index 6d911ecd5d3cb..7be2a3ec8c9e1 100644
--- a/x-pack/plugins/ml/common/util/runtime_field_utils.ts
+++ b/x-pack/plugins/ml/common/util/runtime_field_utils.ts
@@ -4,14 +4,14 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-
+import { estypes } from '@elastic/elasticsearch';
import { isPopulatedObject } from './object_utils';
import { RUNTIME_FIELD_TYPES } from '../../../../../src/plugins/data/common';
-import type { RuntimeField, RuntimeMappings } from '../types/fields';
+import type { RuntimeMappings } from '../types/fields';
type RuntimeType = typeof RUNTIME_FIELD_TYPES[number];
-export function isRuntimeField(arg: unknown): arg is RuntimeField {
+export function isRuntimeField(arg: unknown): arg is estypes.RuntimeField {
return (
((isPopulatedObject(arg, ['type']) && Object.keys(arg).length === 1) ||
(isPopulatedObject(arg, ['type', 'script']) &&
diff --git a/x-pack/plugins/ml/public/application/components/data_grid/common.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.ts
index d3e58c4d7bb0d..f723c1d72b818 100644
--- a/x-pack/plugins/ml/public/application/components/data_grid/common.ts
+++ b/x-pack/plugins/ml/public/application/components/data_grid/common.ts
@@ -18,6 +18,7 @@ import { i18n } from '@kbn/i18n';
import { CoreSetup } from 'src/core/public';
+import type { estypes } from '@elastic/elasticsearch';
import {
IndexPattern,
IFieldType,
@@ -49,7 +50,7 @@ import { getNestedProperty } from '../../util/object_utils';
import { mlFieldFormatService } from '../../services/field_format_service';
import { DataGridItem, IndexPagination, RenderCellValue } from './types';
-import { RuntimeMappings, RuntimeField } from '../../../../common/types/fields';
+import { RuntimeMappings } from '../../../../common/types/fields';
import { isRuntimeMappings } from '../../../../common/util/runtime_field_utils';
export const INIT_MAX_COLUMNS = 10;
@@ -179,7 +180,7 @@ export const getDataGridSchemasFromFieldTypes = (fieldTypes: FieldTypes, results
export const NON_AGGREGATABLE = 'non-aggregatable';
export const getDataGridSchemaFromESFieldType = (
- fieldType: ES_FIELD_TYPES | undefined | RuntimeField['type']
+ fieldType: ES_FIELD_TYPES | undefined | estypes.RuntimeField['type']
): string | undefined => {
// Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json']
// To fall back to the default string schema it needs to be undefined.
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/form_options_validation.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/form_options_validation.ts
index 1e1f376049579..79986e8ddb098 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/form_options_validation.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/form_options_validation.ts
@@ -6,8 +6,8 @@
*/
import { i18n } from '@kbn/i18n';
+import { estypes } from '@elastic/elasticsearch';
import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public';
-import { RuntimeType } from '../../../../../../../../../../src/plugins/data/common';
import { EVENT_RATE_FIELD_ID } from '../../../../../../../common/types/fields';
import { ANALYSIS_CONFIG_TYPE } from '../../../../common/analytics';
import { AnalyticsJobType } from '../../../analytics_management/hooks/use_create_analytics_form/state';
@@ -18,7 +18,7 @@ export const CATEGORICAL_TYPES = new Set(['ip', 'keyword']);
// Regression supports numeric fields. Classification supports categorical, numeric, and boolean.
export const shouldAddAsDepVarOption = (
fieldId: string,
- fieldType: ES_FIELD_TYPES | RuntimeType,
+ fieldType: ES_FIELD_TYPES | estypes.RuntimeField['type'],
jobType: AnalyticsJobType
) => {
if (fieldId === EVENT_RATE_FIELD_ID) return false;
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings.tsx
index d21bf67a1f51c..5b8fc82ef587b 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/runtime_mappings/runtime_mappings.tsx
@@ -131,7 +131,7 @@ export const RuntimeMappings: FC = ({ actions, state }) => {
defaultMessage: 'Runtime mappings',
})}
>
-
+
{isPopulatedObject(runtimeMappings) ? (
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts
index f48f4a62f5a7d..2d9ae1cd4689b 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts
@@ -13,7 +13,7 @@ import { CoreSetup } from 'src/core/public';
import { IndexPattern } from '../../../../../../../../../src/plugins/data/public';
import { isRuntimeMappings } from '../../../../../../common/util/runtime_field_utils';
-import { RuntimeMappings, RuntimeField } from '../../../../../../common/types/fields';
+import { RuntimeMappings } from '../../../../../../common/types/fields';
import { DEFAULT_SAMPLER_SHARD_SIZE } from '../../../../../../common/constants/field_histograms';
import { DataLoader } from '../../../../datavisualizer/index_based/data_loader';
@@ -44,7 +44,7 @@ interface MLEuiDataGridColumn extends EuiDataGridColumn {
function getRuntimeFieldColumns(runtimeMappings: RuntimeMappings) {
return Object.keys(runtimeMappings).map((id) => {
const field = runtimeMappings[id];
- const schema = getDataGridSchemaFromESFieldType(field.type as RuntimeField['type']);
+ const schema = getDataGridSchemaFromESFieldType(field.type as estypes.RuntimeField['type']);
return { id, schema, isExpandable: schema !== 'boolean', isRuntimeFieldColumn: true };
});
}
@@ -64,7 +64,7 @@ export const useIndexData = (
const field = indexPattern.fields.getByName(id);
const isRuntimeFieldColumn = field?.runtimeField !== undefined;
const schema = isRuntimeFieldColumn
- ? getDataGridSchemaFromESFieldType(field?.type as RuntimeField['type'])
+ ? getDataGridSchemaFromESFieldType(field?.type as estypes.RuntimeField['type'])
: getDataGridSchemaFromKibanaFieldType(field);
return {
id,
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.test.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.test.tsx
new file mode 100644
index 0000000000000..858ab58b53f4b
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.test.tsx
@@ -0,0 +1,216 @@
+/*
+ * 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 from 'react';
+import { render, fireEvent, waitFor, screen } from '@testing-library/react';
+
+import { IntlProvider } from 'react-intl';
+
+import {
+ getIndexPatternAndSavedSearch,
+ IndexPatternAndSavedSearch,
+} from '../../../../../util/index_utils';
+
+import { SourceSelection } from './source_selection';
+
+jest.mock('../../../../../../../../../../src/plugins/saved_objects/public', () => {
+ const SavedObjectFinderUi = ({
+ onChoose,
+ }: {
+ onChoose: (id: string, type: string, fullName: string, savedObject: object) => void;
+ }) => {
+ return (
+ <>
+
+
+
+
+ >
+ );
+ };
+
+ return {
+ SavedObjectFinderUi,
+ };
+});
+
+const mockNavigateToPath = jest.fn();
+jest.mock('../../../../../contexts/kibana', () => ({
+ useMlKibana: () => ({
+ services: {
+ savedObjects: {},
+ uiSettings: {},
+ },
+ }),
+ useNavigateToPath: () => mockNavigateToPath,
+}));
+
+jest.mock('../../../../../util/index_utils', () => {
+ return {
+ getIndexPatternAndSavedSearch: jest.fn(
+ async (id: string): Promise => {
+ return {
+ indexPattern: {
+ fields: [],
+ title:
+ id === 'the-remote-saved-search-id'
+ ? 'my_remote_cluster:index-pattern-title'
+ : 'index-pattern-title',
+ },
+ savedSearch: null,
+ };
+ }
+ ),
+ };
+});
+
+const mockOnClose = jest.fn();
+const mockGetIndexPatternAndSavedSearch = getIndexPatternAndSavedSearch as jest.Mock;
+
+describe('Data Frame Analytics: ', () => {
+ afterEach(() => {
+ mockNavigateToPath.mockClear();
+ mockGetIndexPatternAndSavedSearch.mockClear();
+ });
+
+ it('renders the title text', async () => {
+ // prepare
+ render(
+
+
+
+ );
+
+ // assert
+ expect(screen.queryByText('New analytics job')).toBeInTheDocument();
+ expect(mockNavigateToPath).toHaveBeenCalledTimes(0);
+ expect(mockGetIndexPatternAndSavedSearch).toHaveBeenCalledTimes(0);
+ });
+
+ it('shows the error callout when clicking a remote index pattern', async () => {
+ // prepare
+ render(
+
+
+
+ );
+
+ // act
+ fireEvent.click(screen.getByText('RemoteIndexPattern', { selector: 'button' }));
+ await waitFor(() => screen.getByTestId('analyticsCreateSourceIndexModalCcsErrorCallOut'));
+
+ // assert
+ expect(
+ screen.queryByText('Index patterns using cross-cluster search are not supported.')
+ ).toBeInTheDocument();
+ expect(mockNavigateToPath).toHaveBeenCalledTimes(0);
+ expect(mockGetIndexPatternAndSavedSearch).toHaveBeenCalledTimes(0);
+ });
+
+ it('calls navigateToPath for a plain index pattern ', async () => {
+ // prepare
+ render(
+
+
+
+ );
+
+ // act
+ fireEvent.click(screen.getByText('PlainIndexPattern', { selector: 'button' }));
+
+ // assert
+ await waitFor(() => {
+ expect(
+ screen.queryByText('Index patterns using cross-cluster search are not supported.')
+ ).not.toBeInTheDocument();
+ expect(mockNavigateToPath).toHaveBeenCalledWith(
+ '/data_frame_analytics/new_job?index=the-plain-index-pattern-id'
+ );
+ expect(mockGetIndexPatternAndSavedSearch).toHaveBeenCalledTimes(0);
+ });
+ });
+
+ it('shows the error callout when clicking a saved search using a remote index pattern', async () => {
+ // prepare
+ render(
+
+
+
+ );
+
+ // act
+ fireEvent.click(screen.getByText('RemoteSavedSearch', { selector: 'button' }));
+ await waitFor(() => screen.getByTestId('analyticsCreateSourceIndexModalCcsErrorCallOut'));
+
+ // assert
+ expect(
+ screen.queryByText('Index patterns using cross-cluster search are not supported.')
+ ).toBeInTheDocument();
+ expect(
+ screen.queryByText(
+ `The saved search 'the-remote-saved-search-title' uses the index pattern 'my_remote_cluster:index-pattern-title'.`
+ )
+ ).toBeInTheDocument();
+ expect(mockNavigateToPath).toHaveBeenCalledTimes(0);
+ expect(mockGetIndexPatternAndSavedSearch).toHaveBeenCalledWith('the-remote-saved-search-id');
+ });
+
+ it('calls navigateToPath for a saved search using a plain index pattern ', async () => {
+ // prepare
+ render(
+
+
+
+ );
+
+ // act
+ fireEvent.click(screen.getByText('PlainSavedSearch', { selector: 'button' }));
+
+ // assert
+ await waitFor(() => {
+ expect(
+ screen.queryByText('Index patterns using cross-cluster search are not supported.')
+ ).not.toBeInTheDocument();
+ expect(mockNavigateToPath).toHaveBeenCalledWith(
+ '/data_frame_analytics/new_job?savedSearchId=the-plain-saved-search-id'
+ );
+ expect(mockGetIndexPatternAndSavedSearch).toHaveBeenCalledWith('the-plain-saved-search-id');
+ });
+ });
+});
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx
index 40f97690d7790..cbc5a226eb319 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx
@@ -5,15 +5,28 @@
* 2.0.
*/
-import React, { FC } from 'react';
+import React, { useState, FC } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiModal, EuiModalBody, EuiModalHeader, EuiModalHeaderTitle } from '@elastic/eui';
+import {
+ EuiCallOut,
+ EuiModal,
+ EuiModalBody,
+ EuiModalHeader,
+ EuiModalHeaderTitle,
+ EuiSpacer,
+} from '@elastic/eui';
+
+import type { SimpleSavedObject } from 'src/core/public';
import { SavedObjectFinderUi } from '../../../../../../../../../../src/plugins/saved_objects/public';
import { useMlKibana, useNavigateToPath } from '../../../../../contexts/kibana';
+import { getNestedProperty } from '../../../../../util/object_utils';
+
+import { getIndexPatternAndSavedSearch } from '../../../../../util/index_utils';
+
const fixedPageSize: number = 8;
interface Props {
@@ -26,7 +39,49 @@ export const SourceSelection: FC = ({ onClose }) => {
} = useMlKibana();
const navigateToPath = useNavigateToPath();
- const onSearchSelected = async (id: string, type: string) => {
+ const [isCcsCallOut, setIsCcsCallOut] = useState(false);
+ const [ccsCallOutBodyText, setCcsCallOutBodyText] = useState();
+
+ const onSearchSelected = async (
+ id: string,
+ type: string,
+ fullName: string,
+ savedObject: SimpleSavedObject
+ ) => {
+ // Kibana index patterns including `:` are cross-cluster search indices
+ // and are not supported by Data Frame Analytics yet. For saved searches
+ // and index patterns that use cross-cluster search we intercept
+ // the selection before redirecting and show an error callout instead.
+ let indexPatternTitle = '';
+
+ if (type === 'index-pattern') {
+ indexPatternTitle = getNestedProperty(savedObject, 'attributes.title');
+ } else if (type === 'search') {
+ const indexPatternAndSavedSearch = await getIndexPatternAndSavedSearch(id);
+ indexPatternTitle = indexPatternAndSavedSearch.indexPattern?.title ?? '';
+ }
+
+ if (indexPatternTitle.includes(':')) {
+ setIsCcsCallOut(true);
+ if (type === 'search') {
+ setCcsCallOutBodyText(
+ i18n.translate(
+ 'xpack.ml.dataFrame.analytics.create.searchSelection.CcsErrorCallOutBody',
+ {
+ defaultMessage: `The saved search '{savedSearchTitle}' uses the index pattern '{indexPatternTitle}'.`,
+ values: {
+ savedSearchTitle: getNestedProperty(savedObject, 'attributes.title'),
+ indexPatternTitle,
+ },
+ }
+ )
+ );
+ } else {
+ setCcsCallOutBodyText(undefined);
+ }
+ return;
+ }
+
await navigateToPath(
`/data_frame_analytics/new_job?${
type === 'index-pattern' ? 'index' : 'savedSearchId'
@@ -54,6 +109,23 @@ export const SourceSelection: FC = ({ onClose }) => {
+ {isCcsCallOut && (
+ <>
+
+ {typeof ccsCallOutBodyText === 'string' && {ccsCallOutBodyText}
}
+
+
+ >
+ )}
any>(func: T, context?: any) => {
const memoizedLoadAnnotationsTableData = memoize(
loadAnnotationsTableData
);
-const memoizedLoadDataForCharts = memoize(loadDataForCharts);
const memoizedLoadFilteredTopInfluencers = memoize(
loadFilteredTopInfluencers
);
@@ -96,7 +95,7 @@ export const isLoadExplorerDataConfig = (arg: any): arg is LoadExplorerDataConfi
const loadExplorerDataProvider = (
mlResultsService: MlResultsService,
anomalyTimelineService: AnomalyTimelineService,
- anomalyExplorerService: AnomalyExplorerChartsService,
+ anomalyExplorerChartsService: AnomalyExplorerChartsService,
timefilter: TimefilterContract
) => {
const memoizedLoadOverallData = memoize(
@@ -108,8 +107,8 @@ const loadExplorerDataProvider = (
anomalyTimelineService
);
const memoizedAnomalyDataChange = memoize(
- anomalyExplorerService.getAnomalyData,
- anomalyExplorerService
+ anomalyExplorerChartsService.getAnomalyData,
+ anomalyExplorerChartsService
);
return (config: LoadExplorerDataConfig): Observable> => {
@@ -160,9 +159,7 @@ const loadExplorerDataProvider = (
swimlaneBucketInterval.asSeconds(),
bounds
),
- anomalyChartRecords: memoizedLoadDataForCharts(
- lastRefresh,
- mlResultsService,
+ anomalyChartRecords: anomalyExplorerChartsService.loadDataForCharts$(
jobIds,
timerange.earliestMs,
timerange.latestMs,
@@ -214,42 +211,30 @@ const loadExplorerDataProvider = (
// show the view-by loading indicator
// and pass on the data we already fetched.
tap(explorerService.setViewBySwimlaneLoading),
- // Trigger a side-effect to update the charts.
- tap(({ anomalyChartRecords, topFieldValues }) => {
- if (selectedCells !== undefined && Array.isArray(anomalyChartRecords)) {
- memoizedAnomalyDataChange(
- lastRefresh,
- explorerService,
- combinedJobRecords,
- swimlaneContainerWidth,
- anomalyChartRecords,
- timerange.earliestMs,
- timerange.latestMs,
- timefilter,
- tableSeverity
- );
- } else {
- memoizedAnomalyDataChange(
- lastRefresh,
- explorerService,
- combinedJobRecords,
- swimlaneContainerWidth,
- [],
- timerange.earliestMs,
- timerange.latestMs,
- timefilter,
- tableSeverity
- );
- }
- }),
- // Load view-by swimlane data and filtered top influencers.
- // mergeMap is used to have access to the already fetched data and act on it in arg #1.
- // In arg #2 of mergeMap we combine the data and pass it on in the action format
- // which can be consumed by explorerReducer() later on.
+ tap(explorerService.setChartsDataLoading),
mergeMap(
- ({ anomalyChartRecords, influencers, overallState, topFieldValues }) =>
+ ({
+ anomalyChartRecords,
+ influencers,
+ overallState,
+ topFieldValues,
+ annotationsData,
+ tableData,
+ }) =>
forkJoin({
- influencers:
+ anomalyChartsData: memoizedAnomalyDataChange(
+ lastRefresh,
+ combinedJobRecords,
+ swimlaneContainerWidth,
+ selectedCells !== undefined && Array.isArray(anomalyChartRecords)
+ ? anomalyChartRecords
+ : [],
+ timerange.earliestMs,
+ timerange.latestMs,
+ timefilter,
+ tableSeverity
+ ),
+ filteredTopInfluencers:
(selectionInfluencers.length > 0 || influencersFilterQuery !== undefined) &&
anomalyChartRecords !== undefined &&
anomalyChartRecords.length > 0
@@ -280,24 +265,26 @@ const loadExplorerDataProvider = (
swimlaneContainerWidth,
influencersFilterQuery
),
- }),
- (
- { annotationsData, overallState, tableData },
- { influencers, viewBySwimlaneState }
- ): Partial => {
- return {
- annotations: annotationsData,
- influencers: influencers as any,
- loading: false,
- viewBySwimlaneDataLoading: false,
- overallSwimlaneData: overallState,
- viewBySwimlaneData: viewBySwimlaneState as any,
- tableData,
- swimlaneLimit: isViewBySwimLaneData(viewBySwimlaneState)
- ? viewBySwimlaneState.cardinality
- : undefined,
- };
- }
+ }).pipe(
+ tap(({ anomalyChartsData }) => {
+ explorerService.setCharts(anomalyChartsData as ExplorerChartsData);
+ }),
+ map(({ viewBySwimlaneState, filteredTopInfluencers }) => {
+ return {
+ annotations: annotationsData,
+ influencers: filteredTopInfluencers as any,
+ loading: false,
+ viewBySwimlaneDataLoading: false,
+ anomalyChartsDataLoading: false,
+ overallSwimlaneData: overallState,
+ viewBySwimlaneData: viewBySwimlaneState as any,
+ tableData,
+ swimlaneLimit: isViewBySwimLaneData(viewBySwimlaneState)
+ ? viewBySwimlaneState.cardinality
+ : undefined,
+ };
+ })
+ )
)
);
};
@@ -319,7 +306,7 @@ export const useExplorerData = (): [Partial | undefined, (d: any)
uiSettings,
mlResultsService
);
- const anomalyExplorerService = new AnomalyExplorerChartsService(
+ const anomalyExplorerChartsService = new AnomalyExplorerChartsService(
timefilter,
mlApiServices,
mlResultsService
@@ -327,7 +314,7 @@ export const useExplorerData = (): [Partial | undefined, (d: any)
return loadExplorerDataProvider(
mlResultsService,
anomalyTimelineService,
- anomalyExplorerService,
+ anomalyExplorerChartsService,
timefilter
);
}, []);
diff --git a/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx b/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx
deleted file mode 100644
index 8fe2c32b766b4..0000000000000
--- a/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx
+++ /dev/null
@@ -1,312 +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, { FC, useCallback, useMemo, useState, useEffect } from 'react';
-import { debounce } from 'lodash';
-import {
- EuiFormRow,
- EuiCheckboxGroup,
- EuiInMemoryTableProps,
- EuiModal,
- EuiModalHeader,
- EuiModalHeaderTitle,
- EuiSpacer,
- EuiButtonEmpty,
- EuiButton,
- EuiModalFooter,
-} from '@elastic/eui';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiModalBody } from '@elastic/eui';
-import { EuiInMemoryTable } from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
-import { useMlKibana } from '../contexts/kibana';
-import { DashboardSavedObject } from '../../../../../../src/plugins/dashboard/public';
-import { getDefaultSwimlanePanelTitle } from '../../embeddables/anomaly_swimlane/anomaly_swimlane_embeddable';
-import { useDashboardService } from '../services/dashboard_service';
-import { SWIMLANE_TYPE, SwimlaneType } from './explorer_constants';
-import { JobId } from '../../../common/types/anomaly_detection_jobs';
-import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE } from '../../embeddables';
-
-export interface DashboardItem {
- id: string;
- title: string;
- description: string | undefined;
- attributes: DashboardSavedObject;
-}
-
-export type EuiTableProps = EuiInMemoryTableProps;
-
-function getDefaultEmbeddablePanelConfig(jobIds: JobId[]) {
- return {
- type: ANOMALY_SWIMLANE_EMBEDDABLE_TYPE,
- title: getDefaultSwimlanePanelTitle(jobIds),
- };
-}
-
-interface AddToDashboardControlProps {
- jobIds: JobId[];
- viewBy: string;
- onClose: (callback?: () => Promise) => void;
-}
-
-/**
- * Component for attaching anomaly swim lane embeddable to dashboards.
- */
-export const AddToDashboardControl: FC = ({
- onClose,
- jobIds,
- viewBy,
-}) => {
- const {
- notifications: { toasts },
- services: {
- application: { navigateToUrl },
- },
- } = useMlKibana();
-
- useEffect(() => {
- fetchDashboards();
-
- return () => {
- fetchDashboards.cancel();
- };
- }, []);
-
- const dashboardService = useDashboardService();
-
- const [isLoading, setIsLoading] = useState(false);
- const [selectedSwimlanes, setSelectedSwimlanes] = useState<{ [key in SwimlaneType]: boolean }>({
- [SWIMLANE_TYPE.OVERALL]: true,
- [SWIMLANE_TYPE.VIEW_BY]: false,
- });
- const [dashboardItems, setDashboardItems] = useState([]);
- const [selectedItems, setSelectedItems] = useState([]);
-
- const fetchDashboards = useCallback(
- debounce(async (query?: string) => {
- try {
- const response = await dashboardService.fetchDashboards(query);
- const items: DashboardItem[] = response.savedObjects.map((savedObject) => {
- return {
- id: savedObject.id,
- title: savedObject.attributes.title,
- description: savedObject.attributes.description,
- attributes: savedObject.attributes,
- };
- });
- setDashboardItems(items);
- } catch (e) {
- toasts.danger({
- body: e,
- });
- }
- setIsLoading(false);
- }, 500),
- []
- );
-
- const search: EuiTableProps['search'] = useMemo(() => {
- return {
- onChange: ({ queryText }) => {
- setIsLoading(true);
- fetchDashboards(queryText);
- },
- box: {
- incremental: true,
- 'data-test-subj': 'mlDashboardsSearchBox',
- },
- };
- }, []);
-
- const addSwimlaneToDashboardCallback = useCallback(async () => {
- const swimlanes = Object.entries(selectedSwimlanes)
- .filter(([, isSelected]) => isSelected)
- .map(([swimlaneType]) => swimlaneType);
-
- for (const selectedDashboard of selectedItems) {
- const panelsData = swimlanes.map((swimlaneType) => {
- const config = getDefaultEmbeddablePanelConfig(jobIds);
- if (swimlaneType === SWIMLANE_TYPE.VIEW_BY) {
- return {
- ...config,
- embeddableConfig: {
- jobIds,
- swimlaneType,
- viewBy,
- },
- };
- }
- return {
- ...config,
- embeddableConfig: {
- jobIds,
- swimlaneType,
- },
- };
- });
-
- try {
- await dashboardService.attachPanels(
- selectedDashboard.id,
- selectedDashboard.attributes,
- panelsData
- );
- toasts.success({
- title: (
-
- ),
- toastLifeTimeMs: 3000,
- });
- } catch (e) {
- toasts.danger({
- body: e,
- });
- }
- }
- }, [selectedSwimlanes, selectedItems]);
-
- const columns: EuiTableProps['columns'] = [
- {
- field: 'title',
- name: i18n.translate('xpack.ml.explorer.dashboardsTable.titleColumnHeader', {
- defaultMessage: 'Title',
- }),
- sortable: true,
- truncateText: true,
- },
- {
- field: 'description',
- name: i18n.translate('xpack.ml.explorer.dashboardsTable.descriptionColumnHeader', {
- defaultMessage: 'Description',
- }),
- truncateText: true,
- },
- ];
-
- const swimlaneTypeOptions = [
- {
- id: SWIMLANE_TYPE.OVERALL,
- label: i18n.translate('xpack.ml.explorer.overallLabel', {
- defaultMessage: 'Overall',
- }),
- },
- {
- id: SWIMLANE_TYPE.VIEW_BY,
- label: i18n.translate('xpack.ml.explorer.viewByFieldLabel', {
- defaultMessage: 'View by {viewByField}',
- values: { viewByField: viewBy },
- }),
- },
- ];
-
- const selection: EuiTableProps['selection'] = {
- onSelectionChange: setSelectedItems,
- };
-
- const noSwimlaneSelected = Object.values(selectedSwimlanes).every((isSelected) => !isSelected);
-
- return (
-
-
-
-
-
-
-
-
- }
- >
- {
- const newSelection = {
- ...selectedSwimlanes,
- [optionId]: !selectedSwimlanes[optionId as SwimlaneType],
- };
- setSelectedSwimlanes(newSelection);
- }}
- data-test-subj="mlAddToDashboardSwimlaneTypeSelector"
- />
-
-
-
-
-
- }
- data-test-subj="mlDashboardSelectionContainer"
- >
-
-
-
-
-
-
-
- {
- onClose(async () => {
- const selectedDashboardId = selectedItems[0].id;
- await addSwimlaneToDashboardCallback();
- await navigateToUrl(await dashboardService.getDashboardEditUrl(selectedDashboardId));
- });
- }}
- data-test-subj="mlAddAndEditDashboardButton"
- >
-
-
-
-
-
-
-
- );
-};
diff --git a/x-pack/plugins/ml/public/application/explorer/anomaly_context_menu.tsx b/x-pack/plugins/ml/public/application/explorer/anomaly_context_menu.tsx
new file mode 100644
index 0000000000000..9f65449169ee6
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/explorer/anomaly_context_menu.tsx
@@ -0,0 +1,110 @@
+/*
+ * 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, { useMemo, useState, FC } from 'react';
+import {
+ EuiButtonIcon,
+ EuiContextMenuItem,
+ EuiContextMenuPanel,
+ EuiFlexItem,
+ EuiPopover,
+} from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
+import { useMlKibana } from '../contexts/kibana';
+import type { AppStateSelectedCells, ExplorerJob } from './explorer_utils';
+import { TimeRangeBounds } from '../util/time_buckets';
+import { AddAnomalyChartsToDashboardControl } from './dashboard_controls/add_anomaly_charts_to_dashboard_controls';
+
+interface AnomalyContextMenuProps {
+ selectedJobs: ExplorerJob[];
+ selectedCells?: AppStateSelectedCells;
+ bounds?: TimeRangeBounds;
+ interval?: number;
+ chartsCount: number;
+}
+export const AnomalyContextMenu: FC = ({
+ selectedJobs,
+ selectedCells,
+ bounds,
+ interval,
+ chartsCount,
+}) => {
+ const {
+ services: {
+ application: { capabilities },
+ },
+ } = useMlKibana();
+ const [isMenuOpen, setIsMenuOpen] = useState(false);
+ const [isAddDashboardsActive, setIsAddDashboardActive] = useState(false);
+
+ const canEditDashboards = capabilities.dashboard?.createNew ?? false;
+ const menuItems = useMemo(() => {
+ const items = [];
+ if (canEditDashboards) {
+ items.push(
+
+
+
+ );
+ }
+ return items;
+ }, [canEditDashboards]);
+
+ const jobIds = selectedJobs.map(({ id }) => id);
+
+ return (
+ <>
+ {menuItems.length > 0 && (
+
+
+ }
+ isOpen={isMenuOpen}
+ closePopover={setIsMenuOpen.bind(null, false)}
+ panelPaddingSize="none"
+ anchorPosition="downLeft"
+ >
+
+
+
+ )}
+ {isAddDashboardsActive && selectedJobs && (
+ {
+ setIsAddDashboardActive(false);
+ if (callback) {
+ await callback();
+ }
+ }}
+ selectedCells={selectedCells}
+ bounds={bounds}
+ interval={interval}
+ jobIds={jobIds}
+ />
+ )}
+ >
+ );
+};
diff --git a/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx b/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx
index 7c63d4087ce1e..37967d18dbbd9 100644
--- a/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx
+++ b/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx
@@ -24,7 +24,7 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { OVERALL_LABEL, SWIMLANE_TYPE, VIEW_BY_JOB_LABEL } from './explorer_constants';
-import { AddToDashboardControl } from './add_to_dashboard_control';
+import { AddSwimlaneToDashboardControl } from './dashboard_controls/add_swimlane_to_dashboard_controls';
import { useMlKibana } from '../contexts/kibana';
import { TimeBuckets } from '../util/time_buckets';
import { UI_SETTINGS } from '../../../../../../src/plugins/data/common';
@@ -294,7 +294,7 @@ export const AnomalyTimeline: FC = React.memo(
)}
{isAddDashboardsActive && selectedJobs && (
- {
setIsAddDashboardActive(false);
if (callback) {
diff --git a/x-pack/plugins/ml/public/application/explorer/dashboard_controls/add_anomaly_charts_to_dashboard_controls.tsx b/x-pack/plugins/ml/public/application/explorer/dashboard_controls/add_anomaly_charts_to_dashboard_controls.tsx
new file mode 100644
index 0000000000000..5c3c6edee59c5
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/explorer/dashboard_controls/add_anomaly_charts_to_dashboard_controls.tsx
@@ -0,0 +1,128 @@
+/*
+ * 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, { FC, useCallback, useState } from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiFieldNumber, EuiFormRow, formatDate } from '@elastic/eui';
+import { useDashboardTable } from './use_dashboards_table';
+import { AddToDashboardControl } from './add_to_dashboard_controls';
+import { useAddToDashboardActions } from './use_add_to_dashboard_actions';
+import { AppStateSelectedCells, getSelectionTimeRange } from '../explorer_utils';
+import { TimeRange } from '../../../../../../../src/plugins/data/common/query';
+import { DEFAULT_MAX_SERIES_TO_PLOT } from '../../services/anomaly_explorer_charts_service';
+import { JobId } from '../../../../common/types/anomaly_detection_jobs';
+import { ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE } from '../../../embeddables';
+import { getDefaultExplorerChartsPanelTitle } from '../../../embeddables/anomaly_charts/anomaly_charts_embeddable';
+import { TimeRangeBounds } from '../../util/time_buckets';
+import { useTableSeverity } from '../../components/controls/select_severity';
+import { MAX_ANOMALY_CHARTS_ALLOWED } from '../../../embeddables/anomaly_charts/anomaly_charts_initializer';
+
+function getDefaultEmbeddablePanelConfig(jobIds: JobId[]) {
+ return {
+ type: ANOMALY_EXPLORER_CHARTS_EMBEDDABLE_TYPE,
+ title: getDefaultExplorerChartsPanelTitle(jobIds),
+ };
+}
+
+export interface AddToDashboardControlProps {
+ jobIds: string[];
+ selectedCells?: AppStateSelectedCells;
+ bounds?: TimeRangeBounds;
+ interval?: number;
+ onClose: (callback?: () => Promise) => void;
+}
+
+/**
+ * Component for attaching anomaly swim lane embeddable to dashboards.
+ */
+export const AddAnomalyChartsToDashboardControl: FC = ({
+ onClose,
+ jobIds,
+ selectedCells,
+ bounds,
+ interval,
+}) => {
+ const [severity] = useTableSeverity();
+ const [maxSeriesToPlot, setMaxSeriesToPlot] = useState(DEFAULT_MAX_SERIES_TO_PLOT);
+
+ const getPanelsData = useCallback(async () => {
+ let timeRange: TimeRange | undefined;
+ if (selectedCells !== undefined && interval !== undefined && bounds !== undefined) {
+ const { earliestMs, latestMs } = getSelectionTimeRange(selectedCells, interval, bounds);
+ timeRange = {
+ from: formatDate(earliestMs, 'MMM D, YYYY @ HH:mm:ss.SSS'),
+ to: formatDate(latestMs, 'MMM D, YYYY @ HH:mm:ss.SSS'),
+ mode: 'absolute',
+ };
+ }
+
+ const config = getDefaultEmbeddablePanelConfig(jobIds);
+ return [
+ {
+ ...config,
+ embeddableConfig: {
+ jobIds,
+ maxSeriesToPlot: maxSeriesToPlot ?? DEFAULT_MAX_SERIES_TO_PLOT,
+ severityThreshold: severity.val,
+ ...(timeRange ?? {}),
+ },
+ },
+ ];
+ }, [selectedCells, interval, bounds, jobIds, maxSeriesToPlot, severity]);
+
+ const { selectedItems, selection, dashboardItems, isLoading, search } = useDashboardTable();
+ const { addToDashboardAndEditCallback, addToDashboardCallback } = useAddToDashboardActions({
+ onClose,
+ getPanelsData,
+ selectedDashboards: selectedItems,
+ });
+ const title = (
+
+ );
+
+ const disabled = selectedItems.length < 1 && !Array.isArray(jobIds === undefined);
+
+ const extraControls = (
+
+ }
+ >
+ setMaxSeriesToPlot(parseInt(e.target.value, 10))}
+ min={0}
+ max={MAX_ANOMALY_CHARTS_ALLOWED}
+ />
+
+ );
+
+ return (
+