From 6af5828b3d4b37af9ba6facb0bbeedc700a26bce Mon Sep 17 00:00:00 2001
From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Date: Wed, 7 Apr 2021 23:06:05 -0400
Subject: [PATCH 01/22] [Security Solution][Detections]Fixes Rule Management
Cypress Tests (#96505) (#96521)
## Summary
Fixes two cypress tests:
> Deleting prebuilt rules "before each" hook for "Does not allow to delete one rule when more than one is selected"
https://github.com/elastic/kibana/issues/68607
This one is more of a drive around the pot-hole fix as we were waiting for the Alerts Table to load when we really didn't need to. Removed unnecessary check.
> Alerts rules, prebuilt rules Loads prebuilt rules
https://github.com/elastic/kibana/issues/71300
This one was fixed with a `.pipe()` and `.should('not.be.visible')` to ensure the click was successful. Also removed unnecessary check on the Alerts Table loading that was present here as well too..
Co-authored-by: Garrett Spong
---
.../integration/detection_rules/prebuilt_rules.spec.ts | 8 +-------
.../cypress/tasks/alerts_detection_rules.ts | 5 ++++-
2 files changed, 5 insertions(+), 8 deletions(-)
diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts
index d290773d425e2..fb0a01bd1c7d3 100644
--- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts
@@ -14,11 +14,7 @@ import {
SHOWING_RULES_TEXT,
} from '../../screens/alerts_detection_rules';
-import {
- goToManageAlertsDetectionRules,
- waitForAlertsIndexToBeCreated,
- waitForAlertsPanelToBeLoaded,
-} from '../../tasks/alerts';
+import { goToManageAlertsDetectionRules, waitForAlertsIndexToBeCreated } from '../../tasks/alerts';
import {
changeRowsPerPageTo300,
deleteFirstRule,
@@ -47,7 +43,6 @@ describe('Alerts rules, prebuilt rules', () => {
const expectedElasticRulesBtnText = `Elastic rules (${expectedNumberOfRules})`;
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
- waitForAlertsPanelToBeLoaded();
waitForAlertsIndexToBeCreated();
goToManageAlertsDetectionRules();
waitForRulesTableToBeLoaded();
@@ -79,7 +74,6 @@ describe('Deleting prebuilt rules', () => {
cleanKibana();
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
- waitForAlertsPanelToBeLoaded();
waitForAlertsIndexToBeCreated();
goToManageAlertsDetectionRules();
waitForRulesTableToBeLoaded();
diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts
index 10644e046a68b..d66b839267ea0 100644
--- a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts
+++ b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts
@@ -191,7 +191,10 @@ export const resetAllRulesIdleModalTimeout = () => {
export const changeRowsPerPageTo = (rowsCount: number) => {
cy.get(PAGINATION_POPOVER_BTN).click({ force: true });
- cy.get(rowsPerPageSelector(rowsCount)).click();
+ cy.get(rowsPerPageSelector(rowsCount))
+ .pipe(($el) => $el.trigger('click'))
+ .should('not.be.visible');
+
waitForRulesTableToBeRefreshed();
};
From 36d86ad29ad93121b3b1fda62cb403801debb180 Mon Sep 17 00:00:00 2001
From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Date: Thu, 8 Apr 2021 02:36:07 -0400
Subject: [PATCH 02/22] [Security Solution][Detections] Fixes Closing Alerts
Cypress Test (#96523) (#96526)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Summary
As identified in https://github.com/elastic/kibana/pull/96505#issuecomment-815392671, this fixes the flakiness in the `Closing alerts` cypress test. Method used was to just delete the rule after the initial batch of alerts were generated. Alternatively we could add a function for disabling the rule (didn't see one in there), but the outcome is the same, no more alerts generated while the test is being performed. 🙂
> Passing locally, though upon further inspection, this test is definitely going to be flakey as it's checking counts on alerts as they move through different states and there are new alerts that keep coming in (hence the count mis-match in the above failure). Potential fixes would be to use an absolute daterange to after the first round of alerts were generated, or just stop generating alerts before performing the alert state changes.
##### Before:
##### After:
Co-authored-by: Garrett Spong
---
.../cypress/integration/detection_alerts/closing.spec.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/closing.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/closing.spec.ts
index e9d17a361d336..b7c0e1c6fcd6e 100644
--- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/closing.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/closing.spec.ts
@@ -25,7 +25,7 @@ import {
waitForAlerts,
waitForAlertsIndexToBeCreated,
} from '../../tasks/alerts';
-import { createCustomRuleActivated } from '../../tasks/api_calls/rules';
+import { createCustomRuleActivated, deleteCustomRule } from '../../tasks/api_calls/rules';
import { cleanKibana } from '../../tasks/common';
import { waitForAlertsToPopulate } from '../../tasks/create_new_rule';
import { loginAndWaitForPage } from '../../tasks/login';
@@ -42,6 +42,7 @@ describe('Closing alerts', () => {
createCustomRuleActivated(newRule);
refreshPage();
waitForAlertsToPopulate();
+ deleteCustomRule();
});
it('Closes and opens alerts', () => {
From 95992f0e984ed2dd9e2959cd5f1059b6e74c9a1f Mon Sep 17 00:00:00 2001
From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Date: Thu, 8 Apr 2021 03:01:58 -0400
Subject: [PATCH 03/22] [Fleet] Install security_rule assets as saved objects
(#95885) (#96527)
* [Fleet] Install security_rule assets as saved objects
* Add security-rule to update_assets.ts
* Update UUIDs for security_rule asset
* Change .type to match the saved object type not the asset type
* Add saved object mapping for security-rule
* Make SO non-hidden
* Fix SO mapping for security-rule
* Make security-rule a non-hidden asset
Co-authored-by: Ross Wolf <31489089+rw-access@users.noreply.github.com>
---
.../package_to_package_policy.test.ts | 1 +
.../plugins/fleet/common/types/models/epm.ts | 2 +
.../fleet/sections/epm/constants.tsx | 2 +
.../services/epm/kibana/assets/install.ts | 2 +
.../services/epm/packages/assets.test.ts | 2 +-
.../rules/saved_object_mappings.ts | 24 +++++++++
.../security_solution/server/saved_objects.ts | 6 ++-
.../apis/epm/install_remove_assets.ts | 10 ++++
.../apis/epm/update_assets.ts | 5 ++
.../security_rule/sample_security_rule.json | 50 +++++++++++++++++++
.../security_rule/sample_security_rule.json | 50 +++++++++++++++++++
11 files changed, 152 insertions(+), 2 deletions(-)
create mode 100644 x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/security_rule/sample_security_rule.json
create mode 100644 x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/security_rule/sample_security_rule.json
diff --git a/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts b/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts
index a4cca4455a274..65b853ed5b38f 100644
--- a/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts
+++ b/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts
@@ -31,6 +31,7 @@ describe('Fleet - packageToPackagePolicy', () => {
map: [],
lens: [],
ml_module: [],
+ security_rule: [],
},
elasticsearch: {
ingest_pipeline: [],
diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts
index 80fabd51613ae..3bc0d97d64646 100644
--- a/x-pack/plugins/fleet/common/types/models/epm.ts
+++ b/x-pack/plugins/fleet/common/types/models/epm.ts
@@ -50,6 +50,7 @@ export enum KibanaAssetType {
indexPattern = 'index_pattern',
map = 'map',
lens = 'lens',
+ securityRule = 'security_rule',
mlModule = 'ml_module',
}
@@ -64,6 +65,7 @@ export enum KibanaSavedObjectType {
map = 'map',
lens = 'lens',
mlModule = 'ml-module',
+ securityRule = 'security-rule',
}
export enum ElasticsearchAssetType {
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/constants.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/constants.tsx
index ea19a330adfee..6ddff968bd3f3 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/constants.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/constants.tsx
@@ -33,6 +33,7 @@ export const AssetTitleMap: Record = {
map: 'Map',
data_stream_ilm_policy: 'Data Stream ILM Policy',
lens: 'Lens',
+ security_rule: 'Security Rule',
ml_module: 'ML Module',
};
@@ -48,6 +49,7 @@ export const AssetIcons: Record = {
visualization: 'visualizeApp',
map: 'emsApp',
lens: 'lensApp',
+ security_rule: 'securityApp',
ml_module: 'mlApp',
};
diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts
index bfcc40e18fe80..0f2d7b6679bf9 100644
--- a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts
+++ b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts
@@ -38,6 +38,7 @@ const KibanaSavedObjectTypeMapping: Record {
diff --git a/x-pack/plugins/fleet/server/services/epm/packages/assets.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/assets.test.ts
index 999cf878d07b7..c5b104696aaf4 100644
--- a/x-pack/plugins/fleet/server/services/epm/packages/assets.test.ts
+++ b/x-pack/plugins/fleet/server/services/epm/packages/assets.test.ts
@@ -43,7 +43,7 @@ const tests = [
name: 'coredns',
version: '1.0.1',
},
- // Non existant dataset
+ // Non existent dataset
dataset: 'foo',
filter: (path: string) => {
return true;
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/saved_object_mappings.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/saved_object_mappings.ts
index 4ed53e39fa5eb..813e800f34ce2 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/saved_object_mappings.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/saved_object_mappings.ts
@@ -53,3 +53,27 @@ export const type: SavedObjectsType = {
namespaceType: 'single',
mappings: ruleStatusSavedObjectMappings,
};
+
+export const ruleAssetSavedObjectType = 'security-rule';
+
+export const ruleAssetSavedObjectMappings: SavedObjectsType['mappings'] = {
+ dynamic: false,
+ properties: {
+ name: {
+ type: 'keyword',
+ },
+ rule_id: {
+ type: 'keyword',
+ },
+ version: {
+ type: 'long',
+ },
+ },
+};
+
+export const ruleAssetType: SavedObjectsType = {
+ name: ruleAssetSavedObjectType,
+ hidden: false,
+ namespaceType: 'agnostic',
+ mappings: ruleAssetSavedObjectMappings,
+};
diff --git a/x-pack/plugins/security_solution/server/saved_objects.ts b/x-pack/plugins/security_solution/server/saved_objects.ts
index d483bd25266af..42abb3dab2ac4 100644
--- a/x-pack/plugins/security_solution/server/saved_objects.ts
+++ b/x-pack/plugins/security_solution/server/saved_objects.ts
@@ -8,7 +8,10 @@
import { CoreSetup } from '../../../../src/core/server';
import { noteType, pinnedEventType, timelineType } from './lib/timeline/saved_object_mappings';
-import { type as ruleStatusType } from './lib/detection_engine/rules/saved_object_mappings';
+import {
+ type as ruleStatusType,
+ ruleAssetType,
+} from './lib/detection_engine/rules/saved_object_mappings';
import { type as ruleActionsType } from './lib/detection_engine/rule_actions/saved_object_mappings';
import { type as signalsMigrationType } from './lib/detection_engine/migrations/saved_objects';
import {
@@ -21,6 +24,7 @@ const types = [
pinnedEventType,
ruleActionsType,
ruleStatusType,
+ ruleAssetType,
timelineType,
exceptionsArtifactType,
manifestType,
diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts b/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts
index abc91a973e6b6..8e09e331bf867 100644
--- a/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts
+++ b/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts
@@ -399,6 +399,11 @@ const expectAssetsInstalled = ({
id: 'sample_ml_module',
});
expect(resMlModule.id).equal('sample_ml_module');
+ const resSecurityRule = await kibanaServer.savedObjects.get({
+ type: 'security-rule',
+ id: 'sample_security_rule',
+ });
+ expect(resSecurityRule.id).equal('sample_security_rule');
const resIndexPattern = await kibanaServer.savedObjects.get({
type: 'index-pattern',
id: 'test-*',
@@ -472,6 +477,10 @@ const expectAssetsInstalled = ({
id: 'sample_search',
type: 'search',
},
+ {
+ id: 'sample_security_rule',
+ type: 'security-rule',
+ },
{
id: 'sample_visualization',
type: 'visualization',
@@ -537,6 +546,7 @@ const expectAssetsInstalled = ({
{ id: 'e21b59b5-eb76-5ab0-bef2-1c8e379e6197', type: 'epm-packages-assets' },
{ id: '4c758d70-ecf1-56b3-b704-6d8374841b34', type: 'epm-packages-assets' },
{ id: 'e786cbd9-0f3b-5a0b-82a6-db25145ebf58', type: 'epm-packages-assets' },
+ { id: 'd8b175c3-0d42-5ec7-90c1-d1e4b307a4c2', type: 'epm-packages-assets' },
{ id: '53c94591-aa33-591d-8200-cd524c2a0561', type: 'epm-packages-assets' },
{ id: 'b658d2d4-752e-54b8-afc2-4c76155c1466', type: 'epm-packages-assets' },
],
diff --git a/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts b/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts
index 1a559ac5a5c75..9b55822311bd7 100644
--- a/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts
+++ b/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts
@@ -296,6 +296,10 @@ export default function (providerContext: FtrProviderContext) {
id: 'sample_lens',
type: 'lens',
},
+ {
+ id: 'sample_security_rule',
+ type: 'security-rule',
+ },
{
id: 'sample_ml_module',
type: 'ml-module',
@@ -350,6 +354,7 @@ export default function (providerContext: FtrProviderContext) {
{ id: '7f4c5aca-b4f5-5f0a-95af-051da37513fc', type: 'epm-packages-assets' },
{ id: '4281a436-45a8-54ab-9724-fda6849f789d', type: 'epm-packages-assets' },
{ id: '2e56f08b-1d06-55ed-abee-4708e1ccf0aa', type: 'epm-packages-assets' },
+ { id: '4035007b-9c33-5227-9803-2de8a17523b5', type: 'epm-packages-assets' },
{ id: 'c7bf1a39-e057-58a0-afde-fb4b48751d8c', type: 'epm-packages-assets' },
{ id: '8c665f28-a439-5f43-b5fd-8fda7b576735', type: 'epm-packages-assets' },
],
diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/security_rule/sample_security_rule.json b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/security_rule/sample_security_rule.json
new file mode 100644
index 0000000000000..6bedde67b8923
--- /dev/null
+++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/security_rule/sample_security_rule.json
@@ -0,0 +1,50 @@
+{
+ "attributes": {
+ "author": [
+ "Elastic"
+ ],
+ "description": "Identifies a suspicious parent child process relationship with cmd.exe descending from svchost.exe",
+ "from": "now-9m",
+ "index": [
+ "winlogbeat-*",
+ "logs-endpoint.events.*",
+ "logs-windows.*"
+ ],
+ "language": "kuery",
+ "license": "Elastic License v2",
+ "name": "Svchost spawning Cmd",
+ "query": "event.category:process and event.type:(start or process_started) and process.parent.name:svchost.exe and process.name:cmd.exe",
+ "risk_score": 21,
+ "rule_id": "sample_security_rule",
+ "severity": "low",
+ "tags": [
+ "Elastic",
+ "Host",
+ "Windows",
+ "Threat Detection",
+ "Execution"
+ ],
+ "threat": [
+ {
+ "framework": "MITRE ATT\u0026CK",
+ "tactic": {
+ "id": "TA0002",
+ "name": "Execution",
+ "reference": "https://attack.mitre.org/tactics/TA0002/"
+ },
+ "technique": [
+ {
+ "id": "T1059",
+ "name": "Command and Scripting Interpreter",
+ "reference": "https://attack.mitre.org/techniques/T1059/"
+ }
+ ]
+ }
+ ],
+ "timestamp_override": "event.ingested",
+ "type": "query",
+ "version": 7
+ },
+ "id": "sample_security_rule",
+ "type": "security-rule"
+}
diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/security_rule/sample_security_rule.json b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/security_rule/sample_security_rule.json
new file mode 100644
index 0000000000000..6bedde67b8923
--- /dev/null
+++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/security_rule/sample_security_rule.json
@@ -0,0 +1,50 @@
+{
+ "attributes": {
+ "author": [
+ "Elastic"
+ ],
+ "description": "Identifies a suspicious parent child process relationship with cmd.exe descending from svchost.exe",
+ "from": "now-9m",
+ "index": [
+ "winlogbeat-*",
+ "logs-endpoint.events.*",
+ "logs-windows.*"
+ ],
+ "language": "kuery",
+ "license": "Elastic License v2",
+ "name": "Svchost spawning Cmd",
+ "query": "event.category:process and event.type:(start or process_started) and process.parent.name:svchost.exe and process.name:cmd.exe",
+ "risk_score": 21,
+ "rule_id": "sample_security_rule",
+ "severity": "low",
+ "tags": [
+ "Elastic",
+ "Host",
+ "Windows",
+ "Threat Detection",
+ "Execution"
+ ],
+ "threat": [
+ {
+ "framework": "MITRE ATT\u0026CK",
+ "tactic": {
+ "id": "TA0002",
+ "name": "Execution",
+ "reference": "https://attack.mitre.org/tactics/TA0002/"
+ },
+ "technique": [
+ {
+ "id": "T1059",
+ "name": "Command and Scripting Interpreter",
+ "reference": "https://attack.mitre.org/techniques/T1059/"
+ }
+ ]
+ }
+ ],
+ "timestamp_override": "event.ingested",
+ "type": "query",
+ "version": 7
+ },
+ "id": "sample_security_rule",
+ "type": "security-rule"
+}
From 5180afc26312995db2cba7d373a725bafcf9ac20 Mon Sep 17 00:00:00 2001
From: Garrett Spong
Date: Thu, 8 Apr 2021 02:13:14 -0600
Subject: [PATCH 04/22] [Detection Rules] Resolves regression where Elastic
Endgame rules would warn about unmapped timestamp override field (#96394)
(#96528)
related to https://github.com/elastic/detection-rules/pull/1082
## Summary
Endgame promotion rules in Kibana/7.12 are at version 5 and have timestamp_override defined (which should not be). These same rules are at version 4 in the detection-rules repo 7.12 branch and kibana/master and timestamp_override is not defined. These updates are targeted for 7.12.1
There most likely was an issue with the maze of backports and interlaced updates.
To fix the rules, they need to be reconciled across:
detection-rules 7.12 & main
kibana 7.12.1 and master
bump detection-rules/7.12 to v6 -> PR to kibana/master -> backport to 7.12.1
### Checklist
Delete any items that are not applicable to this PR.
- [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md)
### For maintainers
- [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
# Conflicts:
# x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_adversary_behavior_detected.json
# x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_dumping_detected.json
# x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_dumping_prevented.json
# x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_manipulation_detected.json
# x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_manipulation_prevented.json
# x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_exploit_detected.json
# x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_exploit_prevented.json
# x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_malware_detected.json
# x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_malware_prevented.json
# x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_permission_theft_detected.json
# x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_permission_theft_prevented.json
# x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_process_injection_detected.json
# x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_process_injection_prevented.json
# x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_ransomware_detected.json
# x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_ransomware_prevented.json
Co-authored-by: Justin Ibarra
---
.../prepackaged_rules/endgame_adversary_behavior_detected.json | 3 +--
.../rules/prepackaged_rules/endgame_cred_dumping_detected.json | 3 +--
.../prepackaged_rules/endgame_cred_dumping_prevented.json | 3 +--
.../prepackaged_rules/endgame_cred_manipulation_detected.json | 3 +--
.../prepackaged_rules/endgame_cred_manipulation_prevented.json | 3 +--
.../rules/prepackaged_rules/endgame_exploit_detected.json | 3 +--
.../rules/prepackaged_rules/endgame_exploit_prevented.json | 3 +--
.../rules/prepackaged_rules/endgame_malware_detected.json | 3 +--
.../rules/prepackaged_rules/endgame_malware_prevented.json | 3 +--
.../prepackaged_rules/endgame_permission_theft_detected.json | 3 +--
.../prepackaged_rules/endgame_permission_theft_prevented.json | 3 +--
.../prepackaged_rules/endgame_process_injection_detected.json | 3 +--
.../prepackaged_rules/endgame_process_injection_prevented.json | 3 +--
.../rules/prepackaged_rules/endgame_ransomware_detected.json | 3 +--
.../rules/prepackaged_rules/endgame_ransomware_prevented.json | 3 +--
15 files changed, 15 insertions(+), 30 deletions(-)
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_adversary_behavior_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_adversary_behavior_detected.json
index bf75f431da71e..bf53625cef750 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_adversary_behavior_detected.json
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_adversary_behavior_detected.json
@@ -19,7 +19,6 @@
"Elastic",
"Elastic Endgame"
],
- "timestamp_override": "event.ingested",
"type": "query",
- "version": 5
+ "version": 6
}
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_dumping_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_dumping_detected.json
index 3bc78b5f9b333..43cb19f50d675 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_dumping_detected.json
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_dumping_detected.json
@@ -19,7 +19,6 @@
"Elastic",
"Elastic Endgame"
],
- "timestamp_override": "event.ingested",
"type": "query",
- "version": 5
+ "version": 6
}
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_dumping_prevented.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_dumping_prevented.json
index 4c81580727c27..29b5bc3f39cf1 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_dumping_prevented.json
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_dumping_prevented.json
@@ -19,7 +19,6 @@
"Elastic",
"Elastic Endgame"
],
- "timestamp_override": "event.ingested",
"type": "query",
- "version": 5
+ "version": 6
}
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_manipulation_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_manipulation_detected.json
index 530daa7d58624..393591a241114 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_manipulation_detected.json
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_manipulation_detected.json
@@ -19,7 +19,6 @@
"Elastic",
"Elastic Endgame"
],
- "timestamp_override": "event.ingested",
"type": "query",
- "version": 5
+ "version": 6
}
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_manipulation_prevented.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_manipulation_prevented.json
index c23c7cc4e301c..e9ca199c4a791 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_manipulation_prevented.json
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_cred_manipulation_prevented.json
@@ -19,7 +19,6 @@
"Elastic",
"Elastic Endgame"
],
- "timestamp_override": "event.ingested",
"type": "query",
- "version": 5
+ "version": 6
}
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_exploit_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_exploit_detected.json
index fed4b0d87c54e..a169582c2da92 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_exploit_detected.json
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_exploit_detected.json
@@ -19,7 +19,6 @@
"Elastic",
"Elastic Endgame"
],
- "timestamp_override": "event.ingested",
"type": "query",
- "version": 5
+ "version": 6
}
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_exploit_prevented.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_exploit_prevented.json
index 84c4c0356bb6c..b781a1fae1847 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_exploit_prevented.json
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_exploit_prevented.json
@@ -19,7 +19,6 @@
"Elastic",
"Elastic Endgame"
],
- "timestamp_override": "event.ingested",
"type": "query",
- "version": 5
+ "version": 6
}
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_malware_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_malware_detected.json
index 0bca9c21bcc3b..f7a064961f039 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_malware_detected.json
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_malware_detected.json
@@ -19,7 +19,6 @@
"Elastic",
"Elastic Endgame"
],
- "timestamp_override": "event.ingested",
"type": "query",
- "version": 5
+ "version": 6
}
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_malware_prevented.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_malware_prevented.json
index 192c1b40e4c10..59cbd98e2d42b 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_malware_prevented.json
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_malware_prevented.json
@@ -19,7 +19,6 @@
"Elastic",
"Elastic Endgame"
],
- "timestamp_override": "event.ingested",
"type": "query",
- "version": 5
+ "version": 6
}
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_permission_theft_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_permission_theft_detected.json
index 5f88961b724b8..b3db96d6d121b 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_permission_theft_detected.json
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_permission_theft_detected.json
@@ -19,7 +19,6 @@
"Elastic",
"Elastic Endgame"
],
- "timestamp_override": "event.ingested",
"type": "query",
- "version": 5
+ "version": 6
}
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_permission_theft_prevented.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_permission_theft_prevented.json
index 80745c301ca41..18b316a293da8 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_permission_theft_prevented.json
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_permission_theft_prevented.json
@@ -19,7 +19,6 @@
"Elastic",
"Elastic Endgame"
],
- "timestamp_override": "event.ingested",
"type": "query",
- "version": 5
+ "version": 6
}
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_process_injection_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_process_injection_detected.json
index 2510abc2b7bf3..861daa2d004c7 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_process_injection_detected.json
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_process_injection_detected.json
@@ -19,7 +19,6 @@
"Elastic",
"Elastic Endgame"
],
- "timestamp_override": "event.ingested",
"type": "query",
- "version": 5
+ "version": 6
}
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_process_injection_prevented.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_process_injection_prevented.json
index 79c262a8d68f5..5f78a3517e931 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_process_injection_prevented.json
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_process_injection_prevented.json
@@ -19,7 +19,6 @@
"Elastic",
"Elastic Endgame"
],
- "timestamp_override": "event.ingested",
"type": "query",
- "version": 5
+ "version": 6
}
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_ransomware_detected.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_ransomware_detected.json
index 6d378f712810f..4c060bb52f32f 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_ransomware_detected.json
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_ransomware_detected.json
@@ -19,7 +19,6 @@
"Elastic",
"Elastic Endgame"
],
- "timestamp_override": "event.ingested",
"type": "query",
- "version": 5
+ "version": 6
}
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_ransomware_prevented.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_ransomware_prevented.json
index 0e4373825c950..78845ffc4c845 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_ransomware_prevented.json
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/endgame_ransomware_prevented.json
@@ -19,7 +19,6 @@
"Elastic",
"Elastic Endgame"
],
- "timestamp_override": "event.ingested",
"type": "query",
- "version": 5
+ "version": 6
}
From 048b9f3c171fb14e54b9a166e3663bda342ce185 Mon Sep 17 00:00:00 2001
From: spalger
Date: Thu, 8 Apr 2021 02:09:29 -0700
Subject: [PATCH 05/22] skip suite blocking es promotion (#96515)
(cherry picked from commit f9317281d1ebdb8fb2aadf9f6a716cd776a51299)
---
x-pack/test/fleet_api_integration/apis/agents_setup.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/x-pack/test/fleet_api_integration/apis/agents_setup.ts b/x-pack/test/fleet_api_integration/apis/agents_setup.ts
index 91d6ca0119d1d..d49bc91251b01 100644
--- a/x-pack/test/fleet_api_integration/apis/agents_setup.ts
+++ b/x-pack/test/fleet_api_integration/apis/agents_setup.ts
@@ -15,7 +15,8 @@ export default function (providerContext: FtrProviderContext) {
const es = getService('es');
const esArchiver = getService('esArchiver');
- describe('fleet_agents_setup', () => {
+ // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/96515
+ describe.skip('fleet_agents_setup', () => {
skipIfNoDockerRegistry(providerContext);
before(async () => {
await esArchiver.load('empty_kibana');
From 8589455db7e7368ccf70505bdb0965e14dcdb883 Mon Sep 17 00:00:00 2001
From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com>
Date: Thu, 8 Apr 2021 06:23:19 -0400
Subject: [PATCH 06/22] [SECURITY SOLUTION] Add new exception list type and
feature flag for event filtering (#96037) (#96485)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* New exception list type for event filtering
* New feature flag for event filtering
Co-authored-by: David Sánchez
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
x-pack/plugins/lists/common/schemas/common/schemas.ts | 7 ++++++-
.../common/detection_engine/schemas/types/lists.test.ts | 6 +++---
.../security_solution/common/experimental_features.ts | 1 +
3 files changed, 10 insertions(+), 4 deletions(-)
diff --git a/x-pack/plugins/lists/common/schemas/common/schemas.ts b/x-pack/plugins/lists/common/schemas/common/schemas.ts
index e553b65a2f610..f261e4e3eefa6 100644
--- a/x-pack/plugins/lists/common/schemas/common/schemas.ts
+++ b/x-pack/plugins/lists/common/schemas/common/schemas.ts
@@ -212,13 +212,18 @@ export type Tags = t.TypeOf;
export const tagsOrUndefined = t.union([tags, t.undefined]);
export type TagsOrUndefined = t.TypeOf;
-export const exceptionListType = t.keyof({ detection: null, endpoint: null });
+export const exceptionListType = t.keyof({
+ detection: null,
+ endpoint: null,
+ endpoint_events: null,
+});
export const exceptionListTypeOrUndefined = t.union([exceptionListType, t.undefined]);
export type ExceptionListType = t.TypeOf;
export type ExceptionListTypeOrUndefined = t.TypeOf;
export enum ExceptionListTypeEnum {
DETECTION = 'detection',
ENDPOINT = 'endpoint',
+ ENDPOINT_EVENTS = 'endpoint_events',
}
export const exceptionListItemType = t.keyof({ simple: null });
diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.test.ts
index e331eea51eec0..28b70f51742a7 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.test.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.test.ts
@@ -94,7 +94,7 @@ describe('Lists', () => {
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
- 'Invalid value "1" supplied to "Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint", namespace_type: "agnostic" | "single" |}>"',
+ 'Invalid value "1" supplied to "Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint" | "endpoint_events", namespace_type: "agnostic" | "single" |}>"',
]);
expect(message.schema).toEqual({});
});
@@ -125,8 +125,8 @@ describe('Lists', () => {
const message = pipe(decoded, foldLeftRight);
expect(getPaths(left(message.errors))).toEqual([
- 'Invalid value "1" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint", namespace_type: "agnostic" | "single" |}> | undefined)"',
- 'Invalid value "[1]" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint", namespace_type: "agnostic" | "single" |}> | undefined)"',
+ 'Invalid value "1" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint" | "endpoint_events", namespace_type: "agnostic" | "single" |}> | undefined)"',
+ 'Invalid value "[1]" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint" | "endpoint_events", namespace_type: "agnostic" | "single" |}> | undefined)"',
]);
expect(message.schema).toEqual({});
});
diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts
index 19de81cb95c3f..39551e3ee6f1c 100644
--- a/x-pack/plugins/security_solution/common/experimental_features.ts
+++ b/x-pack/plugins/security_solution/common/experimental_features.ts
@@ -14,6 +14,7 @@ export type ExperimentalFeatures = typeof allowedExperimentalValues;
const allowedExperimentalValues = Object.freeze({
fleetServerEnabled: false,
trustedAppsByPolicyEnabled: false,
+ eventFilteringEnabled: false,
});
type ExperimentalConfigKeys = Array;
From ed3d8b574d040270acf1ca6cb3da6b46cf64f84c Mon Sep 17 00:00:00 2001
From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Date: Thu, 8 Apr 2021 07:34:28 -0400
Subject: [PATCH 07/22] Skip rendering empty add action variables button as
disabled (#96342) (#96541)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Skip rendering empty add action variables button
* Fix jest tests
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Mike Côté
---
.../components/add_message_variables.test.tsx | 12 ++++++++++++
.../application/components/add_message_variables.tsx | 7 +++++--
.../es_index/es_index_params.test.tsx | 7 +++++++
.../pagerduty/pagerduty_params.test.tsx | 7 +++++++
.../webhook/webhook_params.test.tsx | 7 +++++++
5 files changed, 38 insertions(+), 2 deletions(-)
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.test.tsx
index 8d27edd9e4bcc..4e03a2a09bed4 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.test.tsx
@@ -117,4 +117,16 @@ describe('AddMessageVariables', () => {
wrapper.find('button[data-test-subj="variableMenuButton-deprecatedVar"]').getDOMNode()
).toBeDisabled();
});
+
+ test(`it does't render when no variables exist`, () => {
+ const wrapper = mountWithIntl(
+
+ );
+
+ expect(wrapper.find('[data-test-subj="fooAddVariableButton"]')).toHaveLength(0);
+ });
});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx
index bf89e4f6ae6e1..57b251fba0d45 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React, { useState } from 'react';
+import React, { useState, Fragment } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiPopover,
@@ -61,13 +61,16 @@ export const AddMessageVariables: React.FunctionComponent = ({
}
);
+ if ((messageVariables?.length ?? 0) === 0) {
+ return ;
+ }
+
return (
setIsVariablesPopoverOpen(true)}
iconType="indexOpen"
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx
index 97c1c41f68730..b792cf6574455 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx
@@ -22,6 +22,13 @@ describe('IndexParamsFields renders', () => {
errors={{ index: [] }}
editAction={() => {}}
index={0}
+ messageVariables={[
+ {
+ name: 'myVar',
+ description: 'My variable description',
+ useWithTripleBracesInTemplates: true,
+ },
+ ]}
/>
);
expect(wrapper.find('[data-test-subj="documentsJsonEditor"]').first().prop('value')).toBe(`{
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx
index 6e3f4213b7907..4d47cbf3685a1 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx
@@ -30,6 +30,13 @@ describe('PagerDutyParamsFields renders', () => {
errors={{ summary: [], timestamp: [], dedupKey: [] }}
editAction={() => {}}
index={0}
+ messageVariables={[
+ {
+ name: 'myVar',
+ description: 'My variable description',
+ useWithTripleBracesInTemplates: true,
+ },
+ ]}
/>
);
expect(wrapper.find('[data-test-subj="severitySelect"]').length > 0).toBeTruthy();
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.test.tsx
index 801d9a6b43ec6..a3756ae74fd14 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_params.test.tsx
@@ -21,6 +21,13 @@ describe('WebhookParamsFields renders', () => {
errors={{ body: [] }}
editAction={() => {}}
index={0}
+ messageVariables={[
+ {
+ name: 'myVar',
+ description: 'My variable description',
+ useWithTripleBracesInTemplates: true,
+ },
+ ]}
/>
);
expect(wrapper.find('[data-test-subj="bodyJsonEditor"]').length > 0).toBeTruthy();
From e88148235deff84ba64d1514d40e012f599d5893 Mon Sep 17 00:00:00 2001
From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Date: Thu, 8 Apr 2021 08:43:05 -0400
Subject: [PATCH 08/22] [Telemetry] enforce import export type (#96487)
(#96546)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: hardikpnsp
---
packages/kbn-analytics/tsconfig.json | 1 +
packages/kbn-telemetry-tools/src/tools/tasks/index.ts | 4 +++-
packages/kbn-telemetry-tools/tsconfig.json | 3 ++-
src/plugins/kibana_usage_collection/tsconfig.json | 3 ++-
.../telemetry/common/telemetry_config/index.ts | 6 ++----
src/plugins/telemetry/public/index.ts | 2 +-
src/plugins/telemetry/server/index.ts | 9 ++++++---
.../telemetry_collection/get_data_telemetry/index.ts | 9 ++-------
.../telemetry/server/telemetry_collection/index.ts | 11 ++++-------
.../telemetry/server/telemetry_repository/index.ts | 2 +-
src/plugins/telemetry/tsconfig.json | 3 ++-
.../telemetry_collection_manager/server/index.ts | 2 +-
.../telemetry_collection_manager/tsconfig.json | 3 ++-
.../telemetry_management_section/public/index.ts | 2 +-
.../telemetry_management_section/tsconfig.json | 3 ++-
src/plugins/usage_collection/public/index.ts | 2 +-
.../usage_collection/server/collector/index.ts | 10 ++++++----
src/plugins/usage_collection/server/index.ts | 7 +++----
.../usage_collection/server/usage_collection.mock.ts | 3 ++-
src/plugins/usage_collection/tsconfig.json | 3 ++-
.../telemetry_collection_xpack/server/index.ts | 2 +-
.../server/telemetry_collection/index.ts | 2 +-
.../plugins/telemetry_collection_xpack/tsconfig.json | 3 ++-
23 files changed, 50 insertions(+), 45 deletions(-)
diff --git a/packages/kbn-analytics/tsconfig.json b/packages/kbn-analytics/tsconfig.json
index c2e579e7fdbea..80a2255d71805 100644
--- a/packages/kbn-analytics/tsconfig.json
+++ b/packages/kbn-analytics/tsconfig.json
@@ -7,6 +7,7 @@
"emitDeclarationOnly": true,
"declaration": true,
"declarationMap": true,
+ "isolatedModules": true,
"sourceMap": true,
"sourceRoot": "../../../../../packages/kbn-analytics/src",
"types": [
diff --git a/packages/kbn-telemetry-tools/src/tools/tasks/index.ts b/packages/kbn-telemetry-tools/src/tools/tasks/index.ts
index 5d946b73d9759..f55a9aa80d40d 100644
--- a/packages/kbn-telemetry-tools/src/tools/tasks/index.ts
+++ b/packages/kbn-telemetry-tools/src/tools/tasks/index.ts
@@ -7,7 +7,9 @@
*/
export { ErrorReporter } from './error_reporter';
-export { TaskContext, createTaskContext } from './task_context';
+
+export type { TaskContext } from './task_context';
+export { createTaskContext } from './task_context';
export { parseConfigsTask } from './parse_configs_task';
export { extractCollectorsTask } from './extract_collectors_task';
diff --git a/packages/kbn-telemetry-tools/tsconfig.json b/packages/kbn-telemetry-tools/tsconfig.json
index 39946fe9907e5..419af1d02f83b 100644
--- a/packages/kbn-telemetry-tools/tsconfig.json
+++ b/packages/kbn-telemetry-tools/tsconfig.json
@@ -6,7 +6,8 @@
"declaration": true,
"declarationMap": true,
"sourceMap": true,
- "sourceRoot": "../../../../packages/kbn-telemetry-tools/src"
+ "sourceRoot": "../../../../packages/kbn-telemetry-tools/src",
+ "isolatedModules": true
},
"include": [
"src/**/*",
diff --git a/src/plugins/kibana_usage_collection/tsconfig.json b/src/plugins/kibana_usage_collection/tsconfig.json
index d664d936f6667..ee07dfe589e4a 100644
--- a/src/plugins/kibana_usage_collection/tsconfig.json
+++ b/src/plugins/kibana_usage_collection/tsconfig.json
@@ -5,7 +5,8 @@
"outDir": "./target/types",
"emitDeclarationOnly": true,
"declaration": true,
- "declarationMap": true
+ "declarationMap": true,
+ "isolatedModules": true
},
"include": [
"common/*",
diff --git a/src/plugins/telemetry/common/telemetry_config/index.ts b/src/plugins/telemetry/common/telemetry_config/index.ts
index 84b6486f35b24..cc4ff102742d7 100644
--- a/src/plugins/telemetry/common/telemetry_config/index.ts
+++ b/src/plugins/telemetry/common/telemetry_config/index.ts
@@ -9,7 +9,5 @@
export { getTelemetryOptIn } from './get_telemetry_opt_in';
export { getTelemetrySendUsageFrom } from './get_telemetry_send_usage_from';
export { getTelemetryAllowChangingOptInStatus } from './get_telemetry_allow_changing_opt_in_status';
-export {
- getTelemetryFailureDetails,
- TelemetryFailureDetails,
-} from './get_telemetry_failure_details';
+export { getTelemetryFailureDetails } from './get_telemetry_failure_details';
+export type { TelemetryFailureDetails } from './get_telemetry_failure_details';
diff --git a/src/plugins/telemetry/public/index.ts b/src/plugins/telemetry/public/index.ts
index 6cca9bdf881dd..47ba7828eaec2 100644
--- a/src/plugins/telemetry/public/index.ts
+++ b/src/plugins/telemetry/public/index.ts
@@ -8,7 +8,7 @@
import { PluginInitializerContext } from 'kibana/public';
import { TelemetryPlugin, TelemetryPluginConfig } from './plugin';
-export { TelemetryPluginStart, TelemetryPluginSetup } from './plugin';
+export type { TelemetryPluginStart, TelemetryPluginSetup } from './plugin';
export function plugin(initializerContext: PluginInitializerContext) {
return new TelemetryPlugin(initializerContext);
diff --git a/src/plugins/telemetry/server/index.ts b/src/plugins/telemetry/server/index.ts
index debdf7515cd58..1c335426ffd03 100644
--- a/src/plugins/telemetry/server/index.ts
+++ b/src/plugins/telemetry/server/index.ts
@@ -13,7 +13,7 @@ import { configSchema, TelemetryConfigType } from './config';
export { FetcherTask } from './fetcher';
export { handleOldSettings } from './handle_old_settings';
-export { TelemetryPluginSetup, TelemetryPluginStart } from './plugin';
+export type { TelemetryPluginSetup, TelemetryPluginStart } from './plugin';
export const config: PluginConfigDescriptor = {
schema: configSchema,
@@ -34,9 +34,12 @@ export { constants };
export {
getClusterUuids,
getLocalStats,
- TelemetryLocalStats,
DATA_TELEMETRY_ID,
+ buildDataTelemetryPayload,
+} from './telemetry_collection';
+
+export type {
+ TelemetryLocalStats,
DataTelemetryIndex,
DataTelemetryPayload,
- buildDataTelemetryPayload,
} from './telemetry_collection';
diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts
index def1131dfb1a3..c93b7e872924b 100644
--- a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts
+++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts
@@ -7,10 +7,5 @@
*/
export { DATA_TELEMETRY_ID } from './constants';
-
-export {
- getDataTelemetry,
- buildDataTelemetryPayload,
- DataTelemetryPayload,
- DataTelemetryIndex,
-} from './get_data_telemetry';
+export { getDataTelemetry, buildDataTelemetryPayload } from './get_data_telemetry';
+export type { DataTelemetryPayload, DataTelemetryIndex } from './get_data_telemetry';
diff --git a/src/plugins/telemetry/server/telemetry_collection/index.ts b/src/plugins/telemetry/server/telemetry_collection/index.ts
index 55f9c7f0e624c..151e89a11a192 100644
--- a/src/plugins/telemetry/server/telemetry_collection/index.ts
+++ b/src/plugins/telemetry/server/telemetry_collection/index.ts
@@ -6,12 +6,9 @@
* Side Public License, v 1.
*/
-export {
- DATA_TELEMETRY_ID,
- DataTelemetryIndex,
- DataTelemetryPayload,
- buildDataTelemetryPayload,
-} from './get_data_telemetry';
-export { getLocalStats, TelemetryLocalStats } from './get_local_stats';
+export { DATA_TELEMETRY_ID, buildDataTelemetryPayload } from './get_data_telemetry';
+export type { DataTelemetryIndex, DataTelemetryPayload } from './get_data_telemetry';
+export { getLocalStats } from './get_local_stats';
+export type { TelemetryLocalStats } from './get_local_stats';
export { getClusterUuids } from './get_cluster_stats';
export { registerCollection } from './register_collection';
diff --git a/src/plugins/telemetry/server/telemetry_repository/index.ts b/src/plugins/telemetry/server/telemetry_repository/index.ts
index 4e3f046f7611f..594b53259a65f 100644
--- a/src/plugins/telemetry/server/telemetry_repository/index.ts
+++ b/src/plugins/telemetry/server/telemetry_repository/index.ts
@@ -8,7 +8,7 @@
export { getTelemetrySavedObject } from './get_telemetry_saved_object';
export { updateTelemetrySavedObject } from './update_telemetry_saved_object';
-export {
+export type {
TelemetrySavedObject,
TelemetrySavedObjectAttributes,
} from '../../common/telemetry_config/types';
diff --git a/src/plugins/telemetry/tsconfig.json b/src/plugins/telemetry/tsconfig.json
index bdced01d9eb6f..6629e479906c9 100644
--- a/src/plugins/telemetry/tsconfig.json
+++ b/src/plugins/telemetry/tsconfig.json
@@ -5,7 +5,8 @@
"outDir": "./target/types",
"emitDeclarationOnly": true,
"declaration": true,
- "declarationMap": true
+ "declarationMap": true,
+ "isolatedModules": true
},
"include": [
"public/**/**/*",
diff --git a/src/plugins/telemetry_collection_manager/server/index.ts b/src/plugins/telemetry_collection_manager/server/index.ts
index 77077b73cf8ad..c0cd124a132c0 100644
--- a/src/plugins/telemetry_collection_manager/server/index.ts
+++ b/src/plugins/telemetry_collection_manager/server/index.ts
@@ -16,7 +16,7 @@ export function plugin(initializerContext: PluginInitializerContext) {
return new TelemetryCollectionManagerPlugin(initializerContext);
}
-export {
+export type {
TelemetryCollectionManagerPluginSetup,
TelemetryCollectionManagerPluginStart,
StatsCollectionConfig,
diff --git a/src/plugins/telemetry_collection_manager/tsconfig.json b/src/plugins/telemetry_collection_manager/tsconfig.json
index 1bba81769f0dd..1329979860603 100644
--- a/src/plugins/telemetry_collection_manager/tsconfig.json
+++ b/src/plugins/telemetry_collection_manager/tsconfig.json
@@ -5,7 +5,8 @@
"outDir": "./target/types",
"emitDeclarationOnly": true,
"declaration": true,
- "declarationMap": true
+ "declarationMap": true,
+ "isolatedModules": true
},
"include": [
"server/**/*",
diff --git a/src/plugins/telemetry_management_section/public/index.ts b/src/plugins/telemetry_management_section/public/index.ts
index 28b04418f512d..db6ea17556ed3 100644
--- a/src/plugins/telemetry_management_section/public/index.ts
+++ b/src/plugins/telemetry_management_section/public/index.ts
@@ -10,7 +10,7 @@ import { TelemetryManagementSectionPlugin } from './plugin';
export { OptInExampleFlyout } from './components';
-export { TelemetryManagementSectionPluginSetup } from './plugin';
+export type { TelemetryManagementSectionPluginSetup } from './plugin';
export function plugin() {
return new TelemetryManagementSectionPlugin();
}
diff --git a/src/plugins/telemetry_management_section/tsconfig.json b/src/plugins/telemetry_management_section/tsconfig.json
index 48e40814b8570..2daee868ac200 100644
--- a/src/plugins/telemetry_management_section/tsconfig.json
+++ b/src/plugins/telemetry_management_section/tsconfig.json
@@ -5,7 +5,8 @@
"outDir": "./target/types",
"emitDeclarationOnly": true,
"declaration": true,
- "declarationMap": true
+ "declarationMap": true,
+ "isolatedModules": true
},
"include": [
"public/**/*",
diff --git a/src/plugins/usage_collection/public/index.ts b/src/plugins/usage_collection/public/index.ts
index b9e0e0a8985b1..9b009b1d9e264 100644
--- a/src/plugins/usage_collection/public/index.ts
+++ b/src/plugins/usage_collection/public/index.ts
@@ -10,7 +10,7 @@ import { PluginInitializerContext } from '../../../core/public';
import { UsageCollectionPlugin } from './plugin';
export { METRIC_TYPE } from '@kbn/analytics';
-export { UsageCollectionSetup, UsageCollectionStart } from './plugin';
+export type { UsageCollectionSetup, UsageCollectionStart } from './plugin';
export { TrackApplicationView } from './components';
export function plugin(initializerContext: PluginInitializerContext) {
diff --git a/src/plugins/usage_collection/server/collector/index.ts b/src/plugins/usage_collection/server/collector/index.ts
index 5f48f9fb93813..d5e0d95659e58 100644
--- a/src/plugins/usage_collection/server/collector/index.ts
+++ b/src/plugins/usage_collection/server/collector/index.ts
@@ -6,9 +6,10 @@
* Side Public License, v 1.
*/
-export { CollectorSet, CollectorSetPublic } from './collector_set';
-export {
- Collector,
+export { CollectorSet } from './collector_set';
+export type { CollectorSetPublic } from './collector_set';
+export { Collector } from './collector';
+export type {
AllowedSchemaTypes,
AllowedSchemaNumberTypes,
SchemaField,
@@ -16,4 +17,5 @@ export {
CollectorOptions,
CollectorFetchContext,
} from './collector';
-export { UsageCollector, UsageCollectorOptions } from './usage_collector';
+export { UsageCollector } from './usage_collector';
+export type { UsageCollectorOptions } from './usage_collector';
diff --git a/src/plugins/usage_collection/server/index.ts b/src/plugins/usage_collection/server/index.ts
index dfc9d19b69646..dd9e6644a827d 100644
--- a/src/plugins/usage_collection/server/index.ts
+++ b/src/plugins/usage_collection/server/index.ts
@@ -9,17 +9,16 @@
import { PluginInitializerContext } from 'src/core/server';
import { UsageCollectionPlugin } from './plugin';
-export {
+export { Collector } from './collector';
+export type {
AllowedSchemaTypes,
MakeSchemaFrom,
SchemaField,
CollectorOptions,
UsageCollectorOptions,
- Collector,
CollectorFetchContext,
} from './collector';
-
-export { UsageCollectionSetup } from './plugin';
+export type { UsageCollectionSetup } from './plugin';
export { config } from './config';
export const plugin = (initializerContext: PluginInitializerContext) =>
new UsageCollectionPlugin(initializerContext);
diff --git a/src/plugins/usage_collection/server/usage_collection.mock.ts b/src/plugins/usage_collection/server/usage_collection.mock.ts
index 1a60d84e7948c..7e3f4273bbea8 100644
--- a/src/plugins/usage_collection/server/usage_collection.mock.ts
+++ b/src/plugins/usage_collection/server/usage_collection.mock.ts
@@ -16,7 +16,8 @@ import {
import { CollectorOptions, Collector, UsageCollector } from './collector';
import { UsageCollectionSetup, CollectorFetchContext } from './index';
-export { CollectorOptions, Collector };
+export type { CollectorOptions };
+export { Collector };
const logger = loggingSystemMock.createLogger();
diff --git a/src/plugins/usage_collection/tsconfig.json b/src/plugins/usage_collection/tsconfig.json
index 96b2c4d37e17c..68a0853994e80 100644
--- a/src/plugins/usage_collection/tsconfig.json
+++ b/src/plugins/usage_collection/tsconfig.json
@@ -5,7 +5,8 @@
"outDir": "./target/types",
"emitDeclarationOnly": true,
"declaration": true,
- "declarationMap": true
+ "declarationMap": true,
+ "isolatedModules": true
},
"include": [
"public/**/*",
diff --git a/x-pack/plugins/telemetry_collection_xpack/server/index.ts b/x-pack/plugins/telemetry_collection_xpack/server/index.ts
index d924882e17fbd..aab1bdb58fe59 100644
--- a/x-pack/plugins/telemetry_collection_xpack/server/index.ts
+++ b/x-pack/plugins/telemetry_collection_xpack/server/index.ts
@@ -7,7 +7,7 @@
import { TelemetryCollectionXpackPlugin } from './plugin';
-export { ESLicense } from './telemetry_collection';
+export type { ESLicense } from './telemetry_collection';
// This exports static code and TypeScript types,
// as well as, Kibana Platform `plugin()` initializer.
diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/index.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/index.ts
index 4599b068b9b38..c1a11caf44f24 100644
--- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/index.ts
+++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/index.ts
@@ -5,5 +5,5 @@
* 2.0.
*/
-export { ESLicense } from './get_license';
+export type { ESLicense } from './get_license';
export { getStatsWithXpack } from './get_stats_with_xpack';
diff --git a/x-pack/plugins/telemetry_collection_xpack/tsconfig.json b/x-pack/plugins/telemetry_collection_xpack/tsconfig.json
index 476f5926f757a..1221200c7548c 100644
--- a/x-pack/plugins/telemetry_collection_xpack/tsconfig.json
+++ b/x-pack/plugins/telemetry_collection_xpack/tsconfig.json
@@ -5,7 +5,8 @@
"outDir": "./target/types",
"emitDeclarationOnly": true,
"declaration": true,
- "declarationMap": true
+ "declarationMap": true,
+ "isolatedModules": true
},
"include": [
"common/**/*",
From 5ae94462603fc800fd2f5d0cee105f12d3794098 Mon Sep 17 00:00:00 2001
From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Date: Thu, 8 Apr 2021 09:16:58 -0400
Subject: [PATCH 09/22] [ML] Fix switches positioning on the Transform and DFA
wizards (#96535) (#96549)
* [ML] fix edit runtime mapping switch positioning
* [ML] fix transform wizard switches
Co-authored-by: Dima Arnautov
---
.../components/runtime_mappings/runtime_mappings.tsx | 2 +-
.../advanced_runtime_mappings_settings.tsx | 2 +-
.../components/step_define/step_define_form.tsx | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
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/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_settings/advanced_runtime_mappings_settings.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_settings/advanced_runtime_mappings_settings.tsx
index 277226c81c925..7965db99b335b 100644
--- a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_settings/advanced_runtime_mappings_settings.tsx
+++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_settings/advanced_runtime_mappings_settings.tsx
@@ -91,7 +91,7 @@ export const AdvancedRuntimeMappingsSettings: FC = (props) =
defaultMessage: 'Runtime mappings',
})}
>
-
+
{runtimeMappings !== undefined && Object.keys(runtimeMappings).length > 0 ? (
= React.memo((props) => {
}
>
<>
-
+
{/* Flex Column #1: Search Bar / Advanced Search Editor */}
{searchItems.savedSearch === undefined && (
From d972b8e50bb80cc38dc328c3bc3fd5569f257a73 Mon Sep 17 00:00:00 2001
From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Date: Thu, 8 Apr 2021 09:52:29 -0400
Subject: [PATCH 10/22] [Alerting] Update feature privilege display names
(#96083) (#96554)
* Updating feature display names
* Updating feature display names
Co-authored-by: ymao1
---
x-pack/examples/alerting_example/server/plugin.ts | 2 +-
x-pack/plugins/stack_alerts/server/feature.ts | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
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
Date: Thu, 8 Apr 2021 15:57:04 +0200
Subject: [PATCH 11/22] [7.x] [APM] Extract server type utils to package
(#96349) (#96552)
---
package.json | 2 +
packages/kbn-io-ts-utils/jest.config.js | 13 +
packages/kbn-io-ts-utils/package.json | 13 +
packages/kbn-io-ts-utils/src/index.ts | 11 +
.../src}/json_rt/index.test.ts | 9 +-
.../kbn-io-ts-utils/src}/json_rt/index.ts | 5 +-
.../src/merge_rt}/index.test.ts | 25 +-
.../kbn-io-ts-utils/src/merge_rt}/index.ts | 37 +-
.../src}/strict_keys_rt/index.test.ts | 28 +-
.../src}/strict_keys_rt/index.ts | 78 +--
packages/kbn-io-ts-utils/tsconfig.json | 19 +
.../kbn-server-route-repository/README.md | 7 +
.../jest.config.js | 13 +
.../kbn-server-route-repository/package.json | 16 +
.../src/create_server_route_factory.ts | 38 ++
.../src/create_server_route_repository.ts | 39 ++
.../src/decode_request_params.test.ts | 122 +++++
.../src/decode_request_params.ts | 43 ++
.../src/format_request.ts | 20 +
.../kbn-server-route-repository/src/index.ts | 24 +
.../src/parse_endpoint.ts | 22 +
.../src/route_validation_object.ts | 20 +
.../src/test_types.ts | 238 ++++++++
.../src/typings.ts | 192 +++++++
.../kbn-server-route-repository/tsconfig.json | 20 +
.../apm/common/latency_aggregation_types.ts | 6 +-
.../runtime_types/iso_to_epoch_rt/index.ts | 5 +-
.../link_preview.test.tsx | 9 +-
.../CustomizeUI/CustomLink/index.test.tsx | 4 +-
.../service_overview.test.tsx | 25 +-
...pm_observability_overview_fetchers.test.ts | 6 +-
.../apm/public/services/rest/callApmApiSpy.ts | 24 +
.../public/services/rest/createCallApmApi.ts | 83 ++-
x-pack/plugins/apm/server/index.ts | 6 +-
.../create_es_client/call_async_with_debug.ts | 2 +-
.../create_internal_es_client/index.ts | 11 +-
.../server/lib/helpers/setup_request.test.ts | 127 ++---
.../apm/server/lib/helpers/setup_request.ts | 33 +-
.../create_static_index_pattern.test.ts | 36 +-
.../create_static_index_pattern.ts | 8 +-
.../get_apm_index_pattern_title.ts | 8 +-
.../get_dynamic_index_pattern.ts | 12 +-
.../settings/apm_indices/get_apm_indices.ts | 9 +-
x-pack/plugins/apm/server/plugin.ts | 100 ++--
.../apm/server/routes/alerts/chart_preview.ts | 40 +-
.../plugins/apm/server/routes/correlations.ts | 47 +-
.../server/routes/create_api/index.test.ts | 368 -------------
.../apm/server/routes/create_api/index.ts | 185 -------
.../apm/server/routes/create_apm_api.ts | 230 --------
.../server/routes/create_apm_server_route.ts | 13 +
.../create_apm_server_route_repository.ts | 15 +
.../plugins/apm/server/routes/create_route.ts | 29 -
.../plugins/apm/server/routes/environments.ts | 16 +-
x-pack/plugins/apm/server/routes/errors.ts | 35 +-
.../get_global_apm_server_route_repository.ts | 82 +++
.../apm/server/routes/index_pattern.ts | 48 +-
x-pack/plugins/apm/server/routes/metrics.ts | 15 +-
.../server/routes/observability_overview.ts | 22 +-
.../routes/register_routes/index.test.ts | 507 ++++++++++++++++++
.../server/routes/register_routes/index.ts | 143 +++++
.../plugins/apm/server/routes/rum_client.ts | 122 +++--
.../plugins/apm/server/routes/service_map.ts | 31 +-
.../apm/server/routes/service_nodes.ts | 15 +-
x-pack/plugins/apm/server/routes/services.ts | 214 +++++---
.../routes/settings/agent_configuration.ts | 99 ++--
.../routes/settings/anomaly_detection.ts | 35 +-
.../apm/server/routes/settings/apm_indices.ts | 28 +-
.../apm/server/routes/settings/custom_link.ts | 70 ++-
x-pack/plugins/apm/server/routes/traces.ts | 37 +-
.../plugins/apm/server/routes/transactions.ts | 106 ++--
x-pack/plugins/apm/server/routes/typings.ts | 188 ++-----
x-pack/plugins/apm/server/types.ts | 164 ++++++
.../common/apm_api_supertest.ts | 19 +-
.../tests/inspect/inspect.ts | 1 -
.../instances_primary_statistics.ts | 7 +-
yarn.lock | 8 +
76 files changed, 2822 insertions(+), 1685 deletions(-)
create mode 100644 packages/kbn-io-ts-utils/jest.config.js
create mode 100644 packages/kbn-io-ts-utils/package.json
create mode 100644 packages/kbn-io-ts-utils/src/index.ts
rename {x-pack/plugins/apm/common/runtime_types => packages/kbn-io-ts-utils/src}/json_rt/index.test.ts (85%)
rename {x-pack/plugins/apm/common/runtime_types => packages/kbn-io-ts-utils/src}/json_rt/index.ts (74%)
rename {x-pack/plugins/apm/common/runtime_types/merge => packages/kbn-io-ts-utils/src/merge_rt}/index.test.ts (66%)
rename {x-pack/plugins/apm/common/runtime_types/merge => packages/kbn-io-ts-utils/src/merge_rt}/index.ts (62%)
rename {x-pack/plugins/apm/common/runtime_types => packages/kbn-io-ts-utils/src}/strict_keys_rt/index.test.ts (77%)
rename {x-pack/plugins/apm/common/runtime_types => packages/kbn-io-ts-utils/src}/strict_keys_rt/index.ts (66%)
create mode 100644 packages/kbn-io-ts-utils/tsconfig.json
create mode 100644 packages/kbn-server-route-repository/README.md
create mode 100644 packages/kbn-server-route-repository/jest.config.js
create mode 100644 packages/kbn-server-route-repository/package.json
create mode 100644 packages/kbn-server-route-repository/src/create_server_route_factory.ts
create mode 100644 packages/kbn-server-route-repository/src/create_server_route_repository.ts
create mode 100644 packages/kbn-server-route-repository/src/decode_request_params.test.ts
create mode 100644 packages/kbn-server-route-repository/src/decode_request_params.ts
create mode 100644 packages/kbn-server-route-repository/src/format_request.ts
create mode 100644 packages/kbn-server-route-repository/src/index.ts
create mode 100644 packages/kbn-server-route-repository/src/parse_endpoint.ts
create mode 100644 packages/kbn-server-route-repository/src/route_validation_object.ts
create mode 100644 packages/kbn-server-route-repository/src/test_types.ts
create mode 100644 packages/kbn-server-route-repository/src/typings.ts
create mode 100644 packages/kbn-server-route-repository/tsconfig.json
create mode 100644 x-pack/plugins/apm/public/services/rest/callApmApiSpy.ts
delete mode 100644 x-pack/plugins/apm/server/routes/create_api/index.test.ts
delete mode 100644 x-pack/plugins/apm/server/routes/create_api/index.ts
delete mode 100644 x-pack/plugins/apm/server/routes/create_apm_api.ts
create mode 100644 x-pack/plugins/apm/server/routes/create_apm_server_route.ts
create mode 100644 x-pack/plugins/apm/server/routes/create_apm_server_route_repository.ts
delete mode 100644 x-pack/plugins/apm/server/routes/create_route.ts
create mode 100644 x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts
create mode 100644 x-pack/plugins/apm/server/routes/register_routes/index.test.ts
create mode 100644 x-pack/plugins/apm/server/routes/register_routes/index.ts
create mode 100644 x-pack/plugins/apm/server/types.ts
diff --git a/package.json b/package.json
index 1aa32375411af..623247b4fd1a1 100644
--- a/package.json
+++ b/package.json
@@ -128,10 +128,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",
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/x-pack/plugins/apm/common/latency_aggregation_types.ts b/x-pack/plugins/apm/common/latency_aggregation_types.ts
index d9db58f223144..964d6f4ed1015 100644
--- a/x-pack/plugins/apm/common/latency_aggregation_types.ts
+++ b/x-pack/plugins/apm/common/latency_aggregation_types.ts
@@ -14,7 +14,7 @@ export enum LatencyAggregationType {
}
export const latencyAggregationTypeRt = t.union([
- t.literal('avg'),
- t.literal('p95'),
- t.literal('p99'),
+ t.literal(LatencyAggregationType.avg),
+ t.literal(LatencyAggregationType.p95),
+ t.literal(LatencyAggregationType.p99),
]);
diff --git a/x-pack/plugins/apm/common/runtime_types/iso_to_epoch_rt/index.ts b/x-pack/plugins/apm/common/runtime_types/iso_to_epoch_rt/index.ts
index 1a17f82a52141..970e39bc4f86f 100644
--- a/x-pack/plugins/apm/common/runtime_types/iso_to_epoch_rt/index.ts
+++ b/x-pack/plugins/apm/common/runtime_types/iso_to_epoch_rt/index.ts
@@ -21,8 +21,5 @@ export const isoToEpochRt = new t.Type(
? t.failure(input, context)
: t.success(epochDate);
}),
- (a) => {
- const d = new Date(a);
- return d.toISOString();
- }
+ (output) => new Date(output).toISOString()
);
diff --git a/x-pack/plugins/apm/public/components/app/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/test/apm_api_integration/common/apm_api_supertest.ts b/x-pack/test/apm_api_integration/common/apm_api_supertest.ts
index 542982778dfff..ed104a6fdf064 100644
--- a/x-pack/test/apm_api_integration/common/apm_api_supertest.ts
+++ b/x-pack/test/apm_api_integration/common/apm_api_supertest.ts
@@ -8,24 +8,25 @@
import { format } from 'url';
import supertest from 'supertest';
import request from 'superagent';
-import { MaybeParams } from '../../../plugins/apm/server/routes/typings';
import { parseEndpoint } from '../../../plugins/apm/common/apm_api/parse_endpoint';
-import { APMAPI } from '../../../plugins/apm/server/routes/create_apm_api';
-import type { APIReturnType } from '../../../plugins/apm/public/services/rest/createCallApmApi';
+import type {
+ APIReturnType,
+ APIEndpoint,
+ APIClientRequestParamsOf,
+} from '../../../plugins/apm/public/services/rest/createCallApmApi';
export function createApmApiSupertest(st: supertest.SuperTest) {
- return async (
+ return async (
options: {
- endpoint: TPath;
- } & MaybeParams
+ endpoint: TEndpoint;
+ } & APIClientRequestParamsOf & { params?: { query?: { _inspect?: boolean } } }
): Promise<{
status: number;
- body: APIReturnType;
+ body: APIReturnType;
}> => {
const { endpoint } = options;
- // @ts-expect-error
- const params = 'params' in options ? options.params : {};
+ const params = 'params' in options ? (options.params as Record) : {};
const { method, pathname } = parseEndpoint(endpoint, params?.path);
const url = format({ pathname, query: params?.query });
diff --git a/x-pack/test/apm_api_integration/tests/inspect/inspect.ts b/x-pack/test/apm_api_integration/tests/inspect/inspect.ts
index aae2e38e8ec8e..4f65808de820e 100644
--- a/x-pack/test/apm_api_integration/tests/inspect/inspect.ts
+++ b/x-pack/test/apm_api_integration/tests/inspect/inspect.ts
@@ -81,7 +81,6 @@ export default function customLinksTests({ getService }: FtrProviderContext) {
it('for agent configs', async () => {
const { status, body } = await supertestRead({
endpoint: 'GET /api/apm/settings/agent-configuration',
- // @ts-expect-error
params: {
query: {
_inspect: true,
diff --git a/x-pack/test/apm_api_integration/tests/service_overview/instances_primary_statistics.ts b/x-pack/test/apm_api_integration/tests/service_overview/instances_primary_statistics.ts
index aac92685a3c34..baa95eb56a126 100644
--- a/x-pack/test/apm_api_integration/tests/service_overview/instances_primary_statistics.ts
+++ b/x-pack/test/apm_api_integration/tests/service_overview/instances_primary_statistics.ts
@@ -13,6 +13,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context';
import archives from '../../common/fixtures/es_archiver/archives_metadata';
import { registry } from '../../common/registry';
import { createApmApiSupertest } from '../../common/apm_api_supertest';
+import { LatencyAggregationType } from '../../../../plugins/apm/common/latency_aggregation_types';
export default function ApiTest({ getService }: FtrProviderContext) {
const apmApiSupertest = createApmApiSupertest(getService('supertest'));
@@ -31,7 +32,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
params: {
path: { serviceName: 'opbeans-java' },
query: {
- latencyAggregationType: 'avg',
+ latencyAggregationType: LatencyAggregationType.avg,
start,
end,
transactionType: 'request',
@@ -61,7 +62,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
params: {
path: { serviceName: 'opbeans-java' },
query: {
- latencyAggregationType: 'avg',
+ latencyAggregationType: LatencyAggregationType.avg,
start,
end,
transactionType: 'request',
@@ -130,7 +131,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
params: {
path: { serviceName: 'opbeans-ruby' },
query: {
- latencyAggregationType: 'avg',
+ latencyAggregationType: LatencyAggregationType.avg,
start,
end,
transactionType: 'request',
diff --git a/yarn.lock b/yarn.lock
index 546fe439f56dd..e1118cf0b74fd 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2680,6 +2680,10 @@
version "0.0.0"
uid ""
+"@kbn/io-ts-utils@link:packages/kbn-io-ts-utils":
+ version "0.0.0"
+ uid ""
+
"@kbn/legacy-logging@link:packages/kbn-legacy-logging":
version "0.0.0"
uid ""
@@ -2712,6 +2716,10 @@
version "0.0.0"
uid ""
+"@kbn/server-route-repository@link:packages/kbn-server-route-repository":
+ version "0.0.0"
+ uid ""
+
"@kbn/std@link:packages/kbn-std":
version "0.0.0"
uid ""
From 92da416341fa8377da893c44e16740e4a5f4c1d5 Mon Sep 17 00:00:00 2001
From: Anton Dosov
Date: Thu, 8 Apr 2021 16:04:30 +0200
Subject: [PATCH 12/22] Don't trigger auto-refresh until previous refresh
completes (#93410) (#96547)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
...n-plugins-data-public.autorefreshdonefn.md | 11 +
.../kibana-plugin-plugins-data-public.md | 3 +
...a-public.waituntilnextsessioncompletes_.md | 25 +++
...ic.waituntilnextsessioncompletesoptions.md | 20 ++
...nextsessioncompletesoptions.waitforidle.md | 13 ++
.../public/application/dashboard_app.tsx | 24 +-
src/plugins/data/README.mdx | 159 ++++++++------
src/plugins/data/public/index.ts | 3 +
src/plugins/data/public/public.api.md | 47 ++--
.../data/public/query/timefilter/index.ts | 2 +-
.../timefilter/lib/auto_refresh_loop.test.ts | 205 ++++++++++++++++++
.../query/timefilter/lib/auto_refresh_loop.ts | 80 +++++++
.../query/timefilter/timefilter.test.ts | 45 +++-
.../public/query/timefilter/timefilter.ts | 30 +--
.../timefilter/timefilter_service.mock.ts | 2 +-
src/plugins/data/public/search/index.ts | 2 +
.../data/public/search/session/index.ts | 4 +
.../search/session/session_helpers.test.ts | 88 ++++++++
.../public/search/session/session_helpers.ts | 48 ++++
.../public/application/angular/discover.js | 26 ++-
src/plugins/expressions/public/loader.ts | 7 +-
.../public/embeddable/visualize_embeddable.ts | 8 +-
.../components/visualize_top_nav.tsx | 8 +-
.../lens/public/app_plugin/app.test.tsx | 6 +-
x-pack/plugins/lens/public/app_plugin/app.tsx | 15 +-
25 files changed, 755 insertions(+), 126 deletions(-)
create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.autorefreshdonefn.md
create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md
create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.md
create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.waitforidle.md
create mode 100644 src/plugins/data/public/query/timefilter/lib/auto_refresh_loop.test.ts
create mode 100644 src/plugins/data/public/query/timefilter/lib/auto_refresh_loop.ts
create mode 100644 src/plugins/data/public/search/session/session_helpers.test.ts
create mode 100644 src/plugins/data/public/search/session/session_helpers.ts
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.autorefreshdonefn.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.autorefreshdonefn.md
new file mode 100644
index 0000000000000..a5694ea2d1af9
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.autorefreshdonefn.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AutoRefreshDoneFn](./kibana-plugin-plugins-data-public.autorefreshdonefn.md)
+
+## AutoRefreshDoneFn type
+
+Signature:
+
+```typescript
+export declare type AutoRefreshDoneFn = () => void;
+```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md
index d2e7ef9db05e8..4429f45f55645 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md
@@ -47,6 +47,7 @@
| [getSearchParamsFromRequest(searchRequest, dependencies)](./kibana-plugin-plugins-data-public.getsearchparamsfromrequest.md) | |
| [getTime(indexPattern, timeRange, options)](./kibana-plugin-plugins-data-public.gettime.md) | |
| [plugin(initializerContext)](./kibana-plugin-plugins-data-public.plugin.md) | |
+| [waitUntilNextSessionCompletes$(sessionService, { waitForIdle })](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md) | Creates an observable that emits when next search session completes. This utility is helpful to use in the application to delay some tasks until next session completes. |
## Interfaces
@@ -92,6 +93,7 @@
| [SearchInterceptorDeps](./kibana-plugin-plugins-data-public.searchinterceptordeps.md) | |
| [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.md) | Provide info about current search session to be stored in the Search Session saved object |
| [SearchSourceFields](./kibana-plugin-plugins-data-public.searchsourcefields.md) | search source fields |
+| [WaitUntilNextSessionCompletesOptions](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.md) | Options for [waitUntilNextSessionCompletes$()](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md) |
## Variables
@@ -141,6 +143,7 @@
| [AggParam](./kibana-plugin-plugins-data-public.aggparam.md) | |
| [AggsStart](./kibana-plugin-plugins-data-public.aggsstart.md) | AggsStart represents the actual external contract as AggsCommonStart is only used internally. The difference is that AggsStart includes the typings for the registry with initialized agg types. |
| [AutocompleteStart](./kibana-plugin-plugins-data-public.autocompletestart.md) | \* |
+| [AutoRefreshDoneFn](./kibana-plugin-plugins-data-public.autorefreshdonefn.md) | |
| [CustomFilter](./kibana-plugin-plugins-data-public.customfilter.md) | |
| [EsaggsExpressionFunctionDefinition](./kibana-plugin-plugins-data-public.esaggsexpressionfunctiondefinition.md) | |
| [EsdslExpressionFunctionDefinition](./kibana-plugin-plugins-data-public.esdslexpressionfunctiondefinition.md) | |
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md
new file mode 100644
index 0000000000000..a4b294fb1decd
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md
@@ -0,0 +1,25 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [waitUntilNextSessionCompletes$](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md)
+
+## waitUntilNextSessionCompletes$() function
+
+Creates an observable that emits when next search session completes. This utility is helpful to use in the application to delay some tasks until next session completes.
+
+Signature:
+
+```typescript
+export declare function waitUntilNextSessionCompletes$(sessionService: ISessionService, { waitForIdle }?: WaitUntilNextSessionCompletesOptions): import("rxjs").Observable;
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| sessionService | ISessionService
| [ISessionService](./kibana-plugin-plugins-data-public.isessionservice.md) |
+| { waitForIdle } | WaitUntilNextSessionCompletesOptions
| |
+
+Returns:
+
+`import("rxjs").Observable`
+
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.md
new file mode 100644
index 0000000000000..d575722a22453
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.md
@@ -0,0 +1,20 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [WaitUntilNextSessionCompletesOptions](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.md)
+
+## WaitUntilNextSessionCompletesOptions interface
+
+Options for [waitUntilNextSessionCompletes$()](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletes_.md)
+
+Signature:
+
+```typescript
+export interface WaitUntilNextSessionCompletesOptions
+```
+
+## Properties
+
+| Property | Type | Description |
+| --- | --- | --- |
+| [waitForIdle](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.waitforidle.md) | number
| For how long to wait between session state transitions before considering that session completed |
+
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.waitforidle.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.waitforidle.md
new file mode 100644
index 0000000000000..60d3df7783852
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.waitforidle.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [WaitUntilNextSessionCompletesOptions](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.md) > [waitForIdle](./kibana-plugin-plugins-data-public.waituntilnextsessioncompletesoptions.waitforidle.md)
+
+## WaitUntilNextSessionCompletesOptions.waitForIdle property
+
+For how long to wait between session state transitions before considering that session completed
+
+Signature:
+
+```typescript
+waitForIdle?: number;
+```
diff --git a/src/plugins/dashboard/public/application/dashboard_app.tsx b/src/plugins/dashboard/public/application/dashboard_app.tsx
index 3d6f08f321977..e7e2ccfd46b9c 100644
--- a/src/plugins/dashboard/public/application/dashboard_app.tsx
+++ b/src/plugins/dashboard/public/application/dashboard_app.tsx
@@ -10,7 +10,7 @@ import { History } from 'history';
import { merge, Subject, Subscription } from 'rxjs';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
-import { debounceTime, tap } from 'rxjs/operators';
+import { debounceTime, finalize, switchMap, tap } from 'rxjs/operators';
import { useKibana } from '../../../kibana_react/public';
import { DashboardConstants } from '../dashboard_constants';
import { DashboardTopNav } from './top_nav/dashboard_top_nav';
@@ -30,7 +30,7 @@ import {
useSavedDashboard,
} from './hooks';
-import { IndexPattern } from '../services/data';
+import { IndexPattern, waitUntilNextSessionCompletes$ } from '../services/data';
import { EmbeddableRenderer } from '../services/embeddable';
import { DashboardContainerInput } from '.';
import { leaveConfirmStrings } from '../dashboard_strings';
@@ -209,14 +209,26 @@ export function DashboardApp({
);
subscriptions.add(
- merge(
- data.query.timefilter.timefilter.getAutoRefreshFetch$(),
- searchSessionIdQuery$
- ).subscribe(() => {
+ searchSessionIdQuery$.subscribe(() => {
triggerRefresh$.next({ force: true });
})
);
+ subscriptions.add(
+ data.query.timefilter.timefilter
+ .getAutoRefreshFetch$()
+ .pipe(
+ tap(() => {
+ triggerRefresh$.next({ force: true });
+ }),
+ switchMap((done) =>
+ // best way on a dashboard to estimate that panels are updated is to rely on search session service state
+ waitUntilNextSessionCompletes$(data.search.session).pipe(finalize(done))
+ )
+ )
+ .subscribe()
+ );
+
dashboardStateManager.registerChangeListener(() => {
setUnsavedChanges(dashboardStateManager.getIsDirty(data.query.timefilter.timefilter));
// we aren't checking dirty state because there are changes the container needs to know about
diff --git a/src/plugins/data/README.mdx b/src/plugins/data/README.mdx
index 60e74a3fa126c..30006e2b497bd 100644
--- a/src/plugins/data/README.mdx
+++ b/src/plugins/data/README.mdx
@@ -5,7 +5,7 @@ title: Data services
image: https://source.unsplash.com/400x175/?Search
summary: The data plugin contains services for searching, querying and filtering.
date: 2020-12-02
-tags: ['kibana','dev', 'contributor', 'api docs']
+tags: ['kibana', 'dev', 'contributor', 'api docs']
---
# data
@@ -149,7 +149,6 @@ Index patterns provide Rest-like HTTP CRUD+ API with the following endpoints:
- Remove a scripted field — `DELETE /api/index_patterns/index_pattern/{id}/scripted_field/{name}`
- Update a scripted field — `POST /api/index_patterns/index_pattern/{id}/scripted_field/{name}`
-
### Index Patterns API
Index Patterns REST API allows you to create, retrieve and delete index patterns. I also
@@ -212,11 +211,10 @@ The endpoint returns the created index pattern object.
```json
{
- "index_pattern": {}
+ "index_pattern": {}
}
```
-
#### Fetch an index pattern by ID
Retrieve an index pattern by its ID.
@@ -229,23 +227,22 @@ Returns an index pattern object.
```json
{
- "index_pattern": {
- "id": "...",
- "version": "...",
- "title": "...",
- "type": "...",
- "intervalName": "...",
- "timeFieldName": "...",
- "sourceFilters": [],
- "fields": {},
- "typeMeta": {},
- "fieldFormats": {},
- "fieldAttrs": {}
- }
+ "index_pattern": {
+ "id": "...",
+ "version": "...",
+ "title": "...",
+ "type": "...",
+ "intervalName": "...",
+ "timeFieldName": "...",
+ "sourceFilters": [],
+ "fields": {},
+ "typeMeta": {},
+ "fieldFormats": {},
+ "fieldAttrs": {}
+ }
}
```
-
#### Delete an index pattern by ID
Delete and index pattern by its ID.
@@ -256,21 +253,21 @@ DELETE /api/index_patterns/index_pattern/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Returns an '200 OK` response with empty body on success.
-
#### Partially update an index pattern by ID
Update part of an index pattern. Only provided fields will be updated on the
index pattern, missing fields will stay as they are persisted.
These fields can be update partially:
- - `title`
- - `timeFieldName`
- - `intervalName`
- - `fields` (optionally refresh fields)
- - `sourceFilters`
- - `fieldFormatMap`
- - `type`
- - `typeMeta`
+
+- `title`
+- `timeFieldName`
+- `intervalName`
+- `fields` (optionally refresh fields)
+- `sourceFilters`
+- `fieldFormatMap`
+- `type`
+- `typeMeta`
Update a title of an index pattern.
@@ -318,18 +315,14 @@ This endpoint returns the updated index pattern object.
```json
{
- "index_pattern": {
-
- }
+ "index_pattern": {}
}
```
-
### Fields API
Fields API allows to change field metadata, such as `count`, `customLabel`, and `format`.
-
#### Update fields
Update endpoint allows you to update fields presentation metadata, such as `count`,
@@ -383,13 +376,10 @@ This endpoint returns the updated index pattern object.
```json
{
- "index_pattern": {
-
- }
+ "index_pattern": {}
}
```
-
### Scripted Fields API
Scripted Fields API provides CRUD API for scripted fields of an index pattern.
@@ -487,7 +477,7 @@ Returns the field object.
```json
{
- "field": {}
+ "field": {}
}
```
@@ -529,47 +519,86 @@ POST /api/index_patterns/index_pattern/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/scri
}
```
-
## Query
The query service is responsible for managing the configuration of a search query (`QueryState`): filters, time range, query string, and settings such as the auto refresh behavior and saved queries.
It contains sub-services for each of those configurations:
- - `data.query.filterManager` - Manages the `filters` component of a `QueryState`. The global filter state (filters that are persisted between applications) are owned by this service.
- - `data.query.timefilter` - Responsible for the time range filter and the auto refresh behavior settings.
- - `data.query.queryString` - Responsible for the query string and query language settings.
- - `data.query.savedQueries` - Responsible for persisting a `QueryState` into a `SavedObject`, so it can be restored and used by other applications.
- Any changes to the `QueryState` are published on the `data.query.state$`, which is useful when wanting to persist global state or run a search upon data changes.
+- `data.query.filterManager` - Manages the `filters` component of a `QueryState`. The global filter state (filters that are persisted between applications) are owned by this service.
+- `data.query.timefilter` - Responsible for the time range filter and the auto refresh behavior settings.
+- `data.query.queryString` - Responsible for the query string and query language settings.
+- `data.query.savedQueries` - Responsible for persisting a `QueryState` into a `SavedObject`, so it can be restored and used by other applications.
- A simple use case is:
+Any changes to the `QueryState` are published on the `data.query.state$`, which is useful when wanting to persist global state or run a search upon data changes.
- ```.ts
- function searchOnChange(indexPattern: IndexPattern, aggConfigs: AggConfigs) {
- data.query.state$.subscribe(() => {
+A simple use case is:
- // Constuct the query portion of the search request
- const query = data.query.getEsQuery(indexPattern);
+```.ts
+function searchOnChange(indexPattern: IndexPattern, aggConfigs: AggConfigs) {
+ data.query.state$.subscribe(() => {
+
+ // Constuct the query portion of the search request
+ const query = data.query.getEsQuery(indexPattern);
+
+ // Construct a request
+ const request = {
+ params: {
+ index: indexPattern.title,
+ body: {
+ aggs: aggConfigs.toDsl(),
+ query,
+ },
+ },
+ };
+
+ // Search with the `data.query` config
+ const search$ = data.search.search(request);
+
+ ...
+ });
+}
- // Construct a request
- const request = {
- params: {
- index: indexPattern.title,
- body: {
- aggs: aggConfigs.toDsl(),
- query,
- },
- },
- };
+```
- // Search with the `data.query` config
- const search$ = data.search.search(request);
+### Timefilter
- ...
- });
- }
+`data.query.timefilter` is responsible for the time range filter and the auto refresh behavior settings.
+
+#### Autorefresh
- ```
+Timefilter provides an API for setting and getting current auto refresh state:
+
+```ts
+const { pause, value } = data.query.timefilter.timefilter.getRefreshInterval();
+
+data.query.timefilter.timefilter.setRefreshInterval({ pause: false, value: 5000 }); // start auto refresh with 5 seconds interval
+```
+
+Timefilter API also provides an `autoRefreshFetch$` observables that apps should use to get notified
+when it is time to refresh data because of auto refresh.
+This API expects apps to confirm when they are done with reloading the data.
+The confirmation mechanism is needed to prevent excessive queue of fetches.
+
+```
+import { refetchData } from '../my-app'
+
+const autoRefreshFetch$ = data.query.timefilter.timefilter.getAutoRefreshFetch$()
+autoRefreshFetch$.subscribe((done) => {
+ try {
+ await refetchData();
+ } finally {
+ // confirm that data fetching was finished
+ done();
+ }
+})
+
+function unmount() {
+ // don't forget to unsubscribe when leaving the app
+ autoRefreshFetch$.unsubscribe()
+}
+
+```
## Search
diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts
index c47cd6cd9740d..d2683e248b7bf 100644
--- a/src/plugins/data/public/index.ts
+++ b/src/plugins/data/public/index.ts
@@ -388,6 +388,8 @@ export {
PainlessError,
noSearchSessionStorageCapabilityMessage,
SEARCH_SESSIONS_MANAGEMENT_ID,
+ waitUntilNextSessionCompletes$,
+ WaitUntilNextSessionCompletesOptions,
} from './search';
export type {
@@ -467,6 +469,7 @@ export {
TimeHistoryContract,
QueryStateChange,
QueryStart,
+ AutoRefreshDoneFn,
} from './query';
export { AggsStart } from './search/aggs';
diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md
index 415d91f0bcdca..f6a7d032c7017 100644
--- a/src/plugins/data/public/public.api.md
+++ b/src/plugins/data/public/public.api.md
@@ -504,6 +504,11 @@ export interface ApplyGlobalFilterActionContext {
// @public (undocumented)
export type AutocompleteStart = ReturnType;
+// Warning: (ae-missing-release-tag) "AutoRefreshDoneFn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+//
+// @public (undocumented)
+export type AutoRefreshDoneFn = () => void;
+
// Warning: (ae-forgotten-export) The symbol "DateFormat" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "DateNanosFormat" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "baseFormattersPublic" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -2655,6 +2660,18 @@ export const UI_SETTINGS: {
readonly AUTOCOMPLETE_USE_TIMERANGE: "autocomplete:useTimeRange";
};
+// Warning: (ae-missing-release-tag) "waitUntilNextSessionCompletes$" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+//
+// @public
+export function waitUntilNextSessionCompletes$(sessionService: ISessionService, { waitForIdle }?: WaitUntilNextSessionCompletesOptions): import("rxjs").Observable;
+
+// Warning: (ae-missing-release-tag) "WaitUntilNextSessionCompletesOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+//
+// @public
+export interface WaitUntilNextSessionCompletesOptions {
+ waitForIdle?: number;
+}
+
// Warnings were encountered during analysis:
//
@@ -2702,21 +2719,21 @@ export const UI_SETTINGS: {
// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:404:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:404:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:404:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:404:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:406:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:407:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:416:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:417:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:419:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:424:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:427:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:428:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:431:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:406:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:406:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:406:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:406:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:408:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:409:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:419:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:420:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:421:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:425:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:426:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:429:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:430:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:433:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/query/state_sync/connect_to_query_state.ts:34:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/search/session/session_service.ts:56:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts
diff --git a/src/plugins/data/public/query/timefilter/index.ts b/src/plugins/data/public/query/timefilter/index.ts
index 83e897824d86c..3dfd4e0fe514f 100644
--- a/src/plugins/data/public/query/timefilter/index.ts
+++ b/src/plugins/data/public/query/timefilter/index.ts
@@ -9,7 +9,7 @@
export { TimefilterService, TimefilterSetup } from './timefilter_service';
export * from './types';
-export { Timefilter, TimefilterContract } from './timefilter';
+export { Timefilter, TimefilterContract, AutoRefreshDoneFn } from './timefilter';
export { TimeHistory, TimeHistoryContract } from './time_history';
export { changeTimeFilter, convertRangeFilterToTimeRangeString } from './lib/change_time_filter';
export { extractTimeFilter, extractTimeRange } from './lib/extract_time_filter';
diff --git a/src/plugins/data/public/query/timefilter/lib/auto_refresh_loop.test.ts b/src/plugins/data/public/query/timefilter/lib/auto_refresh_loop.test.ts
new file mode 100644
index 0000000000000..3c8b316c3b878
--- /dev/null
+++ b/src/plugins/data/public/query/timefilter/lib/auto_refresh_loop.test.ts
@@ -0,0 +1,205 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { createAutoRefreshLoop, AutoRefreshDoneFn } from './auto_refresh_loop';
+
+jest.useFakeTimers();
+
+test('triggers refresh with interval', () => {
+ const { loop$, start, stop } = createAutoRefreshLoop();
+
+ const fn = jest.fn((done) => done());
+ loop$.subscribe(fn);
+
+ jest.advanceTimersByTime(5000);
+ expect(fn).not.toBeCalled();
+
+ start(1000);
+
+ jest.advanceTimersByTime(1001);
+ expect(fn).toHaveBeenCalledTimes(1);
+
+ jest.advanceTimersByTime(1001);
+ expect(fn).toHaveBeenCalledTimes(2);
+
+ stop();
+
+ jest.advanceTimersByTime(5000);
+ expect(fn).toHaveBeenCalledTimes(2);
+});
+
+test('waits for done() to be called', () => {
+ const { loop$, start } = createAutoRefreshLoop();
+
+ let done!: AutoRefreshDoneFn;
+ const fn = jest.fn((_done) => {
+ done = _done;
+ });
+ loop$.subscribe(fn);
+ start(1000);
+
+ jest.advanceTimersByTime(1001);
+ expect(fn).toHaveBeenCalledTimes(1);
+ expect(done).toBeInstanceOf(Function);
+
+ jest.advanceTimersByTime(1001);
+ expect(fn).toHaveBeenCalledTimes(1);
+
+ done();
+
+ jest.advanceTimersByTime(500);
+ expect(fn).toHaveBeenCalledTimes(1);
+ jest.advanceTimersByTime(501);
+ expect(fn).toHaveBeenCalledTimes(2);
+});
+
+test('waits for done() from multiple subscribers to be called', () => {
+ const { loop$, start } = createAutoRefreshLoop();
+
+ let done1!: AutoRefreshDoneFn;
+ const fn1 = jest.fn((_done) => {
+ done1 = _done;
+ });
+ loop$.subscribe(fn1);
+
+ let done2!: AutoRefreshDoneFn;
+ const fn2 = jest.fn((_done) => {
+ done2 = _done;
+ });
+ loop$.subscribe(fn2);
+
+ start(1000);
+
+ jest.advanceTimersByTime(1001);
+ expect(fn1).toHaveBeenCalledTimes(1);
+ expect(done1).toBeInstanceOf(Function);
+
+ jest.advanceTimersByTime(1001);
+ expect(fn1).toHaveBeenCalledTimes(1);
+
+ done1();
+
+ jest.advanceTimersByTime(500);
+ expect(fn1).toHaveBeenCalledTimes(1);
+ jest.advanceTimersByTime(501);
+ expect(fn1).toHaveBeenCalledTimes(1);
+
+ done2();
+
+ jest.advanceTimersByTime(500);
+ expect(fn1).toHaveBeenCalledTimes(1);
+ jest.advanceTimersByTime(501);
+ expect(fn1).toHaveBeenCalledTimes(2);
+});
+
+test('unsubscribe() resets the state', () => {
+ const { loop$, start } = createAutoRefreshLoop();
+
+ let done1!: AutoRefreshDoneFn;
+ const fn1 = jest.fn((_done) => {
+ done1 = _done;
+ });
+ loop$.subscribe(fn1);
+
+ const fn2 = jest.fn();
+ const sub2 = loop$.subscribe(fn2);
+
+ start(1000);
+
+ jest.advanceTimersByTime(1001);
+ expect(fn1).toHaveBeenCalledTimes(1);
+ expect(done1).toBeInstanceOf(Function);
+
+ jest.advanceTimersByTime(1001);
+ expect(fn1).toHaveBeenCalledTimes(1);
+
+ done1();
+
+ jest.advanceTimersByTime(500);
+ expect(fn1).toHaveBeenCalledTimes(1);
+ jest.advanceTimersByTime(501);
+ expect(fn1).toHaveBeenCalledTimes(1);
+
+ sub2.unsubscribe();
+
+ jest.advanceTimersByTime(500);
+ expect(fn1).toHaveBeenCalledTimes(1);
+ jest.advanceTimersByTime(501);
+ expect(fn1).toHaveBeenCalledTimes(2);
+});
+
+test('calling done() twice is ignored', () => {
+ const { loop$, start } = createAutoRefreshLoop();
+
+ let done1!: AutoRefreshDoneFn;
+ const fn1 = jest.fn((_done) => {
+ done1 = _done;
+ });
+ loop$.subscribe(fn1);
+
+ const fn2 = jest.fn();
+ loop$.subscribe(fn2);
+
+ start(1000);
+
+ jest.advanceTimersByTime(1001);
+ expect(fn1).toHaveBeenCalledTimes(1);
+ expect(done1).toBeInstanceOf(Function);
+
+ jest.advanceTimersByTime(1001);
+ expect(fn1).toHaveBeenCalledTimes(1);
+
+ done1();
+
+ jest.advanceTimersByTime(500);
+ expect(fn1).toHaveBeenCalledTimes(1);
+ jest.advanceTimersByTime(501);
+ expect(fn1).toHaveBeenCalledTimes(1);
+
+ done1();
+
+ jest.advanceTimersByTime(500);
+ expect(fn1).toHaveBeenCalledTimes(1);
+ jest.advanceTimersByTime(501);
+ expect(fn1).toHaveBeenCalledTimes(1);
+});
+
+test('calling older done() is ignored', () => {
+ const { loop$, start } = createAutoRefreshLoop();
+
+ let done1!: AutoRefreshDoneFn;
+ const fn1 = jest.fn((_done) => {
+ // @ts-ignore
+ if (done1) return;
+ done1 = _done;
+ });
+ loop$.subscribe(fn1);
+
+ start(1000);
+
+ jest.advanceTimersByTime(1001);
+ expect(fn1).toHaveBeenCalledTimes(1);
+ expect(done1).toBeInstanceOf(Function);
+
+ jest.advanceTimersByTime(1001);
+ expect(fn1).toHaveBeenCalledTimes(1);
+
+ done1();
+
+ jest.advanceTimersByTime(500);
+ expect(fn1).toHaveBeenCalledTimes(1);
+ jest.advanceTimersByTime(501);
+ expect(fn1).toHaveBeenCalledTimes(2);
+
+ done1();
+
+ jest.advanceTimersByTime(500);
+ expect(fn1).toHaveBeenCalledTimes(2);
+ jest.advanceTimersByTime(501);
+ expect(fn1).toHaveBeenCalledTimes(2);
+});
diff --git a/src/plugins/data/public/query/timefilter/lib/auto_refresh_loop.ts b/src/plugins/data/public/query/timefilter/lib/auto_refresh_loop.ts
new file mode 100644
index 0000000000000..1e213b36e1d8b
--- /dev/null
+++ b/src/plugins/data/public/query/timefilter/lib/auto_refresh_loop.ts
@@ -0,0 +1,80 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { defer, Subject } from 'rxjs';
+import { finalize, map } from 'rxjs/operators';
+import { once } from 'lodash';
+
+export type AutoRefreshDoneFn = () => void;
+
+/**
+ * Creates a loop for timepicker's auto refresh
+ * It has a "confirmation" mechanism:
+ * When auto refresh loop emits, it won't continue automatically,
+ * until each subscriber calls received `done` function.
+ *
+ * @internal
+ */
+export const createAutoRefreshLoop = () => {
+ let subscribersCount = 0;
+ const tick = new Subject();
+
+ let _timeoutHandle: number;
+ let _timeout: number = 0;
+
+ function start() {
+ stop();
+ if (_timeout === 0) return;
+ const timeoutHandle = window.setTimeout(() => {
+ let pendingDoneCount = subscribersCount;
+ const done = () => {
+ if (timeoutHandle !== _timeoutHandle) return;
+
+ pendingDoneCount--;
+ if (pendingDoneCount === 0) {
+ start();
+ }
+ };
+ tick.next(done);
+ }, _timeout);
+
+ _timeoutHandle = timeoutHandle;
+ }
+
+ function stop() {
+ window.clearTimeout(_timeoutHandle);
+ _timeoutHandle = -1;
+ }
+
+ return {
+ stop: () => {
+ _timeout = 0;
+ stop();
+ },
+ start: (timeout: number) => {
+ _timeout = timeout;
+ if (subscribersCount > 0) {
+ start();
+ }
+ },
+ loop$: defer(() => {
+ subscribersCount++;
+ start(); // restart the loop on a new subscriber
+ return tick.pipe(map((doneCb) => once(doneCb))); // each subscriber allowed to call done only once
+ }).pipe(
+ finalize(() => {
+ subscribersCount--;
+ if (subscribersCount === 0) {
+ stop();
+ } else {
+ start(); // restart the loop to potentially unblock the interval
+ }
+ })
+ ),
+ };
+};
diff --git a/src/plugins/data/public/query/timefilter/timefilter.test.ts b/src/plugins/data/public/query/timefilter/timefilter.test.ts
index 8e1e76ed19e6d..92ee6b0c30428 100644
--- a/src/plugins/data/public/query/timefilter/timefilter.test.ts
+++ b/src/plugins/data/public/query/timefilter/timefilter.test.ts
@@ -10,7 +10,7 @@ jest.useFakeTimers();
import sinon from 'sinon';
import moment from 'moment';
-import { Timefilter } from './timefilter';
+import { AutoRefreshDoneFn, Timefilter } from './timefilter';
import { Subscription } from 'rxjs';
import { TimeRange, RefreshInterval } from '../../../common';
import { createNowProviderMock } from '../../now_provider/mocks';
@@ -121,7 +121,7 @@ describe('setRefreshInterval', () => {
beforeEach(() => {
update = sinon.spy();
fetch = sinon.spy();
- autoRefreshFetch = sinon.spy();
+ autoRefreshFetch = sinon.spy((done) => done());
timefilter.setRefreshInterval({
pause: false,
value: 0,
@@ -344,3 +344,44 @@ describe('calculateBounds', () => {
expect(() => timefilter.calculateBounds(timeRange)).toThrowError();
});
});
+
+describe('getAutoRefreshFetch$', () => {
+ test('next auto refresh loop starts after "done" called', () => {
+ const autoRefreshFetch = jest.fn();
+ let doneCb: AutoRefreshDoneFn | undefined;
+ timefilter.getAutoRefreshFetch$().subscribe((done) => {
+ autoRefreshFetch();
+ doneCb = done;
+ });
+ timefilter.setRefreshInterval({ pause: false, value: 1000 });
+
+ expect(autoRefreshFetch).toBeCalledTimes(0);
+ jest.advanceTimersByTime(5000);
+ expect(autoRefreshFetch).toBeCalledTimes(1);
+
+ if (doneCb) doneCb();
+
+ jest.advanceTimersByTime(1005);
+ expect(autoRefreshFetch).toBeCalledTimes(2);
+ });
+
+ test('new getAutoRefreshFetch$ subscription restarts refresh loop', () => {
+ const autoRefreshFetch = jest.fn();
+ const fetch$ = timefilter.getAutoRefreshFetch$();
+ const sub1 = fetch$.subscribe((done) => {
+ autoRefreshFetch();
+ // this done will be never called, but loop will be reset by another subscription
+ });
+ timefilter.setRefreshInterval({ pause: false, value: 1000 });
+
+ expect(autoRefreshFetch).toBeCalledTimes(0);
+ jest.advanceTimersByTime(5000);
+ expect(autoRefreshFetch).toBeCalledTimes(1);
+
+ fetch$.subscribe(autoRefreshFetch);
+ expect(autoRefreshFetch).toBeCalledTimes(1);
+ sub1.unsubscribe();
+ jest.advanceTimersByTime(1005);
+ expect(autoRefreshFetch).toBeCalledTimes(2);
+ });
+});
diff --git a/src/plugins/data/public/query/timefilter/timefilter.ts b/src/plugins/data/public/query/timefilter/timefilter.ts
index 436b18f70a2f8..9894010601d2b 100644
--- a/src/plugins/data/public/query/timefilter/timefilter.ts
+++ b/src/plugins/data/public/query/timefilter/timefilter.ts
@@ -22,6 +22,9 @@ import {
TimeRange,
} from '../../../common';
import { TimeHistoryContract } from './time_history';
+import { createAutoRefreshLoop, AutoRefreshDoneFn } from './lib/auto_refresh_loop';
+
+export { AutoRefreshDoneFn };
// TODO: remove!
@@ -32,8 +35,6 @@ export class Timefilter {
private timeUpdate$ = new Subject();
// Fired when a user changes the the autorefresh settings
private refreshIntervalUpdate$ = new Subject();
- // Used when an auto refresh is triggered
- private autoRefreshFetch$ = new Subject();
private fetch$ = new Subject();
private _time: TimeRange;
@@ -45,11 +46,12 @@ export class Timefilter {
private _isTimeRangeSelectorEnabled: boolean = false;
private _isAutoRefreshSelectorEnabled: boolean = false;
- private _autoRefreshIntervalId: number = 0;
-
private readonly timeDefaults: TimeRange;
private readonly refreshIntervalDefaults: RefreshInterval;
+ // Used when an auto refresh is triggered
+ private readonly autoRefreshLoop = createAutoRefreshLoop();
+
constructor(
config: TimefilterConfig,
timeHistory: TimeHistoryContract,
@@ -86,9 +88,13 @@ export class Timefilter {
return this.refreshIntervalUpdate$.asObservable();
};
- public getAutoRefreshFetch$ = () => {
- return this.autoRefreshFetch$.asObservable();
- };
+ /**
+ * Get an observable that emits when it is time to refetch data due to refresh interval
+ * Each subscription to this observable resets internal interval
+ * Emitted value is a callback {@link AutoRefreshDoneFn} that must be called to restart refresh interval loop
+ * Apps should use this callback to start next auto refresh loop when view finished updating
+ */
+ public getAutoRefreshFetch$ = () => this.autoRefreshLoop.loop$;
public getFetch$ = () => {
return this.fetch$.asObservable();
@@ -166,13 +172,9 @@ export class Timefilter {
}
}
- // Clear the previous auto refresh interval and start a new one (if not paused)
- clearInterval(this._autoRefreshIntervalId);
- if (!newRefreshInterval.pause) {
- this._autoRefreshIntervalId = window.setInterval(
- () => this.autoRefreshFetch$.next(),
- newRefreshInterval.value
- );
+ this.autoRefreshLoop.stop();
+ if (!newRefreshInterval.pause && newRefreshInterval.value !== 0) {
+ this.autoRefreshLoop.start(newRefreshInterval.value);
}
};
diff --git a/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts b/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts
index 0f2b01f618186..c22f62f45a709 100644
--- a/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts
+++ b/src/plugins/data/public/query/timefilter/timefilter_service.mock.ts
@@ -20,7 +20,7 @@ const createSetupContractMock = () => {
getEnabledUpdated$: jest.fn(),
getTimeUpdate$: jest.fn(),
getRefreshIntervalUpdate$: jest.fn(),
- getAutoRefreshFetch$: jest.fn(() => new Observable()),
+ getAutoRefreshFetch$: jest.fn(() => new Observable<() => void>()),
getFetch$: jest.fn(),
getTime: jest.fn(),
setTime: jest.fn(),
diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts
index fded4c46992c0..92a5c36202e6f 100644
--- a/src/plugins/data/public/search/index.ts
+++ b/src/plugins/data/public/search/index.ts
@@ -45,6 +45,8 @@ export {
ISessionsClient,
noSearchSessionStorageCapabilityMessage,
SEARCH_SESSIONS_MANAGEMENT_ID,
+ waitUntilNextSessionCompletes$,
+ WaitUntilNextSessionCompletesOptions,
} from './session';
export { getEsPreference } from './es_search';
diff --git a/src/plugins/data/public/search/session/index.ts b/src/plugins/data/public/search/session/index.ts
index 15410400a33e6..ce578378a2fe8 100644
--- a/src/plugins/data/public/search/session/index.ts
+++ b/src/plugins/data/public/search/session/index.ts
@@ -11,3 +11,7 @@ export { SearchSessionState } from './search_session_state';
export { SessionsClient, ISessionsClient } from './sessions_client';
export { noSearchSessionStorageCapabilityMessage } from './i18n';
export { SEARCH_SESSIONS_MANAGEMENT_ID } from './constants';
+export {
+ waitUntilNextSessionCompletes$,
+ WaitUntilNextSessionCompletesOptions,
+} from './session_helpers';
diff --git a/src/plugins/data/public/search/session/session_helpers.test.ts b/src/plugins/data/public/search/session/session_helpers.test.ts
new file mode 100644
index 0000000000000..5b64e7b554d18
--- /dev/null
+++ b/src/plugins/data/public/search/session/session_helpers.test.ts
@@ -0,0 +1,88 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { waitUntilNextSessionCompletes$ } from './session_helpers';
+import { ISessionService, SessionService } from './session_service';
+import { BehaviorSubject } from 'rxjs';
+import { SearchSessionState } from './search_session_state';
+import { NowProviderInternalContract } from '../../now_provider';
+import { coreMock } from '../../../../../core/public/mocks';
+import { createNowProviderMock } from '../../now_provider/mocks';
+import { SEARCH_SESSIONS_MANAGEMENT_ID } from './constants';
+import { getSessionsClientMock } from './mocks';
+
+let sessionService: ISessionService;
+let state$: BehaviorSubject;
+let nowProvider: jest.Mocked;
+let currentAppId$: BehaviorSubject;
+
+beforeEach(() => {
+ const initializerContext = coreMock.createPluginInitializerContext();
+ const startService = coreMock.createSetup().getStartServices;
+ nowProvider = createNowProviderMock();
+ currentAppId$ = new BehaviorSubject('app');
+ sessionService = new SessionService(
+ initializerContext,
+ () =>
+ startService().then(([coreStart, ...rest]) => [
+ {
+ ...coreStart,
+ application: {
+ ...coreStart.application,
+ currentAppId$,
+ capabilities: {
+ ...coreStart.application.capabilities,
+ management: {
+ kibana: {
+ [SEARCH_SESSIONS_MANAGEMENT_ID]: true,
+ },
+ },
+ },
+ },
+ },
+ ...rest,
+ ]),
+ getSessionsClientMock(),
+ nowProvider,
+ { freezeState: false } // needed to use mocks inside state container
+ );
+ state$ = new BehaviorSubject(SearchSessionState.None);
+ sessionService.state$.subscribe(state$);
+});
+
+describe('waitUntilNextSessionCompletes$', () => {
+ beforeEach(() => {
+ jest.useFakeTimers();
+ });
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+ test('emits when next session starts', () => {
+ sessionService.start();
+ let untrackSearch = sessionService.trackSearch({ abort: () => {} });
+ untrackSearch();
+
+ const next = jest.fn();
+ const complete = jest.fn();
+ waitUntilNextSessionCompletes$(sessionService).subscribe({ next, complete });
+ expect(next).not.toBeCalled();
+
+ sessionService.start();
+ expect(next).not.toBeCalled();
+
+ untrackSearch = sessionService.trackSearch({ abort: () => {} });
+ untrackSearch();
+
+ expect(next).not.toBeCalled();
+ jest.advanceTimersByTime(500);
+ expect(next).not.toBeCalled();
+ jest.advanceTimersByTime(1000);
+ expect(next).toBeCalledTimes(1);
+ expect(complete).toBeCalled();
+ });
+});
diff --git a/src/plugins/data/public/search/session/session_helpers.ts b/src/plugins/data/public/search/session/session_helpers.ts
new file mode 100644
index 0000000000000..1f0a2da7e93f4
--- /dev/null
+++ b/src/plugins/data/public/search/session/session_helpers.ts
@@ -0,0 +1,48 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { debounceTime, first, skipUntil } from 'rxjs/operators';
+import { ISessionService } from './session_service';
+import { SearchSessionState } from './search_session_state';
+
+/**
+ * Options for {@link waitUntilNextSessionCompletes$}
+ */
+export interface WaitUntilNextSessionCompletesOptions {
+ /**
+ * For how long to wait between session state transitions before considering that session completed
+ */
+ waitForIdle?: number;
+}
+
+/**
+ * Creates an observable that emits when next search session completes.
+ * This utility is helpful to use in the application to delay some tasks until next session completes.
+ *
+ * @param sessionService - {@link ISessionService}
+ * @param opts - {@link WaitUntilNextSessionCompletesOptions}
+ */
+export function waitUntilNextSessionCompletes$(
+ sessionService: ISessionService,
+ { waitForIdle = 1000 }: WaitUntilNextSessionCompletesOptions = { waitForIdle: 1000 }
+) {
+ return sessionService.state$.pipe(
+ // wait until new session starts
+ skipUntil(sessionService.state$.pipe(first((state) => state === SearchSessionState.None))),
+ // wait until new session starts loading
+ skipUntil(sessionService.state$.pipe(first((state) => state === SearchSessionState.Loading))),
+ // debounce to ignore quick switches from loading <-> completed.
+ // that could happen between sequential search requests inside a single session
+ debounceTime(waitForIdle),
+ // then wait until it finishes
+ first(
+ (state) =>
+ state === SearchSessionState.Completed || state === SearchSessionState.BackgroundCompleted
+ )
+ );
+}
diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js
index 2c80fc111c740..3be047859d3b0 100644
--- a/src/plugins/discover/public/application/angular/discover.js
+++ b/src/plugins/discover/public/application/angular/discover.js
@@ -8,7 +8,7 @@
import _ from 'lodash';
import { merge, Subject, Subscription } from 'rxjs';
-import { debounceTime } from 'rxjs/operators';
+import { debounceTime, tap, filter } from 'rxjs/operators';
import { i18n } from '@kbn/i18n';
import { createSearchSessionRestorationDataProvider, getState, splitState } from './discover_state';
import { RequestAdapter } from '../../../../inspector/public';
@@ -393,12 +393,11 @@ function discoverController($route, $scope) {
$scope.state.index = $scope.indexPattern.id;
$scope.state.sort = getSortArray($scope.state.sort, $scope.indexPattern);
- $scope.opts.fetch = $scope.fetch = function () {
+ $scope.opts.fetch = $scope.fetch = async function () {
$scope.fetchCounter++;
$scope.fetchError = undefined;
if (!validateTimeRange(timefilter.getTime(), toastNotifications)) {
$scope.resultState = 'none';
- return;
}
// Abort any in-progress requests before fetching again
@@ -494,11 +493,19 @@ function discoverController($route, $scope) {
showUnmappedFields,
};
+ // handler emitted by `timefilter.getAutoRefreshFetch$()`
+ // to notify when data completed loading and to start a new autorefresh loop
+ let autoRefreshDoneCb;
const fetch$ = merge(
refetch$,
filterManager.getFetches$(),
timefilter.getFetch$(),
- timefilter.getAutoRefreshFetch$(),
+ timefilter.getAutoRefreshFetch$().pipe(
+ tap((done) => {
+ autoRefreshDoneCb = done;
+ }),
+ filter(() => $scope.fetchStatus !== fetchStatuses.LOADING)
+ ),
data.query.queryString.getUpdates$(),
searchSessionManager.newSearchSessionIdFromURL$
).pipe(debounceTime(100));
@@ -508,7 +515,16 @@ function discoverController($route, $scope) {
$scope,
fetch$,
{
- next: $scope.fetch,
+ next: async () => {
+ try {
+ await $scope.fetch();
+ } finally {
+ // if there is a saved `autoRefreshDoneCb`, notify auto refresh service that
+ // the last fetch is completed so it starts the next auto refresh loop if needed
+ autoRefreshDoneCb?.();
+ autoRefreshDoneCb = undefined;
+ }
+ },
},
(error) => addFatalError(core.fatalErrors, error)
)
diff --git a/src/plugins/expressions/public/loader.ts b/src/plugins/expressions/public/loader.ts
index 65925b5a2e4c2..4165b8906a20e 100644
--- a/src/plugins/expressions/public/loader.ts
+++ b/src/plugins/expressions/public/loader.ts
@@ -118,12 +118,15 @@ export class ExpressionLoader {
return this.execution ? (this.execution.inspect() as Adapters) : undefined;
}
- update(expression?: string | ExpressionAstExpression, params?: IExpressionLoaderParams): void {
+ async update(
+ expression?: string | ExpressionAstExpression,
+ params?: IExpressionLoaderParams
+ ): Promise {
this.setParams(params);
this.loadingSubject.next(true);
if (expression) {
- this.loadData(expression, this.params);
+ await this.loadData(expression, this.params);
} else if (this.data) {
this.render(this.data);
}
diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts
index 429dabeeef042..efb166c8975bb 100644
--- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts
+++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts
@@ -367,8 +367,8 @@ export class VisualizeEmbeddable
}
}
- public reload = () => {
- this.handleVisUpdate();
+ public reload = async () => {
+ await this.handleVisUpdate();
};
private async updateHandler() {
@@ -395,13 +395,13 @@ export class VisualizeEmbeddable
});
if (this.handler && !abortController.signal.aborted) {
- this.handler.update(this.expression, expressionParams);
+ await this.handler.update(this.expression, expressionParams);
}
}
private handleVisUpdate = async () => {
this.handleChanges();
- this.updateHandler();
+ await this.updateHandler();
};
private uiStateChangeHandler = () => {
diff --git a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx
index 256e634ac6c40..f6ef1caf9c9e0 100644
--- a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx
+++ b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx
@@ -183,8 +183,12 @@ const TopNav = ({
useEffect(() => {
const autoRefreshFetchSub = services.data.query.timefilter.timefilter
.getAutoRefreshFetch$()
- .subscribe(() => {
- visInstance.embeddableHandler.reload();
+ .subscribe(async (done) => {
+ try {
+ await visInstance.embeddableHandler.reload();
+ } finally {
+ done();
+ }
});
return () => {
autoRefreshFetchSub.unsubscribe();
diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx
index 20bf349f6b13a..b7dbf1bbe4d87 100644
--- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx
+++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx
@@ -155,11 +155,7 @@ function createMockTimefilter() {
getBounds: jest.fn(() => timeFilter),
getRefreshInterval: () => {},
getRefreshIntervalDefaults: () => {},
- getAutoRefreshFetch$: () => ({
- subscribe: ({ next }: { next: () => void }) => {
- return next;
- },
- }),
+ getAutoRefreshFetch$: () => new Observable(),
};
}
diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx
index dbc10c751a649..39163101fc7bd 100644
--- a/x-pack/plugins/lens/public/app_plugin/app.tsx
+++ b/x-pack/plugins/lens/public/app_plugin/app.tsx
@@ -14,6 +14,7 @@ import { Toast } from 'kibana/public';
import { VisualizeFieldContext } from 'src/plugins/ui_actions/public';
import { Datatable } from 'src/plugins/expressions/public';
import { EuiBreadcrumb } from '@elastic/eui';
+import { finalize, switchMap, tap } from 'rxjs/operators';
import { downloadMultipleAs } from '../../../../../src/plugins/share/public';
import {
createKbnUrlStateStorage,
@@ -37,6 +38,7 @@ import {
Query,
SavedQuery,
syncQueryStateWithUrl,
+ waitUntilNextSessionCompletes$,
} from '../../../../../src/plugins/data/public';
import { LENS_EMBEDDABLE_TYPE, getFullPath, APP_ID } from '../../common';
import { LensAppProps, LensAppServices, LensAppState } from './types';
@@ -193,14 +195,19 @@ export function App({
const autoRefreshSubscription = data.query.timefilter.timefilter
.getAutoRefreshFetch$()
- .subscribe({
- next: () => {
+ .pipe(
+ tap(() => {
setState((s) => ({
...s,
searchSessionId: data.search.session.start(),
}));
- },
- });
+ }),
+ switchMap((done) =>
+ // best way in lens to estimate that all panels are updated is to rely on search session service state
+ waitUntilNextSessionCompletes$(data.search.session).pipe(finalize(done))
+ )
+ )
+ .subscribe();
const kbnUrlStateStorage = createKbnUrlStateStorage({
history,
From 81adccd389b8c064b7a26e61f6071664b81eb8d4 Mon Sep 17 00:00:00 2001
From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com>
Date: Thu, 8 Apr 2021 10:20:43 -0400
Subject: [PATCH 13/22] [Security Solution][Endpoint] Endpoint Event Filtering
List, Test Data Generator and Loader (#96263) (#96456)
* Added new const to List plugin for new Endpont Event Filter list
* Data Generator for event filters ++ script to load event filters (WIP)
* refactor `generate_data` to use `BaseDataGenerator` class
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
x-pack/plugins/lists/common/constants.ts | 9 ++
.../create_endoint_event_filters_list.ts | 79 +++++++++++++
.../data_generators/base_data_generator.ts | 94 +++++++++++++++
.../data_generators/event_filter_generator.ts | 30 +++++
.../common/endpoint/generate_data.ts | 56 ++-------
.../scripts/endpoint/event_filters/index.ts | 111 ++++++++++++++++++
.../scripts/endpoint/load_event_filters.js | 11 ++
7 files changed, 341 insertions(+), 49 deletions(-)
create mode 100644 x-pack/plugins/lists/server/services/exception_lists/create_endoint_event_filters_list.ts
create mode 100644 x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts
create mode 100644 x-pack/plugins/security_solution/common/endpoint/data_generators/event_filter_generator.ts
create mode 100644 x-pack/plugins/security_solution/scripts/endpoint/event_filters/index.ts
create mode 100755 x-pack/plugins/security_solution/scripts/endpoint/load_event_filters.js
diff --git a/x-pack/plugins/lists/common/constants.ts b/x-pack/plugins/lists/common/constants.ts
index 92d8b6f5f7571..4f897c83cb41d 100644
--- a/x-pack/plugins/lists/common/constants.ts
+++ b/x-pack/plugins/lists/common/constants.ts
@@ -60,3 +60,12 @@ export const ENDPOINT_TRUSTED_APPS_LIST_NAME = 'Endpoint Security Trusted Apps L
/** Description of trusted apps agnostic list */
export const ENDPOINT_TRUSTED_APPS_LIST_DESCRIPTION = 'Endpoint Security Trusted Apps List';
+
+/** ID of event filters agnostic list */
+export const ENDPOINT_EVENT_FILTERS_LIST_ID = 'endpoint_event_filters';
+
+/** Name of event filters agnostic list */
+export const ENDPOINT_EVENT_FILTERS_LIST_NAME = 'Endpoint Security Event Filters List';
+
+/** Description of event filters agnostic list */
+export const ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION = 'Endpoint Security Event Filters List';
diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_endoint_event_filters_list.ts b/x-pack/plugins/lists/server/services/exception_lists/create_endoint_event_filters_list.ts
new file mode 100644
index 0000000000000..95e9df03400af
--- /dev/null
+++ b/x-pack/plugins/lists/server/services/exception_lists/create_endoint_event_filters_list.ts
@@ -0,0 +1,79 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { SavedObjectsClientContract } from 'kibana/server';
+import uuid from 'uuid';
+
+import {
+ ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION,
+ ENDPOINT_EVENT_FILTERS_LIST_ID,
+ ENDPOINT_EVENT_FILTERS_LIST_NAME,
+} from '../../../common/constants';
+import { ExceptionListSchema, ExceptionListSoSchema, Version } from '../../../common/schemas';
+
+import { getSavedObjectType, transformSavedObjectToExceptionList } from './utils';
+
+interface CreateEndpointEventFiltersListOptions {
+ savedObjectsClient: SavedObjectsClientContract;
+ user: string;
+ tieBreaker?: string;
+ version: Version;
+}
+
+/**
+ * Creates the Endpoint Trusted Apps agnostic list if it does not yet exist
+ *
+ * @param savedObjectsClient
+ * @param user
+ * @param tieBreaker
+ * @param version
+ */
+export const createEndpointEventFiltersList = async ({
+ savedObjectsClient,
+ user,
+ tieBreaker,
+ version,
+}: CreateEndpointEventFiltersListOptions): Promise => {
+ const savedObjectType = getSavedObjectType({ namespaceType: 'agnostic' });
+ const dateNow = new Date().toISOString();
+ try {
+ const savedObject = await savedObjectsClient.create(
+ savedObjectType,
+ {
+ comments: undefined,
+ created_at: dateNow,
+ created_by: user,
+ description: ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION,
+ entries: undefined,
+ immutable: false,
+ item_id: undefined,
+ list_id: ENDPOINT_EVENT_FILTERS_LIST_ID,
+ list_type: 'list',
+ meta: undefined,
+ name: ENDPOINT_EVENT_FILTERS_LIST_NAME,
+ os_types: [],
+ tags: [],
+ tie_breaker_id: tieBreaker ?? uuid.v4(),
+ type: 'endpoint',
+ updated_by: user,
+ version,
+ },
+ {
+ // We intentionally hard coding the id so that there can only be one Event Filters list within the space
+ id: ENDPOINT_EVENT_FILTERS_LIST_ID,
+ }
+ );
+
+ return transformSavedObjectToExceptionList({ savedObject });
+ } catch (err) {
+ if (savedObjectsClient.errors.isConflictError(err)) {
+ return null;
+ } else {
+ throw err;
+ }
+ }
+};
diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts
new file mode 100644
index 0000000000000..c0888a6c2a4bd
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts
@@ -0,0 +1,94 @@
+/*
+ * 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 seedrandom from 'seedrandom';
+import uuid from 'uuid';
+
+const OS_FAMILY = ['windows', 'macos', 'linux'];
+
+/**
+ * A generic base class to assist in creating domain specific data generators. It includes
+ * several general purpose random data generators for use within the class and exposes one
+ * public method named `generate()` which should be implemented by sub-classes.
+ */
+export class BaseDataGenerator {
+ protected random: seedrandom.prng;
+
+ constructor(seed: string | seedrandom.prng = Math.random().toString()) {
+ if (typeof seed === 'string') {
+ this.random = seedrandom(seed);
+ } else {
+ this.random = seed;
+ }
+ }
+
+ /**
+ * Generate a new record
+ */
+ public generate(): GeneratedDoc {
+ throw new Error('method not implemented!');
+ }
+
+ /** generate random OS family value */
+ protected randomOSFamily(): string {
+ return this.randomChoice(OS_FAMILY);
+ }
+
+ /** generate a UUID (v4) */
+ protected randomUUID(): string {
+ return uuid.v4();
+ }
+
+ /** Generate a random number up to the max provided */
+ protected randomN(max: number): number {
+ return Math.floor(this.random() * max);
+ }
+
+ protected *randomNGenerator(max: number, count: number) {
+ let iCount = count;
+ while (iCount > 0) {
+ yield this.randomN(max);
+ iCount = iCount - 1;
+ }
+ }
+
+ /**
+ * Create an array of a given size and fill it with data provided by a generator
+ *
+ * @param lengthLimit
+ * @param generator
+ * @protected
+ */
+ protected randomArray(lengthLimit: number, generator: () => T): T[] {
+ const rand = this.randomN(lengthLimit) + 1;
+ return [...Array(rand).keys()].map(generator);
+ }
+
+ protected randomMac(): string {
+ return [...this.randomNGenerator(255, 6)].map((x) => x.toString(16)).join('-');
+ }
+
+ protected randomIP(): string {
+ return [10, ...this.randomNGenerator(255, 3)].map((x) => x.toString()).join('.');
+ }
+
+ protected randomVersion(): string {
+ return [6, ...this.randomNGenerator(10, 2)].map((x) => x.toString()).join('.');
+ }
+
+ protected randomChoice(choices: T[]): T {
+ return choices[this.randomN(choices.length)];
+ }
+
+ protected randomString(length: number): string {
+ return [...this.randomNGenerator(36, length)].map((x) => x.toString(36)).join('');
+ }
+
+ protected randomHostname(): string {
+ return `Host-${this.randomString(10)}`;
+ }
+}
diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/event_filter_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/event_filter_generator.ts
new file mode 100644
index 0000000000000..6bdbb9cde2034
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/event_filter_generator.ts
@@ -0,0 +1,30 @@
+/*
+ * 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 { BaseDataGenerator } from './base_data_generator';
+import { ENDPOINT_EVENT_FILTERS_LIST_ID } from '../../../../lists/common/constants';
+import { CreateExceptionListItemSchema } from '../../../../lists/common';
+import { getCreateExceptionListItemSchemaMock } from '../../../../lists/common/schemas/request/create_exception_list_item_schema.mock';
+
+export class EventFilterGenerator extends BaseDataGenerator {
+ generate(): CreateExceptionListItemSchema {
+ const overrides: Partial