diff --git a/.ci/Dockerfile b/.ci/Dockerfile index 065b7f2aafd61..d6eee046611bb 100644 --- a/.ci/Dockerfile +++ b/.ci/Dockerfile @@ -1,7 +1,7 @@ # NOTE: This Dockerfile is ONLY used to run certain tasks in CI. It is not used to run Kibana or as a distributable. # If you're looking for the Kibana Docker image distributable, please see: src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts -ARG NODE_VERSION=10.22.0 +ARG NODE_VERSION=10.22.1 FROM node:${NODE_VERSION} AS base diff --git a/.ci/Jenkinsfile_baseline_capture b/.ci/Jenkinsfile_baseline_capture index 9a49c19b94df2..33ecfcd84fd3e 100644 --- a/.ci/Jenkinsfile_baseline_capture +++ b/.ci/Jenkinsfile_baseline_capture @@ -11,14 +11,14 @@ kibanaPipeline(timeoutMinutes: 120) { 'CI_PARALLEL_PROCESS_NUMBER=1' ]) { parallel([ - 'oss-visualRegression': { - workers.ci(name: 'oss-visualRegression', size: 's-highmem', ramDisk: true) { - kibanaPipeline.functionalTestProcess('oss-visualRegression', './test/scripts/jenkins_visual_regression.sh')() + 'oss-baseline': { + workers.ci(name: 'oss-baseline', size: 's-highmem', ramDisk: true, runErrorReporter: false) { + kibanaPipeline.functionalTestProcess('oss-baseline', './test/scripts/jenkins_baseline.sh')() } }, - 'xpack-visualRegression': { - workers.ci(name: 'xpack-visualRegression', size: 's-highmem', ramDisk: true) { - kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh')() + 'xpack-baseline': { + workers.ci(name: 'xpack-baseline', size: 's-highmem', ramDisk: true, runErrorReporter: false) { + kibanaPipeline.functionalTestProcess('xpack-baseline', './test/scripts/jenkins_xpack_baseline.sh')() } }, ]) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d81f6af4cec28..7daa42af7024d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -59,7 +59,6 @@ # APM /x-pack/plugins/apm/ @elastic/apm-ui /x-pack/test/functional/apps/apm/ @elastic/apm-ui -/src/legacy/core_plugins/apm_oss/ @elastic/apm-ui /src/plugins/apm_oss/ @elastic/apm-ui /src/apm.js @watson @vigneshshanmugam @@ -83,9 +82,6 @@ /src/plugins/home/public @elastic/kibana-core-ui /src/plugins/home/server/*.ts @elastic/kibana-core-ui /src/plugins/home/server/services/ @elastic/kibana-core-ui -# Exclude tutorial resources folder for now because they are not owned by Kibana app and most will move out soon -/src/legacy/core_plugins/kibana/public/home/*.ts @elastic/kibana-core-ui -/src/legacy/core_plugins/kibana/public/home/np_ready/ @elastic/kibana-core-ui /x-pack/plugins/global_search_bar/ @elastic/kibana-core-ui # Observability UIs @@ -167,7 +163,6 @@ # Security /src/core/server/csp/ @elastic/kibana-security @elastic/kibana-platform -/x-pack/legacy/plugins/security/ @elastic/kibana-security /x-pack/legacy/plugins/spaces/ @elastic/kibana-security /x-pack/plugins/spaces/ @elastic/kibana-security /x-pack/plugins/encrypted_saved_objects/ @elastic/kibana-security @@ -286,8 +281,6 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib # Core design /src/plugins/dashboard/**/*.scss @elastic/kibana-core-ui-designers /x-pack/plugins/canvas/**/*.scss @elastic/kibana-core-ui-designers -/src/legacy/core_plugins/kibana/public/home/**/*.scss @elastic/kibana-core-ui-designers -/x-pack/legacy/plugins/security/**/*.scss @elastic/kibana-core-ui-designers /x-pack/legacy/plugins/spaces/**/*.scss @elastic/kibana-core-ui-designers /x-pack/plugins/spaces/**/*.scss @elastic/kibana-core-ui-designers /x-pack/plugins/security/**/*.scss @elastic/kibana-core-ui-designers @@ -297,7 +290,7 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib /x-pack/plugins/infra/**/*.scss @elastic/observability-design /x-pack/plugins/ingest_manager/**/*.scss @elastic/observability-design /x-pack/plugins/observability/**/*.scss @elastic/observability-design -/x-pack/plugins/monitoring/**/*.scss @elastic/observability-design +/x-pack/plugins/monitoring/**/*.scss @elastic/observability-design # Ent. Search design /x-pack/plugins/enterprise_search/**/*.scss @elastic/ent-search-design diff --git a/.i18nrc.json b/.i18nrc.json index e8431fdb3f0e1..153a5a6cafece 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -57,7 +57,8 @@ "visTypeXy": "src/plugins/vis_type_xy", "visualizations": "src/plugins/visualizations", "visualize": "src/plugins/visualize", - "apmOss": "src/plugins/apm_oss" + "apmOss": "src/plugins/apm_oss", + "usageCollection": "src/plugins/usage_collection" }, "exclude": [ "src/legacy/ui/ui_render/ui_render_mixin.js" diff --git a/.node-version b/.node-version index b7604b0c8aea6..c2f6421352c48 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -10.22.0 +10.22.1 diff --git a/.nvmrc b/.nvmrc index b7604b0c8aea6..c2f6421352c48 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -10.22.0 +10.22.1 diff --git a/docs/api/dashboard/import-dashboard.asciidoc b/docs/api/dashboard/import-dashboard.asciidoc index 020ec8018b85b..56bd4abbc8023 100644 --- a/docs/api/dashboard/import-dashboard.asciidoc +++ b/docs/api/dashboard/import-dashboard.asciidoc @@ -23,7 +23,7 @@ experimental[] Import dashboards and corresponding saved objects. [[dashboard-api-import-request-body]] ==== Request body -Use the complete response body from the <> as the request body. Do not manually construct a payload to the endpoint. +Use the complete response body from the <> as the request body. Do not manually construct a payload to the endpoint. The max payload size is determined by the `savedObjects.maxImportPayloadBytes` configuration key. [[dashboard-api-import-response-body]] ==== Response body diff --git a/docs/apm/troubleshooting.asciidoc b/docs/apm/troubleshooting.asciidoc index e00a67f6c78a4..b4c9c6a4ec39e 100644 --- a/docs/apm/troubleshooting.asciidoc +++ b/docs/apm/troubleshooting.asciidoc @@ -49,7 +49,7 @@ GET /_template/apm-{version} *Using Logstash, Kafka, etc.* If you're not outputting data directly from APM Server to Elasticsearch (perhaps you're using Logstash or Kafka), then the index template will not be set up automatically. Instead, you'll need to -{apm-server-ref}/configuration-template.html[load the template manually]. +{apm-server-ref}/apm-server-template.html[load the template manually]. *Using a custom index names* This problem can also occur if you've customized the index name that you write APM data to. diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 7727cd322181f..501a3698d07d9 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -494,6 +494,10 @@ in their infrastructure. |This plugins adopts some conventions in addition to or in place of conventions in Kibana (at the time of the plugin's creation): +|{kib-repo}blob/{branch}/x-pack/plugins/xpack_legacy/README.md[xpackLegacy] +|Contains HTTP endpoints and UiSettings that are slated for removal. + + |=== include::{kibana-root}/src/plugins/dashboard/README.asciidoc[leveloffset=+1] diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.md index ccc73d4fb858e..75da8df2ae15a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.coresetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.coresetup.md @@ -23,6 +23,7 @@ export interface CoreSetupStartServicesAccessor<TPluginsStart, TStart> | [StartServicesAccessor](./kibana-plugin-core-server.startservicesaccessor.md) | | [http](./kibana-plugin-core-server.coresetup.http.md) | HttpServiceSetup & {
resources: HttpResources;
} | [HttpServiceSetup](./kibana-plugin-core-server.httpservicesetup.md) | | [logging](./kibana-plugin-core-server.coresetup.logging.md) | LoggingServiceSetup | [LoggingServiceSetup](./kibana-plugin-core-server.loggingservicesetup.md) | +| [metrics](./kibana-plugin-core-server.coresetup.metrics.md) | MetricsServiceSetup | [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) | | [savedObjects](./kibana-plugin-core-server.coresetup.savedobjects.md) | SavedObjectsServiceSetup | [SavedObjectsServiceSetup](./kibana-plugin-core-server.savedobjectsservicesetup.md) | | [status](./kibana-plugin-core-server.coresetup.status.md) | StatusServiceSetup | [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) | | [uiSettings](./kibana-plugin-core-server.coresetup.uisettings.md) | UiSettingsServiceSetup | [UiSettingsServiceSetup](./kibana-plugin-core-server.uisettingsservicesetup.md) | diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.metrics.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.metrics.md new file mode 100644 index 0000000000000..77c9e867ef8ea --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.coresetup.metrics.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CoreSetup](./kibana-plugin-core-server.coresetup.md) > [metrics](./kibana-plugin-core-server.coresetup.metrics.md) + +## CoreSetup.metrics property + +[MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) + +Signature: + +```typescript +metrics: MetricsServiceSetup; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.corestart.md b/docs/development/core/server/kibana-plugin-core-server.corestart.md index 610c85c71e362..0d5474fae5e16 100644 --- a/docs/development/core/server/kibana-plugin-core-server.corestart.md +++ b/docs/development/core/server/kibana-plugin-core-server.corestart.md @@ -20,7 +20,7 @@ export interface CoreStart | [capabilities](./kibana-plugin-core-server.corestart.capabilities.md) | CapabilitiesStart | [CapabilitiesStart](./kibana-plugin-core-server.capabilitiesstart.md) | | [elasticsearch](./kibana-plugin-core-server.corestart.elasticsearch.md) | ElasticsearchServiceStart | [ElasticsearchServiceStart](./kibana-plugin-core-server.elasticsearchservicestart.md) | | [http](./kibana-plugin-core-server.corestart.http.md) | HttpServiceStart | [HttpServiceStart](./kibana-plugin-core-server.httpservicestart.md) | -| [metrics](./kibana-plugin-core-server.corestart.metrics.md) | MetricsServiceStart | | +| [metrics](./kibana-plugin-core-server.corestart.metrics.md) | MetricsServiceStart | [MetricsServiceStart](./kibana-plugin-core-server.metricsservicestart.md) | | [savedObjects](./kibana-plugin-core-server.corestart.savedobjects.md) | SavedObjectsServiceStart | [SavedObjectsServiceStart](./kibana-plugin-core-server.savedobjectsservicestart.md) | | [uiSettings](./kibana-plugin-core-server.corestart.uisettings.md) | UiSettingsServiceStart | [UiSettingsServiceStart](./kibana-plugin-core-server.uisettingsservicestart.md) | diff --git a/docs/development/core/server/kibana-plugin-core-server.corestart.metrics.md b/docs/development/core/server/kibana-plugin-core-server.corestart.metrics.md index a51c2f842c346..2c32f730c4c9b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.corestart.metrics.md +++ b/docs/development/core/server/kibana-plugin-core-server.corestart.metrics.md @@ -4,6 +4,7 @@ ## CoreStart.metrics property +[MetricsServiceStart](./kibana-plugin-core-server.metricsservicestart.md) Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 69f2cf0338a01..b83c091846f04 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -253,6 +253,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [LegacyElasticsearchClientConfig](./kibana-plugin-core-server.legacyelasticsearchclientconfig.md) | | | [LifecycleResponseFactory](./kibana-plugin-core-server.lifecycleresponsefactory.md) | Creates an object containing redirection or error response with error details, HTTP headers, and other data transmitted to the client. | | [LoggerConfigType](./kibana-plugin-core-server.loggerconfigtype.md) | | +| [MetricsServiceStart](./kibana-plugin-core-server.metricsservicestart.md) | APIs to retrieves metrics gathered and exposed by the core platform. | | [MIGRATION\_ASSISTANCE\_INDEX\_ACTION](./kibana-plugin-core-server.migration_assistance_index_action.md) | | | [MIGRATION\_DEPRECATION\_LEVEL](./kibana-plugin-core-server.migration_deprecation_level.md) | | | [MutatingOperationRefreshSetting](./kibana-plugin-core-server.mutatingoperationrefreshsetting.md) | Elasticsearch Refresh setting for mutating operation | diff --git a/docs/development/core/server/kibana-plugin-core-server.metricsservicestart.md b/docs/development/core/server/kibana-plugin-core-server.metricsservicestart.md new file mode 100644 index 0000000000000..8b3280d528c18 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.metricsservicestart.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [MetricsServiceStart](./kibana-plugin-core-server.metricsservicestart.md) + +## MetricsServiceStart type + +APIs to retrieves metrics gathered and exposed by the core platform. + +Signature: + +```typescript +export declare type MetricsServiceStart = MetricsServiceSetup; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.servicestatuslevels.md b/docs/development/core/server/kibana-plugin-core-server.servicestatuslevels.md index a66cec78c736b..e57dc192cd572 100644 --- a/docs/development/core/server/kibana-plugin-core-server.servicestatuslevels.md +++ b/docs/development/core/server/kibana-plugin-core-server.servicestatuslevels.md @@ -13,18 +13,22 @@ ServiceStatusLevels: Readonly<{ available: Readonly<{ toString: () => "available"; valueOf: () => 0; + toJSON: () => "available"; }>; degraded: Readonly<{ toString: () => "degraded"; valueOf: () => 1; + toJSON: () => "degraded"; }>; unavailable: Readonly<{ toString: () => "unavailable"; valueOf: () => 2; + toJSON: () => "unavailable"; }>; critical: Readonly<{ toString: () => "critical"; valueOf: () => 3; + toJSON: () => "critical"; }>; }> ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.sharedglobalconfig.md b/docs/development/core/server/kibana-plugin-core-server.sharedglobalconfig.md index 7f306919101ef..ec2e1b227a2d7 100644 --- a/docs/development/core/server/kibana-plugin-core-server.sharedglobalconfig.md +++ b/docs/development/core/server/kibana-plugin-core-server.sharedglobalconfig.md @@ -12,5 +12,6 @@ export declare type SharedGlobalConfig = RecursiveReadonly<{ kibana: Pick; elasticsearch: Pick; path: Pick; + savedObjects: Pick; }>; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.isstatuspageanonymous.md b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.isstatuspageanonymous.md new file mode 100644 index 0000000000000..c417aaa2cef48 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.isstatuspageanonymous.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) > [isStatusPageAnonymous](./kibana-plugin-core-server.statusservicesetup.isstatuspageanonymous.md) + +## StatusServiceSetup.isStatusPageAnonymous property + +Whether or not the status HTTP APIs are available to unauthenticated users when an authentication provider is present. + +Signature: + +```typescript +isStatusPageAnonymous: () => boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md index ba0645be4d26c..f522d11a7ffef 100644 --- a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md @@ -74,6 +74,7 @@ core.status.set( | [core$](./kibana-plugin-core-server.statusservicesetup.core_.md) | Observable<CoreStatus> | Current status for all Core services. | | [dependencies$](./kibana-plugin-core-server.statusservicesetup.dependencies_.md) | Observable<Record<string, ServiceStatus>> | Current status for all plugins this plugin depends on. Each key of the Record is a plugin id. | | [derivedStatus$](./kibana-plugin-core-server.statusservicesetup.derivedstatus_.md) | Observable<ServiceStatus> | The status of this plugin as derived from its dependencies. | +| [isStatusPageAnonymous](./kibana-plugin-core-server.statusservicesetup.isstatuspageanonymous.md) | () => boolean | Whether or not the status HTTP APIs are available to unauthenticated users when an authentication provider is present. | | [overall$](./kibana-plugin-core-server.statusservicesetup.overall_.md) | Observable<ServiceStatus> | Overall system status for all of Kibana. | ## Methods diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index 9c8d753a2d668..3489dcd018293 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -104,15 +104,14 @@ security is enabled, `xpack.security.encryptionKey`. [cols="2*<"] |=== | `xpack.reporting.queue.pollInterval` - | Specifies the number of milliseconds that the reporting poller waits between polling the - index for any pending Reporting jobs. Defaults to `3000` (3 seconds). + | Specify the {ref}/common-options.html#time-units[time] that the reporting poller waits between polling the index for any + pending Reporting jobs. Can be specified as number of milliseconds. Defaults to `3s`. | [[xpack-reporting-q-timeout]] `xpack.reporting.queue.timeout` {ess-icon} - | How long each worker has to produce a report. If your machine is slow or under - heavy load, you might need to increase this timeout. Specified in milliseconds. - If a Reporting job execution time goes over this time limit, the job will be - marked as a failure and there will not be a download available. - Defaults to `120000` (two minutes). + | {ref}/common-options.html#time-units[How long] each worker has to produce a report. If your machine is slow or under heavy + load, you might need to increase this timeout. If a Reporting job execution goes over this time limit, the job is marked as a + failure and no download will be available. Can be specified as number of milliseconds. + Defaults to `2m`. |=== @@ -127,24 +126,24 @@ control the capturing process. |=== a| `xpack.reporting.capture.timeouts` `.openUrl` {ess-icon} - | Specify how long to allow the Reporting browser to wait for the "Loading..." screen - to dismiss and find the initial data for the Kibana page. If the time is - exceeded, a page screenshot is captured showing the current state, and the download link shows a warning message. - Defaults to `60000` (1 minute). + | Specify the {ref}/common-options.html#time-units[time] to allow the Reporting browser to wait for the "Loading..." screen + to dismiss and find the initial data for the page. If the time is exceeded, a screenshot is captured showing the current + page, and the download link shows a warning message. Can be specified as number of milliseconds. + Defaults to `1m`. a| `xpack.reporting.capture.timeouts` `.waitForElements` {ess-icon} - | Specify how long to allow the Reporting browser to wait for all visualization - panels to load on the Kibana page. If the time is exceeded, a page screenshot - is captured showing the current state, and the download link shows a warning message. Defaults to `30000` (30 - seconds). + | Specify the {ref}/common-options.html#time-units[time] to allow the Reporting browser to wait for all visualization panels + to load on the page. If the time is exceeded, a screenshot is captured showing the current page, and the download link shows + a warning message. Can be specified as number of milliseconds. + Defaults to `30s`. a| `xpack.reporting.capture.timeouts` `.renderComplete` {ess-icon} - | Specify how long to allow the Reporting browser to wait for all visualizations to - fetch and render the data. If the time is exceeded, a - page screenshot is captured showing the current state, and the download link shows a warning message. Defaults to - `30000` (30 seconds). + | Specify the {ref}/common-options.html#time-units[time] to allow the Reporting browser to wait for all visualizations to + fetch and render the data. If the time is exceeded, a screenshot is captured showing the current page, and the download link shows a + warning message. Can be specified as number of milliseconds. + Defaults to `30s`. |=== @@ -163,11 +162,10 @@ available, but there will likely be errors in the visualizations in the report. job, as many times as this setting. Defaults to `3`. | `xpack.reporting.capture.loadDelay` - | When visualizations are not evented, this is the amount of time before - taking a screenshot. All visualizations that ship with {kib} are evented, so this - setting should not have much effect. If you are seeing empty images instead of - visualizations, try increasing this value. - Defaults to `3000` (3 seconds). + | Specify the {ref}/common-options.html#time-units[amount of time] before taking a screenshot when visualizations are not evented. + All visualizations that ship with {kib} are evented, so this setting should not have much effect. If you are seeing empty images + instead of visualizations, try increasing this value. + Defaults to `3s`. | [[xpack-reporting-browser]] `xpack.reporting.capture.browser.type` {ess-icon} | Specifies the browser to use to capture screenshots. This setting exists for @@ -213,9 +211,9 @@ a| `xpack.reporting.capture.browser` [cols="2*<"] |=== | [[xpack-reporting-csv]] `xpack.reporting.csv.maxSizeBytes` {ess-icon} - | The maximum size of a CSV file before being truncated. This setting exists to prevent - large exports from causing performance and storage issues. - Defaults to `10485760` (10mB). + | The maximum {ref}/common-options.html#byte-units[byte size] of a CSV file before being truncated. This setting exists to + prevent large exports from causing performance and storage issues. Can be specified as number of bytes. + Defaults to `10mb`. | `xpack.reporting.csv.scroll.size` | Number of documents retrieved from {es} for each scroll iteration during a CSV @@ -223,7 +221,7 @@ a| `xpack.reporting.capture.browser` Defaults to `500`. | `xpack.reporting.csv.scroll.duration` - | Amount of time allowed before {kib} cleans the scroll context during a CSV export. + | Amount of {ref}/common-options.html#time-units[time] allowed before {kib} cleans the scroll context during a CSV export. Defaults to `30s`. | `xpack.reporting.csv.checkForFormulas` diff --git a/docs/user/alerting/action-types/server-log.asciidoc b/docs/user/alerting/action-types/server-log.asciidoc index eadca229bc19c..7022320328c85 100644 --- a/docs/user/alerting/action-types/server-log.asciidoc +++ b/docs/user/alerting/action-types/server-log.asciidoc @@ -2,7 +2,7 @@ [[server-log-action-type]] === Server log action -This action type writes and entry to the {kib} server log. +This action type writes an entry to the {kib} server log. [float] [[server-log-connector-configuration]] diff --git a/docs/user/dashboard/drilldowns.asciidoc b/docs/user/dashboard/drilldowns.asciidoc index 85230f1b6f70d..e3d0e16630c5c 100644 --- a/docs/user/dashboard/drilldowns.asciidoc +++ b/docs/user/dashboard/drilldowns.asciidoc @@ -1,3 +1,4 @@ +[role="xpack"] [[drilldowns]] == Use drilldowns for dashboard actions diff --git a/docs/user/dashboard/url-drilldown.asciidoc b/docs/user/dashboard/url-drilldown.asciidoc index 16f82477756b7..e6daf89d72718 100644 --- a/docs/user/dashboard/url-drilldown.asciidoc +++ b/docs/user/dashboard/url-drilldown.asciidoc @@ -1,6 +1,8 @@ [[url-drilldown]] === URL drilldown +beta[] + The URL drilldown allows you to navigate from a dashboard to an internal or external URL. The destination URL can be dynamic, depending on the dashboard context or user’s interaction with a visualization. @@ -197,6 +199,7 @@ context.panel.timeRange.indexPatternIds | ID of saved object behind a panel. | *Single click* + | event.value | Value behind clicked data point. @@ -208,6 +211,22 @@ context.panel.timeRange.indexPatternIds | event.negate | Boolean, indicating whether clicked data point resulted in negative filter. +| +| event.points +| Some visualizations have clickable points that emit more than one data point. Use list of data points in case a single value is insufficient. + + +Example: + +`{{json event.points}}` + +`{{event.points.[0].key}}` + +`{{event.points.[0].value}}` +`{{#each event.points}}key=value&{{/each}}` + +Note: + +`{{event.value}}` is a shorthand for `{{event.points.[0].value}}` + +`{{event.key}}` is a shorthand for `{{event.points.[0].key}}` + | *Range selection* | event.from + event.to diff --git a/docs/user/ml/index.asciidoc b/docs/user/ml/index.asciidoc index 214dae2b96e04..8255585aae411 100644 --- a/docs/user/ml/index.asciidoc +++ b/docs/user/ml/index.asciidoc @@ -20,15 +20,11 @@ image::user/ml/images/ml-data-visualizer-sample.jpg[{data-viz} for sample flight experimental[] You can also upload a CSV, NDJSON, or log file. The *{data-viz}* identifies the file format and field mappings. You can then optionally import that data into an {es} index. To change the default file size limit, see -<>. +<>. -You need the following permissions to use the {data-viz} with file upload: - -* cluster privileges: `monitor`, `manage_ingest_pipelines` -* index privileges: `read`, `manage`, `index` - -For more information, see {ref}/security-privileges.html[Security privileges] -and {ml-docs}/setup.html[Set up {ml-features}]. +If {stack-security-features} are enabled, users must have the necessary +privileges to use {ml-features}. Refer to +{ml-docs}/setup.html#setup-privileges[Set up {ml-features}]. -- diff --git a/docs/user/security/authorization/index.asciidoc b/docs/user/security/authorization/index.asciidoc index 93d6d4249119f..44ca96e4aece5 100644 --- a/docs/user/security/authorization/index.asciidoc +++ b/docs/user/security/authorization/index.asciidoc @@ -28,7 +28,9 @@ Use the **Privilege** menu to grant access to features. The default is **Custom* When using the **Customize by feature** option, you can choose either **All**, **Read** or **None** for access to each feature. As new features are added to Kibana, roles that use the custom option do not automatically get access to the new features. You must manually update the roles. -NOTE: Machine Learning and Stack Monitoring rely on built-in roles to grant access. When a user is assigned the appropriate roles, the Machine Learning and Stack Monitoring application are available; otherwise, these applications are not visible. +NOTE: *{stack-monitor-app}* relies on built-in roles to grant access. When a +user is assigned the appropriate roles, the *{stack-monitor-app}* application is +available; otherwise, it is not visible. To apply your changes, click **Create space privilege**. The space privilege shows up under the Kibana privileges section of the role. diff --git a/kibana.d.ts b/kibana.d.ts index 517bda374af9d..b707405ffbeaf 100644 --- a/kibana.d.ts +++ b/kibana.d.ts @@ -28,7 +28,6 @@ export { Public, Server }; /** * All exports from TS ambient definitions (where types are added for JS source in a .d.ts file). */ -import * as LegacyElasticsearch from './src/legacy/core_plugins/elasticsearch'; import * as LegacyKibanaPluginSpec from './src/legacy/plugin_discovery/plugin_spec/plugin_spec_options'; import * as LegacyKibanaServer from './src/legacy/server/kbn_server'; @@ -44,13 +43,4 @@ export namespace Legacy { export type InitPluginFunction = LegacyKibanaPluginSpec.InitPluginFunction; export type UiExports = LegacyKibanaPluginSpec.UiExports; export type PluginSpecOptions = LegacyKibanaPluginSpec.PluginSpecOptions; - - export namespace Plugins { - export namespace elasticsearch { - export type Plugin = LegacyElasticsearch.ElasticsearchPlugin; - export type Cluster = LegacyElasticsearch.Cluster; - export type ClusterConfig = LegacyElasticsearch.ClusterConfig; - export type CallClusterOptions = LegacyElasticsearch.CallClusterOptions; - } - } } diff --git a/package.json b/package.json index 5008bc0bcebc5..c88fbc0e5fd07 100644 --- a/package.json +++ b/package.json @@ -78,27 +78,18 @@ }, "resolutions": { "**/@types/node": ">=10.17.17 <10.20.0", - "**/@types/react": "^16.9.36", - "**/@types/hapi": "^17.0.18", - "**/@types/angular": "^1.6.56", - "**/@types/hoist-non-react-statics": "^3.3.1", - "**/@types/chai": "^4.2.11", - "**/cypress/@types/lodash": "^4.14.159", - "**/cypress/lodash": "^4.17.20", - "**/typescript": "4.0.2", + "**/cross-fetch/node-fetch": "^2.6.1", + "**/deepmerge": "^4.2.2", + "**/fast-deep-equal": "^3.1.1", "**/graphql-toolkit/lodash": "^4.17.15", "**/hoist-non-react-statics": "^3.3.2", - "**/isomorphic-git/**/base64-js": "^1.2.1", + "**/isomorphic-fetch/node-fetch": "^2.6.1", "**/istanbul-instrumenter-loader/schema-utils": "1.0.0", - "**/image-diff/gm/debug": "^2.6.9", "**/load-grunt-config/lodash": "^4.17.20", + "**/minimist": "^1.2.5", "**/node-jose/node-forge": "^0.10.0", - "**/react-dom": "^16.12.0", - "**/react": "^16.12.0", - "**/react-test-renderer": "^16.12.0", "**/request": "^2.88.2", - "**/deepmerge": "^4.2.2", - "**/fast-deep-equal": "^3.1.1" + "**/typescript": "4.0.2" }, "workspaces": { "packages": [ @@ -128,7 +119,7 @@ "@babel/register": "^7.10.5", "@elastic/datemath": "5.0.3", "@elastic/elasticsearch": "7.9.0-rc.2", - "@elastic/eui": "28.2.0", + "@elastic/eui": "28.4.0", "@elastic/good": "8.1.1-kibana2", "@elastic/numeral": "^2.5.0", "@elastic/request-crypto": "1.1.4", @@ -194,7 +185,7 @@ "moment": "^2.24.0", "moment-timezone": "^0.5.27", "mustache": "2.3.2", - "node-fetch": "1.7.3", + "node-fetch": "2.6.1", "node-forge": "^0.10.0", "opn": "^5.5.0", "oppsy": "^2.0.0", @@ -489,7 +480,7 @@ "zlib": "^1.0.5" }, "engines": { - "node": "10.22.0", + "node": "10.22.1", "yarn": "^1.21.1" } } diff --git a/packages/kbn-es/package.json b/packages/kbn-es/package.json index dabf11fdd0b66..52ef3fe05e751 100644 --- a/packages/kbn-es/package.json +++ b/packages/kbn-es/package.json @@ -14,7 +14,7 @@ "execa": "^4.0.2", "getopts": "^2.2.4", "glob": "^7.1.2", - "node-fetch": "^2.6.0", + "node-fetch": "^2.6.1", "simple-git": "^1.91.0", "tar-fs": "^2.1.0", "tree-kill": "^1.2.2", diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index bbe7b1bc2e8da..8095e05e8b855 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@elastic/charts": "21.1.2", - "@elastic/eui": "28.2.0", + "@elastic/eui": "28.4.0", "@elastic/numeral": "^2.5.0", "@kbn/i18n": "1.0.0", "@kbn/monaco": "1.0.0", diff --git a/src/cli/cluster/cluster_manager.ts b/src/cli/cluster/cluster_manager.ts index b1d1335eb1888..78472bb3f517d 100644 --- a/src/cli/cluster/cluster_manager.ts +++ b/src/cli/cluster/cluster_manager.ts @@ -224,7 +224,6 @@ export class ClusterManager { new Set( [ fromRoot('src/core'), - fromRoot('src/legacy/core_plugins'), fromRoot('src/legacy/server'), fromRoot('src/legacy/ui'), fromRoot('src/legacy/utils'), diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index eeb5564667ec4..d8bd39b9dcdf4 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -178,7 +178,7 @@ export default function (program) { 'A path to scan for plugins, this can be specified multiple ' + 'times to specify multiple directories', pluginDirCollector, - [fromRoot('plugins'), fromRoot('src/legacy/core_plugins')] + [fromRoot('plugins')] ) .option( '--plugin-path ', diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap index 1bff6cd9301ed..699fe2c4c4d4d 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -658,6 +658,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` aria-controls="mockId" aria-expanded={true} className="euiAccordion__button euiAccordion__buttonReverse euiCollapsibleNavGroup__heading" + id="mockId" onClick={[Function]} type="button" > @@ -911,6 +912,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` aria-controls="mockId" aria-expanded={true} className="euiAccordion__button euiAccordion__buttonReverse euiCollapsibleNavGroup__heading" + id="mockId" onClick={[Function]} type="button" > @@ -1200,6 +1202,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` aria-controls="mockId" aria-expanded={true} className="euiAccordion__button euiAccordion__buttonReverse euiCollapsibleNavGroup__heading" + id="mockId" onClick={[Function]} type="button" > @@ -1450,6 +1453,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` aria-controls="mockId" aria-expanded={true} className="euiAccordion__button euiAccordion__buttonReverse euiCollapsibleNavGroup__heading" + id="mockId" onClick={[Function]} type="button" > @@ -1652,6 +1656,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` aria-controls="mockId" aria-expanded={true} className="euiAccordion__button euiAccordion__buttonReverse euiCollapsibleNavGroup__heading" + id="mockId" onClick={[Function]} type="button" > @@ -2875,6 +2880,7 @@ exports[`CollapsibleNav renders the default nav 3`] = ` aria-controls="mockId" aria-expanded={true} className="euiAccordion__button euiAccordion__buttonReverse euiCollapsibleNavGroup__heading" + id="mockId" onClick={[Function]} type="button" > diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap index d90d0824a1237..b5c1f3ca843e4 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap @@ -5734,6 +5734,7 @@ exports[`Header renders 1`] = ` aria-controls="mockId" aria-expanded={true} className="euiAccordion__button euiAccordion__buttonReverse euiCollapsibleNavGroup__heading" + id="mockId" onClick={[Function]} type="button" > diff --git a/src/core/server/context/context_service.mock.ts b/src/core/server/context/context_service.mock.ts index a8d895acad624..24e0d52100bb1 100644 --- a/src/core/server/context/context_service.mock.ts +++ b/src/core/server/context/context_service.mock.ts @@ -21,9 +21,9 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; import { ContextService, ContextSetup } from './context_service'; import { contextMock } from '../../utils/context.mock'; -const createSetupContractMock = () => { +const createSetupContractMock = (mockContext = {}) => { const setupContract: jest.Mocked = { - createContextContainer: jest.fn().mockImplementation(() => contextMock.create()), + createContextContainer: jest.fn().mockImplementation(() => contextMock.create(mockContext)), }; return setupContract; }; diff --git a/src/core/server/elasticsearch/version_check/ensure_es_version.ts b/src/core/server/elasticsearch/version_check/ensure_es_version.ts index 5f926215d167f..70ff8857117de 100644 --- a/src/core/server/elasticsearch/version_check/ensure_es_version.ts +++ b/src/core/server/elasticsearch/version_check/ensure_es_version.ts @@ -72,7 +72,7 @@ export function mapNodesVersionCompatibility( kibanaVersion: string, ignoreVersionMismatch: boolean ): NodesVersionCompatibility { - if (Object.keys(nodesInfo.nodes).length === 0) { + if (Object.keys(nodesInfo.nodes ?? {}).length === 0) { return { isCompatible: false, message: 'Unable to retrieve version information from Elasticsearch nodes.', diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 01797d073ae2e..24d1fc9d369f2 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -60,7 +60,7 @@ import { SavedObjectsServiceStart, } from './saved_objects'; import { CapabilitiesSetup, CapabilitiesStart } from './capabilities'; -import { MetricsServiceStart } from './metrics'; +import { MetricsServiceSetup, MetricsServiceStart } from './metrics'; import { StatusServiceSetup } from './status'; import { Auditor, AuditTrailSetup, AuditTrailStart } from './audit_trail'; import { AppenderConfigType, appendersSchema, LoggingServiceSetup } from './logging'; @@ -320,6 +320,7 @@ export { OpsServerMetrics, OpsProcessMetrics, MetricsServiceSetup, + MetricsServiceStart, } from './metrics'; export { DEFAULT_APP_CATEGORIES } from '../utils'; @@ -414,6 +415,8 @@ export interface CoreSetup = KbnServer as any; @@ -99,6 +100,7 @@ beforeEach(() => { status: statusServiceMock.createInternalSetupContract(), auditTrail: auditTrailServiceMock.createSetupContract(), logging: loggingServiceMock.createInternalSetupContract(), + metrics: metricsServiceMock.createInternalSetupContract(), }, plugins: { 'plugin-id': 'plugin-value' }, uiPlugins: { diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index fd3e3a694e6ae..4dc22be2a9971 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -302,6 +302,10 @@ export class LegacyService implements CoreService { logging: { configure: (config$) => setupDeps.core.logging.configure([], config$), }, + metrics: { + collectionInterval: setupDeps.core.metrics.collectionInterval, + getOpsMetrics$: setupDeps.core.metrics.getOpsMetrics$, + }, savedObjects: { setClientFactoryProvider: setupDeps.core.savedObjects.setClientFactoryProvider, addClientWrapper: setupDeps.core.savedObjects.addClientWrapper, @@ -309,6 +313,7 @@ export class LegacyService implements CoreService { getImportExportObjectLimit: setupDeps.core.savedObjects.getImportExportObjectLimit, }, status: { + isStatusPageAnonymous: setupDeps.core.status.isStatusPageAnonymous, core$: setupDeps.core.status.core$, overall$: setupDeps.core.status.overall$, set: () => { diff --git a/src/core/server/metrics/metrics_service.mock.ts b/src/core/server/metrics/metrics_service.mock.ts index caa7acc001db3..0d9e9af39317c 100644 --- a/src/core/server/metrics/metrics_service.mock.ts +++ b/src/core/server/metrics/metrics_service.mock.ts @@ -78,8 +78,8 @@ type MetricsServiceContract = PublicMethodsOf; const createMock = () => { const mocked: jest.Mocked = { - setup: jest.fn().mockReturnValue(createInternalSetupContractMock()), - start: jest.fn().mockReturnValue(createInternalStartContractMock()), + setup: jest.fn().mockReturnValue(createSetupContractMock()), + start: jest.fn().mockReturnValue(createStartContractMock()), stop: jest.fn(), }; return mocked; diff --git a/src/core/server/metrics/metrics_service.test.ts b/src/core/server/metrics/metrics_service.test.ts index 269931d0e33ad..384a56c8dba94 100644 --- a/src/core/server/metrics/metrics_service.test.ts +++ b/src/core/server/metrics/metrics_service.test.ts @@ -106,6 +106,25 @@ describe('MetricsService', () => { `"#setup() needs to be run first"` ); }); + + it('emits the last value on each getOpsMetrics$ call', async () => { + const firstMetrics = { metric: 'first' }; + const secondMetrics = { metric: 'second' }; + mockOpsCollector.collect + .mockResolvedValueOnce(firstMetrics) + .mockResolvedValueOnce(secondMetrics); + + await metricsService.setup({ http: httpMock }); + const { getOpsMetrics$ } = await metricsService.start(); + + const firstEmission = getOpsMetrics$().pipe(take(1)).toPromise(); + jest.advanceTimersByTime(testInterval); + expect(await firstEmission).toEqual({ metric: 'first' }); + + const secondEmission = getOpsMetrics$().pipe(take(1)).toPromise(); + jest.advanceTimersByTime(testInterval); + expect(await secondEmission).toEqual({ metric: 'second' }); + }); }); describe('#stop', () => { diff --git a/src/core/server/metrics/metrics_service.ts b/src/core/server/metrics/metrics_service.ts index d4696b3aa9aaf..ab58a75d49a98 100644 --- a/src/core/server/metrics/metrics_service.ts +++ b/src/core/server/metrics/metrics_service.ts @@ -37,7 +37,7 @@ export class MetricsService private readonly logger: Logger; private metricsCollector?: OpsMetricsCollector; private collectInterval?: NodeJS.Timeout; - private metrics$ = new ReplaySubject(); + private metrics$ = new ReplaySubject(1); private service?: InternalMetricsServiceSetup; constructor(private readonly coreContext: CoreContext) { diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 5d6bf41fec3f3..7e001ffe28100 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -18,6 +18,7 @@ */ import { of } from 'rxjs'; import { duration } from 'moment'; +import { ByteSizeValue } from '@kbn/config-schema'; import { PluginInitializerContext, CoreSetup, CoreStart, StartServicesAccessor } from '.'; import { loggingSystemMock } from './logging/logging_system.mock'; import { loggingServiceMock } from './logging/logging_service.mock'; @@ -51,6 +52,8 @@ export { typeRegistryMock as savedObjectsTypeRegistryMock } from './saved_object export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; export { metricsServiceMock } from './metrics/metrics_service.mock'; export { renderingMock } from './rendering/rendering_service.mock'; +export { statusServiceMock } from './status/status_service.mock'; +export { contextServiceMock } from './context/context_service.mock'; export function pluginInitializerContextConfigMock(config: T) { const globalConfig: SharedGlobalConfig = { @@ -66,6 +69,9 @@ export function pluginInitializerContextConfigMock(config: T) { startupTimeout: duration('30s'), }, path: { data: '/tmp' }, + savedObjects: { + maxImportPayloadBytes: new ByteSizeValue(10485760), + }, }; const mock: jest.Mocked['config']> = { @@ -133,6 +139,7 @@ function createCoreSetupMock({ uiSettings: uiSettingsMock, auditTrail: auditTrailServiceMock.createSetupContract(), logging: loggingServiceMock.createSetupContract(), + metrics: metricsServiceMock.createSetupContract(), getStartServices: jest .fn, object, any]>, []>() .mockResolvedValue([createCoreStartMock(), pluginStartDeps, pluginStartContract]), @@ -169,6 +176,7 @@ function createInternalCoreSetupMock() { uiSettings: uiSettingsServiceMock.createSetupContract(), auditTrail: auditTrailServiceMock.createSetupContract(), logging: loggingServiceMock.createInternalSetupContract(), + metrics: metricsServiceMock.createInternalSetupContract(), }; return setupDeps; } @@ -178,7 +186,7 @@ function createInternalCoreStartMock() { capabilities: capabilitiesServiceMock.createStartContract(), elasticsearch: elasticsearchServiceMock.createInternalStart(), http: httpServiceMock.createInternalStartContract(), - metrics: metricsServiceMock.createStartContract(), + metrics: metricsServiceMock.createInternalStartContract(), savedObjects: savedObjectsServiceMock.createInternalStartContract(), uiSettings: uiSettingsServiceMock.createStartContract(), auditTrail: auditTrailServiceMock.createStartContract(), diff --git a/src/core/server/plugins/plugin_context.test.ts b/src/core/server/plugins/plugin_context.test.ts index 5ce91c9a623dc..cb4e8f20be982 100644 --- a/src/core/server/plugins/plugin_context.test.ts +++ b/src/core/server/plugins/plugin_context.test.ts @@ -28,6 +28,7 @@ import { rawConfigServiceMock, getEnvOptions } from '../config/mocks'; import { PluginManifest } from './types'; import { Server } from '../server'; import { fromRoot } from '../utils'; +import { ByteSizeValue } from '@kbn/config-schema'; const logger = loggingSystemMock.create(); @@ -93,6 +94,7 @@ describe('createPluginInitializerContext', () => { startupTimeout: duration(5, 's'), }, path: { data: fromRoot('data') }, + savedObjects: { maxImportPayloadBytes: new ByteSizeValue(10485760) }, }); }); diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 8d17300965680..ab3f471fd7942 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -35,6 +35,7 @@ import { ElasticsearchConfigType, config as elasticsearchConfig, } from '../elasticsearch/elasticsearch_config'; +import { SavedObjectsConfigType, savedObjectsConfig } from '../saved_objects/saved_objects_config'; import { CoreSetup, CoreStart } from '..'; export interface InstanceInfo { @@ -91,16 +92,18 @@ export function createPluginInitializerContext( * Note: naming not final here, it will be renamed in a near future (https://github.com/elastic/kibana/issues/46240) * @deprecated */ - globalConfig$: combineLatest( + globalConfig$: combineLatest([ coreContext.configService.atPath(kibanaConfig.path), coreContext.configService.atPath(elasticsearchConfig.path), - coreContext.configService.atPath(pathConfig.path) - ).pipe( - map(([kibana, elasticsearch, path]) => + coreContext.configService.atPath(pathConfig.path), + coreContext.configService.atPath(savedObjectsConfig.path), + ]).pipe( + map(([kibana, elasticsearch, path, savedObjects]) => deepFreeze({ kibana: pick(kibana, SharedGlobalConfigKeys.kibana), elasticsearch: pick(elasticsearch, SharedGlobalConfigKeys.elasticsearch), path: pick(path, SharedGlobalConfigKeys.path), + savedObjects: pick(savedObjects, SharedGlobalConfigKeys.savedObjects), }) ) ), @@ -176,6 +179,10 @@ export function createPluginSetupContext( logging: { configure: (config$) => deps.logging.configure(['plugins', plugin.name], config$), }, + metrics: { + collectionInterval: deps.metrics.collectionInterval, + getOpsMetrics$: deps.metrics.getOpsMetrics$, + }, savedObjects: { setClientFactoryProvider: deps.savedObjects.setClientFactoryProvider, addClientWrapper: deps.savedObjects.addClientWrapper, @@ -188,6 +195,7 @@ export function createPluginSetupContext( set: deps.status.plugins.set.bind(null, plugin.name), dependencies$: deps.status.plugins.getDependenciesStatus$(plugin.name), derivedStatus$: deps.status.plugins.getDerivedStatus$(plugin.name), + isStatusPageAnonymous: deps.status.isStatusPageAnonymous, }, uiSettings: { register: deps.uiSettings.register, diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index 34d5e044222eb..9de181124a349 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -26,6 +26,7 @@ import { ConfigPath, EnvironmentMode, PackageInfo, ConfigDeprecationProvider } f import { LoggerFactory } from '../logging'; import { KibanaConfigType } from '../kibana_config'; import { ElasticsearchConfigType } from '../elasticsearch/elasticsearch_config'; +import { SavedObjectsConfigType } from '../saved_objects/saved_objects_config'; import { CoreSetup, CoreStart } from '..'; /** @@ -263,6 +264,7 @@ export const SharedGlobalConfigKeys = { kibana: ['index', 'autocompleteTerminateAfter', 'autocompleteTimeout'] as const, elasticsearch: ['shardTimeout', 'requestTimeout', 'pingTimeout', 'startupTimeout'] as const, path: ['data'] as const, + savedObjects: ['maxImportPayloadBytes'] as const, }; /** @@ -272,6 +274,7 @@ export type SharedGlobalConfig = RecursiveReadonly<{ kibana: Pick; elasticsearch: Pick; path: Pick; + savedObjects: Pick; }>; /** diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index ab26f29dce3af..1dcf8a22e9cfd 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -416,6 +416,8 @@ export interface CoreSetup Observable; } +// @public +export type MetricsServiceStart = MetricsServiceSetup; + // @public @deprecated (undocumented) export type MIGRATION_ASSISTANCE_INDEX_ACTION = 'upgrade' | 'reindex'; @@ -2589,18 +2591,22 @@ export const ServiceStatusLevels: Readonly<{ available: Readonly<{ toString: () => "available"; valueOf: () => 0; + toJSON: () => "available"; }>; degraded: Readonly<{ toString: () => "degraded"; valueOf: () => 1; + toJSON: () => "degraded"; }>; unavailable: Readonly<{ toString: () => "unavailable"; valueOf: () => 2; + toJSON: () => "unavailable"; }>; critical: Readonly<{ toString: () => "critical"; valueOf: () => 3; + toJSON: () => "critical"; }>; }>; @@ -2661,6 +2667,7 @@ export type SharedGlobalConfig = RecursiveReadonly<{ kibana: Pick; elasticsearch: Pick; path: Pick; + savedObjects: Pick; }>; // @public @@ -2675,6 +2682,7 @@ export interface StatusServiceSetup { dependencies$: Observable>; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "StatusSetup" derivedStatus$: Observable; + isStatusPageAnonymous: () => boolean; overall$: Observable; set(status$: Observable): void; } @@ -2745,7 +2753,8 @@ export const validBodyOutput: readonly ["data", "stream"]; // // src/core/server/http/router/response.ts:316:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts // src/core/server/legacy/types.ts:135:16 - (ae-forgotten-export) The symbol "LegacyPluginSpec" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:272:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:272:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:274:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:274:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:277:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts ``` diff --git a/src/core/server/server.ts b/src/core/server/server.ts index c689e2cb70cc9..8502f563cb0c2 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -152,12 +152,15 @@ export class Server { savedObjects: savedObjectsSetup, }); - await this.metrics.setup({ http: httpSetup }); + const metricsSetup = await this.metrics.setup({ http: httpSetup }); const statusSetup = await this.status.setup({ elasticsearch: elasticsearchServiceSetup, pluginDependencies: pluginTree.asNames, savedObjects: savedObjectsSetup, + environment: environmentSetup, + http: httpSetup, + metrics: metricsSetup, }); const renderingSetup = await this.rendering.setup({ @@ -189,6 +192,7 @@ export class Server { httpResources: httpResourcesSetup, auditTrail: auditTrailSetup, logging: loggingSetup, + metrics: metricsSetup, }; const pluginsSetup = await this.plugins.setup(coreSetup); diff --git a/src/core/server/status/legacy_status.test.ts b/src/core/server/status/legacy_status.test.ts new file mode 100644 index 0000000000000..e3e55442cabd2 --- /dev/null +++ b/src/core/server/status/legacy_status.test.ts @@ -0,0 +1,114 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ServiceStatus, ServiceStatusLevels } from './types'; +import { calculateLegacyStatus } from './legacy_status'; + +const available: ServiceStatus = { level: ServiceStatusLevels.available, summary: 'Available' }; +const degraded: ServiceStatus = { + level: ServiceStatusLevels.degraded, + summary: 'This is degraded!', +}; +const unavailable: ServiceStatus = { + level: ServiceStatusLevels.unavailable, + summary: 'This is unavailable!', +}; +const critical: ServiceStatus = { + level: ServiceStatusLevels.critical, + summary: 'This is critical!', +}; + +describe('calculateLegacyStatus', () => { + it('translates the overall status to the legacy format', () => { + const legacyStatus = calculateLegacyStatus({ + overall: available, + core: {} as any, + plugins: {}, + versionWithoutSnapshot: '1.1.1', + }); + + expect(legacyStatus.overall).toEqual({ + state: 'green', + title: 'Green', + nickname: 'Looking good', + icon: 'success', + uiColor: 'secondary', + since: expect.any(String), + }); + }); + + it('combines core and plugins statuses into statuses array in legacy format', () => { + const legacyStatus = calculateLegacyStatus({ + overall: available, + core: { + elasticsearch: degraded, + savedObjects: critical, + }, + plugins: { + a: available, + b: unavailable, + c: degraded, + }, + versionWithoutSnapshot: '1.1.1', + }); + + expect(legacyStatus.statuses).toEqual([ + { + icon: 'warning', + id: 'core:elasticsearch@1.1.1', + message: 'This is degraded!', + since: expect.any(String), + state: 'yellow', + uiColor: 'warning', + }, + { + icon: 'danger', + id: 'core:savedObjects@1.1.1', + message: 'This is critical!', + since: expect.any(String), + state: 'red', + uiColor: 'danger', + }, + { + icon: 'success', + id: 'plugin:a@1.1.1', + message: 'Available', + since: expect.any(String), + state: 'green', + uiColor: 'secondary', + }, + { + icon: 'danger', + id: 'plugin:b@1.1.1', + message: 'This is unavailable!', + since: expect.any(String), + state: 'red', + uiColor: 'danger', + }, + { + icon: 'warning', + id: 'plugin:c@1.1.1', + message: 'This is degraded!', + since: expect.any(String), + state: 'yellow', + uiColor: 'warning', + }, + ]); + }); +}); diff --git a/src/core/server/status/legacy_status.ts b/src/core/server/status/legacy_status.ts new file mode 100644 index 0000000000000..41777ae97c3da --- /dev/null +++ b/src/core/server/status/legacy_status.ts @@ -0,0 +1,158 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { pick } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { deepFreeze } from '@kbn/std'; + +import { ServiceStatusLevels, ServiceStatus, CoreStatus } from './types'; +import { PluginName } from '../plugins'; + +interface Deps { + overall: ServiceStatus; + core: CoreStatus; + plugins: Record; + versionWithoutSnapshot: string; +} + +export interface LegacyStatusInfo { + overall: LegacyStatusOverall; + statuses: StatusComponentHttp[]; +} + +interface LegacyStatusOverall { + state: LegacyStatusState; + title: string; + nickname: string; + uiColor: LegacyStatusUiColor; + /** ISO-8601 date string w/o timezone */ + since: string; + icon?: string; +} + +type LegacyStatusState = 'green' | 'yellow' | 'red'; +type LegacyStatusIcon = 'danger' | 'warning' | 'success'; +type LegacyStatusUiColor = 'secondary' | 'warning' | 'danger'; + +interface LegacyStateAttr { + id: LegacyStatusState; + state: LegacyStatusState; + title: string; + icon: LegacyStatusIcon; + uiColor: LegacyStatusUiColor; + nickname: string; +} + +export const calculateLegacyStatus = ({ + core, + overall, + plugins, + versionWithoutSnapshot, +}: Deps): LegacyStatusInfo => { + const since = new Date().toISOString(); + const overallLegacy: LegacyStatusOverall = { + since, + ...pick(STATUS_LEVEL_LEGACY_ATTRS[overall.level.toString()], [ + 'state', + 'title', + 'nickname', + 'icon', + 'uiColor', + ]), + }; + const coreStatuses = Object.entries(core).map(([serviceName, s]) => + serviceStatusToHttpComponent(`core:${serviceName}@${versionWithoutSnapshot}`, s, since) + ); + const pluginStatuses = Object.entries(plugins).map(([pluginName, s]) => + serviceStatusToHttpComponent(`plugin:${pluginName}@${versionWithoutSnapshot}`, s, since) + ); + + const componentStatuses: StatusComponentHttp[] = [...coreStatuses, ...pluginStatuses]; + + return { + overall: overallLegacy, + statuses: componentStatuses, + }; +}; + +interface StatusComponentHttp { + id: string; + state: LegacyStatusState; + message: string; + uiColor: LegacyStatusUiColor; + icon: string; + since: string; +} + +const serviceStatusToHttpComponent = ( + serviceName: string, + status: ServiceStatus, + since: string +): StatusComponentHttp => ({ + id: serviceName, + message: status.summary, + since, + ...serviceStatusAttrs(status), +}); + +const serviceStatusAttrs = (status: ServiceStatus) => + pick(STATUS_LEVEL_LEGACY_ATTRS[status.level.toString()], ['state', 'icon', 'uiColor']); + +const STATUS_LEVEL_LEGACY_ATTRS = deepFreeze>({ + [ServiceStatusLevels.critical.toString()]: { + id: 'red', + state: 'red', + title: i18n.translate('core.status.redTitle', { + defaultMessage: 'Red', + }), + icon: 'danger', + uiColor: 'danger', + nickname: 'Danger Will Robinson! Danger!', + }, + [ServiceStatusLevels.unavailable.toString()]: { + id: 'red', + state: 'red', + title: i18n.translate('core.status.redTitle', { + defaultMessage: 'Red', + }), + icon: 'danger', + uiColor: 'danger', + nickname: 'Danger Will Robinson! Danger!', + }, + [ServiceStatusLevels.degraded.toString()]: { + id: 'yellow', + state: 'yellow', + title: i18n.translate('core.status.yellowTitle', { + defaultMessage: 'Yellow', + }), + icon: 'warning', + uiColor: 'warning', + nickname: "I'll be back", + }, + [ServiceStatusLevels.available.toString()]: { + id: 'green', + state: 'green', + title: i18n.translate('core.status.greenTitle', { + defaultMessage: 'Green', + }), + icon: 'success', + uiColor: 'secondary', + nickname: 'Looking good', + }, +}); diff --git a/src/legacy/server/status/lib/index.js b/src/core/server/status/routes/index.ts similarity index 92% rename from src/legacy/server/status/lib/index.js rename to src/core/server/status/routes/index.ts index 93db8b2d22561..db2e8daf0b9ac 100644 --- a/src/legacy/server/status/lib/index.js +++ b/src/core/server/status/routes/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { getKibanaInfoForStats } from './get_kibana_info_for_stats'; +export { registerStatusRoute } from './status'; diff --git a/src/core/server/status/routes/integration_tests/status.test.ts b/src/core/server/status/routes/integration_tests/status.test.ts new file mode 100644 index 0000000000000..e0f86342e3a8a --- /dev/null +++ b/src/core/server/status/routes/integration_tests/status.test.ts @@ -0,0 +1,322 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { BehaviorSubject } from 'rxjs'; +import { first } from 'rxjs/operators'; +import supertest from 'supertest'; +import { omit } from 'lodash'; + +import { createCoreContext, createHttpServer } from '../../../http/test_utils'; +import { ContextService } from '../../../context'; +import { metricsServiceMock } from '../../../metrics/metrics_service.mock'; +import { MetricsServiceSetup } from '../../../metrics'; +import { HttpService, InternalHttpServiceSetup } from '../../../http'; + +import { registerStatusRoute } from '../status'; +import { ServiceStatus, ServiceStatusLevels } from '../../types'; +import { statusServiceMock } from '../../status_service.mock'; + +const coreId = Symbol('core'); + +describe('GET /api/status', () => { + let server: HttpService; + let httpSetup: InternalHttpServiceSetup; + let metrics: jest.Mocked; + + const setupServer = async ({ allowAnonymous = true }: { allowAnonymous?: boolean } = {}) => { + const coreContext = createCoreContext({ coreId }); + const contextService = new ContextService(coreContext); + + server = createHttpServer(coreContext); + httpSetup = await server.setup({ + context: contextService.setup({ pluginDependencies: new Map() }), + }); + + metrics = metricsServiceMock.createSetupContract(); + const status = statusServiceMock.createSetupContract(); + const pluginsStatus$ = new BehaviorSubject>({ + a: { level: ServiceStatusLevels.available, summary: 'a is available' }, + b: { level: ServiceStatusLevels.degraded, summary: 'b is degraded' }, + c: { level: ServiceStatusLevels.unavailable, summary: 'c is unavailable' }, + d: { level: ServiceStatusLevels.critical, summary: 'd is critical' }, + }); + + const router = httpSetup.createRouter(''); + registerStatusRoute({ + router, + config: { + allowAnonymous, + packageInfo: { + branch: 'xbranch', + buildNum: 1234, + buildSha: 'xsha', + dist: true, + version: '9.9.9-SNAPSHOT', + }, + serverName: 'xkibana', + uuid: 'xxxx-xxxxx', + }, + metrics, + status: { + overall$: status.overall$, + core$: status.core$, + plugins$: pluginsStatus$, + }, + }); + + // Register dummy auth provider for testing auth + httpSetup.registerAuth((req, res, auth) => { + if (req.headers.authorization === 'let me in') { + return auth.authenticated(); + } else { + return auth.notHandled(); + } + }); + + await server.start(); + }; + + afterEach(async () => { + await server.stop(); + }); + + describe('allowAnonymous: false', () => { + it('rejects requests with no credentials', async () => { + await setupServer({ allowAnonymous: false }); + await supertest(httpSetup.server.listener).get('/api/status').expect(401); + }); + + it('rejects requests with bad credentials', async () => { + await setupServer({ allowAnonymous: false }); + await supertest(httpSetup.server.listener) + .get('/api/status') + .set('Authorization', 'fake creds') + .expect(401); + }); + + it('accepts authenticated requests', async () => { + await setupServer({ allowAnonymous: false }); + await supertest(httpSetup.server.listener) + .get('/api/status') + .set('Authorization', 'let me in') + .expect(200); + }); + }); + + it('returns basic server info & metrics', async () => { + await setupServer(); + const result = await supertest(httpSetup.server.listener).get('/api/status').expect(200); + + expect(result.body.name).toEqual('xkibana'); + expect(result.body.uuid).toEqual('xxxx-xxxxx'); + expect(result.body.version).toEqual({ + number: '9.9.9', + build_hash: 'xsha', + build_number: 1234, + build_snapshot: true, + }); + const metricsMockValue = await metrics.getOpsMetrics$().pipe(first()).toPromise(); + expect(result.body.metrics).toEqual({ + last_updated: expect.any(String), + collection_interval_in_millis: metrics.collectionInterval, + ...omit(metricsMockValue, ['collected_at']), + requests: { + ...metricsMockValue.requests, + status_codes: metricsMockValue.requests.statusCodes, + }, + }); + }); + + describe('legacy status format', () => { + it('returns legacy status format when no query params provided', async () => { + await setupServer(); + const result = await supertest(httpSetup.server.listener).get('/api/status').expect(200); + expect(result.body.status).toEqual({ + overall: { + icon: 'success', + nickname: 'Looking good', + since: expect.any(String), + state: 'green', + title: 'Green', + uiColor: 'secondary', + }, + statuses: [ + { + icon: 'success', + id: 'core:elasticsearch@9.9.9', + message: 'Service is working', + since: expect.any(String), + state: 'green', + uiColor: 'secondary', + }, + { + icon: 'success', + id: 'core:savedObjects@9.9.9', + message: 'Service is working', + since: expect.any(String), + state: 'green', + uiColor: 'secondary', + }, + { + icon: 'success', + id: 'plugin:a@9.9.9', + message: 'a is available', + since: expect.any(String), + state: 'green', + uiColor: 'secondary', + }, + { + icon: 'warning', + id: 'plugin:b@9.9.9', + message: 'b is degraded', + since: expect.any(String), + state: 'yellow', + uiColor: 'warning', + }, + { + icon: 'danger', + id: 'plugin:c@9.9.9', + message: 'c is unavailable', + since: expect.any(String), + state: 'red', + uiColor: 'danger', + }, + { + icon: 'danger', + id: 'plugin:d@9.9.9', + message: 'd is critical', + since: expect.any(String), + state: 'red', + uiColor: 'danger', + }, + ], + }); + }); + + it('returns legacy status format when v8format=false is provided', async () => { + await setupServer(); + const result = await supertest(httpSetup.server.listener) + .get('/api/status?v8format=false') + .expect(200); + expect(result.body.status).toEqual({ + overall: { + icon: 'success', + nickname: 'Looking good', + since: expect.any(String), + state: 'green', + title: 'Green', + uiColor: 'secondary', + }, + statuses: [ + { + icon: 'success', + id: 'core:elasticsearch@9.9.9', + message: 'Service is working', + since: expect.any(String), + state: 'green', + uiColor: 'secondary', + }, + { + icon: 'success', + id: 'core:savedObjects@9.9.9', + message: 'Service is working', + since: expect.any(String), + state: 'green', + uiColor: 'secondary', + }, + { + icon: 'success', + id: 'plugin:a@9.9.9', + message: 'a is available', + since: expect.any(String), + state: 'green', + uiColor: 'secondary', + }, + { + icon: 'warning', + id: 'plugin:b@9.9.9', + message: 'b is degraded', + since: expect.any(String), + state: 'yellow', + uiColor: 'warning', + }, + { + icon: 'danger', + id: 'plugin:c@9.9.9', + message: 'c is unavailable', + since: expect.any(String), + state: 'red', + uiColor: 'danger', + }, + { + icon: 'danger', + id: 'plugin:d@9.9.9', + message: 'd is critical', + since: expect.any(String), + state: 'red', + uiColor: 'danger', + }, + ], + }); + }); + }); + + describe('v8format', () => { + it('returns new status format when v8format=true is provided', async () => { + await setupServer(); + const result = await supertest(httpSetup.server.listener) + .get('/api/status?v8format=true') + .expect(200); + expect(result.body.status).toEqual({ + core: { + elasticsearch: { + level: 'available', + summary: 'Service is working', + }, + savedObjects: { + level: 'available', + summary: 'Service is working', + }, + }, + overall: { + level: 'available', + summary: 'Service is working', + }, + plugins: { + a: { + level: 'available', + summary: 'a is available', + }, + b: { + level: 'degraded', + summary: 'b is degraded', + }, + c: { + level: 'unavailable', + summary: 'c is unavailable', + }, + d: { + level: 'critical', + summary: 'd is critical', + }, + }, + }); + }); + }); +}); diff --git a/src/core/server/status/routes/status.ts b/src/core/server/status/routes/status.ts new file mode 100644 index 0000000000000..da01a44095529 --- /dev/null +++ b/src/core/server/status/routes/status.ts @@ -0,0 +1,177 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Observable, combineLatest, ReplaySubject } from 'rxjs'; +import { first } from 'rxjs/operators'; +import { schema } from '@kbn/config-schema'; + +import { IRouter } from '../../http'; +import { MetricsServiceSetup } from '../../metrics'; +import { ServiceStatus, CoreStatus } from '../types'; +import { PluginName } from '../../plugins'; +import { calculateLegacyStatus, LegacyStatusInfo } from '../legacy_status'; +import { PackageInfo } from '../../config'; + +const SNAPSHOT_POSTFIX = /-SNAPSHOT$/; + +interface Deps { + router: IRouter; + config: { + allowAnonymous: boolean; + packageInfo: PackageInfo; + serverName: string; + uuid: string; + }; + metrics: MetricsServiceSetup; + status: { + overall$: Observable; + core$: Observable; + plugins$: Observable>; + }; +} + +interface StatusInfo { + overall: ServiceStatus; + core: CoreStatus; + plugins: Record; +} + +interface StatusHttpBody { + name: string; + uuid: string; + version: { + number: string; + build_hash: string; + build_number: number; + build_snapshot: boolean; + }; + status: StatusInfo | LegacyStatusInfo; + metrics: { + /** ISO-8601 date string w/o timezone */ + last_updated: string; + collection_interval_in_millis: number; + process: { + memory: { + heap: { + total_in_bytes: number; + used_in_bytes: number; + size_limit: number; + }; + resident_set_size_in_bytes: number; + }; + event_loop_delay: number; + pid: number; + uptime_in_millis: number; + }; + os: { + load: Record; + memory: { + total_in_bytes: number; + used_in_bytes: number; + free_in_bytes: number; + }; + uptime_in_millis: number; + platform: string; + platformRelease: string; + }; + response_times: { + max_in_millis: number; + }; + requests: { + total: number; + disconnects: number; + statusCodes: Record; + status_codes: Record; + }; + concurrent_connections: number; + }; +} + +export const registerStatusRoute = ({ router, config, metrics, status }: Deps) => { + // Since the status.plugins$ observable is not subscribed to elsewhere, we need to subscribe it here to eagerly load + // the plugins status when Kibana starts up so this endpoint responds quickly on first boot. + const combinedStatus$ = new ReplaySubject< + [ServiceStatus, CoreStatus, Record>] + >(1); + combineLatest([status.overall$, status.core$, status.plugins$]).subscribe(combinedStatus$); + + router.get( + { + path: '/api/status', + options: { + authRequired: !config.allowAnonymous, + tags: ['api'], // ensures that unauthenticated calls receive a 401 rather than a 302 redirect to login page + }, + validate: { + query: schema.object({ + v8format: schema.boolean({ defaultValue: false }), + }), + }, + }, + async (context, req, res) => { + const { version, buildSha, buildNum } = config.packageInfo; + const versionWithoutSnapshot = version.replace(SNAPSHOT_POSTFIX, ''); + const [overall, core, plugins] = await combinedStatus$.pipe(first()).toPromise(); + + let statusInfo: StatusInfo | LegacyStatusInfo; + if (req.query?.v8format) { + statusInfo = { + overall, + core, + plugins, + }; + } else { + statusInfo = calculateLegacyStatus({ + overall, + core, + plugins, + versionWithoutSnapshot, + }); + } + + const lastMetrics = await metrics.getOpsMetrics$().pipe(first()).toPromise(); + + const body: StatusHttpBody = { + name: config.serverName, + uuid: config.uuid, + version: { + number: versionWithoutSnapshot, + build_hash: buildSha, + build_number: buildNum, + build_snapshot: SNAPSHOT_POSTFIX.test(version), + }, + status: statusInfo, + metrics: { + last_updated: lastMetrics.collected_at.toISOString(), + collection_interval_in_millis: metrics.collectionInterval, + os: lastMetrics.os, + process: lastMetrics.process, + response_times: lastMetrics.response_times, + concurrent_connections: lastMetrics.concurrent_connections, + requests: { + ...lastMetrics.requests, + status_codes: lastMetrics.requests.statusCodes, + }, + }, + }; + + return res.ok({ body }); + } + ); +}; diff --git a/src/core/server/status/status_service.mock.ts b/src/core/server/status/status_service.mock.ts index 930ee2970cf55..0ee2d03229a78 100644 --- a/src/core/server/status/status_service.mock.ts +++ b/src/core/server/status/status_service.mock.ts @@ -43,6 +43,7 @@ const createSetupContractMock = () => { set: jest.fn(), dependencies$: new BehaviorSubject({}), derivedStatus$: new BehaviorSubject(available), + isStatusPageAnonymous: jest.fn().mockReturnValue(false), }; return setupContract; diff --git a/src/core/server/status/status_service.test.ts b/src/core/server/status/status_service.test.ts index dcb1e0a559f5d..afacaff044b6f 100644 --- a/src/core/server/status/status_service.test.ts +++ b/src/core/server/status/status_service.test.ts @@ -24,6 +24,9 @@ import { StatusService } from './status_service'; import { first } from 'rxjs/operators'; import { mockCoreContext } from '../core_context.mock'; import { ServiceStatusLevelSnapshotSerializer } from './test_utils'; +import { environmentServiceMock } from '../environment/environment_service.mock'; +import { httpServiceMock } from '../http/http_service.mock'; +import { metricsServiceMock } from '../metrics/metrics_service.mock'; expect.addSnapshotSerializer(ServiceStatusLevelSnapshotSerializer); @@ -44,18 +47,36 @@ describe('StatusService', () => { summary: 'This is degraded!', }; + type SetupDeps = Parameters[0]; + const setupDeps = (overrides: Partial): SetupDeps => { + return { + elasticsearch: { + status$: of(available), + }, + savedObjects: { + status$: of(available), + }, + pluginDependencies: new Map(), + environment: environmentServiceMock.createSetupContract(), + http: httpServiceMock.createInternalSetupContract(), + metrics: metricsServiceMock.createInternalSetupContract(), + ...overrides, + }; + }; + describe('setup', () => { describe('core$', () => { it('rolls up core status observables into single observable', async () => { - const setup = await service.setup({ - elasticsearch: { - status$: of(available), - }, - savedObjects: { - status$: of(degraded), - }, - pluginDependencies: new Map(), - }); + const setup = await service.setup( + setupDeps({ + elasticsearch: { + status$: of(available), + }, + savedObjects: { + status$: of(degraded), + }, + }) + ); expect(await setup.core$.pipe(first()).toPromise()).toEqual({ elasticsearch: available, savedObjects: degraded, @@ -63,15 +84,16 @@ describe('StatusService', () => { }); it('replays last event', async () => { - const setup = await service.setup({ - elasticsearch: { - status$: of(available), - }, - savedObjects: { - status$: of(degraded), - }, - pluginDependencies: new Map(), - }); + const setup = await service.setup( + setupDeps({ + elasticsearch: { + status$: of(available), + }, + savedObjects: { + status$: of(degraded), + }, + }) + ); const subResult1 = await setup.core$.pipe(first()).toPromise(); const subResult2 = await setup.core$.pipe(first()).toPromise(); const subResult3 = await setup.core$.pipe(first()).toPromise(); @@ -92,15 +114,16 @@ describe('StatusService', () => { it('does not emit duplicate events', async () => { const elasticsearch$ = new BehaviorSubject(available); const savedObjects$ = new BehaviorSubject(degraded); - const setup = await service.setup({ - elasticsearch: { - status$: elasticsearch$, - }, - savedObjects: { - status$: savedObjects$, - }, - pluginDependencies: new Map(), - }); + const setup = await service.setup( + setupDeps({ + elasticsearch: { + status$: elasticsearch$, + }, + savedObjects: { + status$: savedObjects$, + }, + }) + ); const statusUpdates: CoreStatus[] = []; const subscription = setup.core$.subscribe((status) => statusUpdates.push(status)); @@ -155,15 +178,16 @@ describe('StatusService', () => { describe('overall$', () => { it('exposes an overall summary', async () => { - const setup = await service.setup({ - elasticsearch: { - status$: of(degraded), - }, - savedObjects: { - status$: of(degraded), - }, - pluginDependencies: new Map(), - }); + const setup = await service.setup( + setupDeps({ + elasticsearch: { + status$: of(degraded), + }, + savedObjects: { + status$: of(degraded), + }, + }) + ); expect(await setup.overall$.pipe(first()).toPromise()).toMatchObject({ level: ServiceStatusLevels.degraded, summary: '[2] services are degraded', @@ -171,15 +195,16 @@ describe('StatusService', () => { }); it('replays last event', async () => { - const setup = await service.setup({ - elasticsearch: { - status$: of(degraded), - }, - savedObjects: { - status$: of(degraded), - }, - pluginDependencies: new Map(), - }); + const setup = await service.setup( + setupDeps({ + elasticsearch: { + status$: of(degraded), + }, + savedObjects: { + status$: of(degraded), + }, + }) + ); const subResult1 = await setup.overall$.pipe(first()).toPromise(); const subResult2 = await setup.overall$.pipe(first()).toPromise(); const subResult3 = await setup.overall$.pipe(first()).toPromise(); @@ -200,15 +225,16 @@ describe('StatusService', () => { it('does not emit duplicate events', async () => { const elasticsearch$ = new BehaviorSubject(available); const savedObjects$ = new BehaviorSubject(degraded); - const setup = await service.setup({ - elasticsearch: { - status$: elasticsearch$, - }, - savedObjects: { - status$: savedObjects$, - }, - pluginDependencies: new Map(), - }); + const setup = await service.setup( + setupDeps({ + elasticsearch: { + status$: elasticsearch$, + }, + savedObjects: { + status$: savedObjects$, + }, + }) + ); const statusUpdates: ServiceStatus[] = []; const subscription = setup.overall$.subscribe((status) => statusUpdates.push(status)); @@ -256,15 +282,16 @@ describe('StatusService', () => { it('debounces events in quick succession', async () => { const savedObjects$ = new BehaviorSubject(available); - const setup = await service.setup({ - elasticsearch: { - status$: new BehaviorSubject(available), - }, - savedObjects: { - status$: savedObjects$, - }, - pluginDependencies: new Map(), - }); + const setup = await service.setup( + setupDeps({ + elasticsearch: { + status$: new BehaviorSubject(available), + }, + savedObjects: { + status$: savedObjects$, + }, + }) + ); const statusUpdates: ServiceStatus[] = []; const subscription = setup.overall$.subscribe((status) => statusUpdates.push(status)); diff --git a/src/core/server/status/status_service.ts b/src/core/server/status/status_service.ts index 8fe65eddb61d3..9acf93f2f8197 100644 --- a/src/core/server/status/status_service.ts +++ b/src/core/server/status/status_service.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Observable, combineLatest } from 'rxjs'; +import { Observable, combineLatest, Subscription } from 'rxjs'; import { map, distinctUntilChanged, shareReplay, take, debounceTime } from 'rxjs/operators'; import { isDeepStrictEqual } from 'util'; @@ -25,8 +25,12 @@ import { CoreService } from '../../types'; import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { InternalElasticsearchServiceSetup } from '../elasticsearch'; +import { InternalHttpServiceSetup } from '../http'; import { InternalSavedObjectsServiceSetup } from '../saved_objects'; import { PluginName } from '../plugins'; +import { InternalMetricsServiceSetup } from '../metrics'; +import { registerStatusRoute } from './routes'; +import { InternalEnvironmentServiceSetup } from '../environment'; import { config, StatusConfigType } from './status_config'; import { ServiceStatus, CoreStatus, InternalStatusServiceSetup } from './types'; @@ -35,7 +39,10 @@ import { PluginsStatusService } from './plugins_status'; interface SetupDeps { elasticsearch: Pick; + environment: InternalEnvironmentServiceSetup; pluginDependencies: ReadonlyMap; + http: InternalHttpServiceSetup; + metrics: InternalMetricsServiceSetup; savedObjects: Pick; } @@ -44,13 +51,21 @@ export class StatusService implements CoreService { private readonly config$: Observable; private pluginsStatus?: PluginsStatusService; + private overallSubscription?: Subscription; - constructor(coreContext: CoreContext) { + constructor(private readonly coreContext: CoreContext) { this.logger = coreContext.logger.get('status'); this.config$ = coreContext.configService.atPath(config.path); } - public async setup({ elasticsearch, pluginDependencies, savedObjects }: SetupDeps) { + public async setup({ + elasticsearch, + pluginDependencies, + http, + metrics, + savedObjects, + environment, + }: SetupDeps) { const statusConfig = await this.config$.pipe(take(1)).toPromise(); const core$ = this.setupCoreStatus({ elasticsearch, savedObjects }); this.pluginsStatus = new PluginsStatusService({ core$, pluginDependencies }); @@ -73,6 +88,26 @@ export class StatusService implements CoreService { shareReplay(1) ); + // Create an unused subscription to ensure all underlying lazy observables are started. + this.overallSubscription = overall$.subscribe(); + + const router = http.createRouter(''); + registerStatusRoute({ + router, + config: { + allowAnonymous: statusConfig.allowAnonymous, + packageInfo: this.coreContext.env.packageInfo, + serverName: http.getServerInfo().name, + uuid: environment.instanceUuid, + }, + metrics, + status: { + overall$, + plugins$: this.pluginsStatus.getAll$(), + core$, + }, + }); + return { core$, overall$, @@ -87,7 +122,12 @@ export class StatusService implements CoreService { public start() {} - public stop() {} + public stop() { + if (this.overallSubscription) { + this.overallSubscription.unsubscribe(); + this.overallSubscription = undefined; + } + } private setupCoreStatus({ elasticsearch, diff --git a/src/core/server/status/types.ts b/src/core/server/status/types.ts index 9fa33a8c6d40c..8efaede79e9d4 100644 --- a/src/core/server/status/types.ts +++ b/src/core/server/status/types.ts @@ -71,6 +71,9 @@ export const ServiceStatusLevels = deepFreeze({ available: { toString: () => 'available', valueOf: () => 0, + toJSON() { + return this.toString(); + }, }, /** * Some features may not be working. @@ -78,6 +81,9 @@ export const ServiceStatusLevels = deepFreeze({ degraded: { toString: () => 'degraded', valueOf: () => 1, + toJSON() { + return this.toString(); + }, }, /** * The service is unavailable, but other functions that do not depend on this service should work. @@ -85,6 +91,9 @@ export const ServiceStatusLevels = deepFreeze({ unavailable: { toString: () => 'unavailable', valueOf: () => 2, + toJSON() { + return this.toString(); + }, }, /** * Block all user functions and display the status page, reserved for Core services only. @@ -92,6 +101,9 @@ export const ServiceStatusLevels = deepFreeze({ critical: { toString: () => 'critical', valueOf: () => 3, + toJSON() { + return this.toString(); + }, }, }); @@ -217,11 +229,17 @@ export interface StatusServiceSetup { * through the dependency tree */ derivedStatus$: Observable; + + /** + * Whether or not the status HTTP APIs are available to unauthenticated users when an authentication provider is + * present. + */ + isStatusPageAnonymous: () => boolean; } /** @internal */ -export interface InternalStatusServiceSetup extends Pick { - isStatusPageAnonymous: () => boolean; +export interface InternalStatusServiceSetup + extends Pick { // Namespaced under `plugins` key to improve clarity that these are APIs for plugins specifically. plugins: { set(plugin: PluginName, status$: Observable): void; diff --git a/src/core/utils/context.mock.ts b/src/core/utils/context.mock.ts index de844f3f0f07d..273d64ec8f822 100644 --- a/src/core/utils/context.mock.ts +++ b/src/core/utils/context.mock.ts @@ -21,15 +21,13 @@ import { IContextContainer } from './context'; export type ContextContainerMock = jest.Mocked>; -const createContextMock = () => { +const createContextMock = (mockContext = {}) => { const contextMock: ContextContainerMock = { registerContext: jest.fn(), - createHandler: jest.fn((id, handler) => (...args: any[]) => - Promise.resolve(handler({}, ...args)) - ), + createHandler: jest.fn(), }; contextMock.createHandler.mockImplementation((pluginId, handler) => (...args) => - handler({}, ...args) + handler(mockContext, ...args) ); return contextMock; }; diff --git a/src/dev/build/tasks/os_packages/docker_generator/run.ts b/src/dev/build/tasks/os_packages/docker_generator/run.ts index 362c34d416743..19487efe1366c 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/run.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/run.ts @@ -40,7 +40,7 @@ export async function runDockerGenerator( ubi: boolean = false ) { // UBI var config - const baseOSImage = ubi ? 'registry.access.redhat.com/ubi8/ubi-minimal:latest' : 'centos:8'; + const baseOSImage = ubi ? 'docker.elastic.co/ubi8/ubi-minimal:latest' : 'centos:8'; const ubiVersionTag = 'ubi8'; const ubiImageFlavor = ubi ? `-${ubiVersionTag}` : ''; diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index 486c8563c5456..5d31db63773fa 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -25,7 +25,6 @@ export default { '/src/plugins', '/src/legacy/ui', '/src/core', - '/src/legacy/core_plugins', '/src/legacy/server', '/src/cli', '/src/cli_keystore', @@ -51,14 +50,11 @@ export default { 'packages/kbn-ui-framework/src/services/**/*.js', '!packages/kbn-ui-framework/src/services/index.js', '!packages/kbn-ui-framework/src/services/**/*/index.js', - 'src/legacy/core_plugins/**/*.{js,mjs,jsx,ts,tsx}', - '!src/legacy/core_plugins/**/{__test__,__snapshots__}/**/*', ], moduleNameMapper: { '@elastic/eui$': '/node_modules/@elastic/eui/test-env', '@elastic/eui/lib/(.*)?': '/node_modules/@elastic/eui/test-env/$1', '^src/plugins/(.*)': '/src/plugins/$1', - '^plugins/([^/.]*)(.*)': '/src/legacy/core_plugins/$1/public$2', '^uiExports/(.*)': '/src/dev/jest/mocks/file_mock.js', '^test_utils/(.*)': '/src/test_utils/public/$1', '^fixtures/(.*)': '/src/fixtures/$1', diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index e22dc03cf57aa..ba58dcdfa4d58 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -132,11 +132,6 @@ export const REMOVE_EXTENSION = ['packages/kbn-plugin-generator/template/**/*.ej * @type {Array} */ export const TEMPORARILY_IGNORED_PATHS = [ - 'src/legacy/core_plugins/console/public/src/directives/helpExample.txt', - 'src/legacy/core_plugins/console/public/src/sense_editor/theme-sense-dark.js', - 'src/legacy/core_plugins/tile_map/public/__tests__/scaledCircleMarkers.png', - 'src/legacy/core_plugins/tile_map/public/__tests__/shadedCircleMarkers.png', - 'src/legacy/core_plugins/tile_map/public/__tests__/shadedGeohashGrid.png', 'src/fixtures/config_upgrade_from_4.0.0_to_4.0.1-snapshot.json', 'src/core/server/core_app/assets/favicons/android-chrome-192x192.png', 'src/core/server/core_app/assets/favicons/android-chrome-256x256.png', diff --git a/src/legacy/core_plugins/elasticsearch/index.d.ts b/src/legacy/core_plugins/elasticsearch/index.d.ts deleted file mode 100644 index 83e7bb19e57ba..0000000000000 --- a/src/legacy/core_plugins/elasticsearch/index.d.ts +++ /dev/null @@ -1,526 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { - Client as ESClient, - GenericParams, - // root params - BulkIndexDocumentsParams, - ClearScrollParams, - CountParams, - CreateDocumentParams, - DeleteDocumentParams, - DeleteDocumentByQueryParams, - DeleteScriptParams, - DeleteTemplateParams, - ExistsParams, - ExplainParams, - FieldStatsParams, - GetParams, - GetResponse, - GetScriptParams, - GetSourceParams, - GetTemplateParams, - IndexDocumentParams, - InfoParams, - MGetParams, - MSearchParams, - MSearchTemplateParams, - MTermVectorsParams, - PingParams, - PutScriptParams, - PutTemplateParams, - ReindexParams, - ReindexRethrottleParams, - RenderSearchTemplateParams, - ScrollParams, - SearchParams, - SearchShardsParams, - SearchTemplateParams, - SuggestParams, - TermvectorsParams, - UpdateDocumentParams, - UpdateDocumentByQueryParams, - MGetResponse, - MSearchResponse, - SearchResponse, - // cat - CatAliasesParams, - CatAllocationParams, - CatFielddataParams, - CatHealthParams, - CatHelpParams, - CatIndicesParams, - CatCommonParams, - CatRecoveryParams, - CatSegmentsParams, - CatShardsParams, - CatSnapshotsParams, - CatTasksParams, - CatThreadPoolParams, - // cluster - ClusterAllocationExplainParams, - ClusterGetSettingsParams, - ClusterHealthParams, - ClusterPendingTasksParams, - ClusterPutSettingsParams, - ClusterRerouteParams, - ClusterStateParams, - ClusterStatsParams, - // indices - IndicesAnalyzeParams, - IndicesClearCacheParams, - IndicesCloseParams, - IndicesCreateParams, - IndicesDeleteParams, - IndicesDeleteAliasParams, - IndicesDeleteTemplateParams, - IndicesExistsParams, - IndicesExistsAliasParams, - IndicesExistsTemplateParams, - IndicesExistsTypeParams, - IndicesFlushParams, - IndicesFlushSyncedParams, - IndicesForcemergeParams, - IndicesGetParams, - IndicesGetAliasParams, - IndicesGetFieldMappingParams, - IndicesGetMappingParams, - IndicesGetSettingsParams, - IndicesGetTemplateParams, - IndicesGetUpgradeParams, - IndicesOpenParams, - IndicesPutAliasParams, - IndicesPutMappingParams, - IndicesPutSettingsParams, - IndicesPutTemplateParams, - IndicesRecoveryParams, - IndicesRefreshParams, - IndicesRolloverParams, - IndicesSegmentsParams, - IndicesShardStoresParams, - IndicesShrinkParams, - IndicesStatsParams, - IndicesUpdateAliasesParams, - IndicesUpgradeParams, - IndicesValidateQueryParams, - // ingest - IngestDeletePipelineParams, - IngestGetPipelineParams, - IngestPutPipelineParams, - IngestSimulateParams, - // nodes - NodesHotThreadsParams, - NodesInfoParams, - NodesStatsParams, - // snapshot - SnapshotCreateParams, - SnapshotCreateRepositoryParams, - SnapshotDeleteParams, - SnapshotDeleteRepositoryParams, - SnapshotGetParams, - SnapshotGetRepositoryParams, - SnapshotRestoreParams, - SnapshotStatusParams, - SnapshotVerifyRepositoryParams, - // tasks - TasksCancelParams, - TasksGetParams, - TasksListParams, -} from 'elasticsearch'; - -export class Cluster { - public callWithRequest: CallClusterWithRequest; - public callWithInternalUser: CallCluster; - constructor(config: ClusterConfig); -} - -export interface ClusterConfig { - [option: string]: any; -} - -export interface Request { - headers: RequestHeaders; -} - -interface RequestHeaders { - [name: string]: string; -} - -interface AssistantAPIClientParams extends GenericParams { - path: '/_migration/assistance'; - method: 'GET'; -} - -type MIGRATION_ASSISTANCE_INDEX_ACTION = 'upgrade' | 'reindex'; -type MIGRATION_DEPRECATION_LEVEL = 'none' | 'info' | 'warning' | 'critical'; - -export interface AssistanceAPIResponse { - indices: { - [indexName: string]: { - action_required: MIGRATION_ASSISTANCE_INDEX_ACTION; - }; - }; -} - -interface DeprecationAPIClientParams extends GenericParams { - path: '/_migration/deprecations'; - method: 'GET'; -} - -export interface DeprecationInfo { - level: MIGRATION_DEPRECATION_LEVEL; - message: string; - url: string; - details?: string; -} - -export interface IndexSettingsDeprecationInfo { - [indexName: string]: DeprecationInfo[]; -} - -export interface DeprecationAPIResponse { - cluster_settings: DeprecationInfo[]; - ml_settings: DeprecationInfo[]; - node_settings: DeprecationInfo[]; - index_settings: IndexSettingsDeprecationInfo; -} - -export interface CallClusterOptions { - wrap401Errors?: boolean; - signal?: AbortSignal; -} - -export interface CallClusterWithRequest { - /* eslint-disable */ - (request: Request, endpoint: 'bulk', params: BulkIndexDocumentsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'clearScroll', params: ClearScrollParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'count', params: CountParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'create', params: CreateDocumentParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'delete', params: DeleteDocumentParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'deleteByQuery', params: DeleteDocumentByQueryParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'deleteScript', params: DeleteScriptParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'deleteTemplate', params: DeleteTemplateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'exists', params: ExistsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'explain', params: ExplainParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'fieldStats', params: FieldStatsParams, options?: CallClusterOptions): ReturnType; - // Generic types cannot be properly looked up with ReturnType. Hard code these explicitly. - (request: Request, endpoint: 'get', params: GetParams, options?: CallClusterOptions): Promise>; - (request: Request, endpoint: 'getScript', params: GetScriptParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'getSource', params: GetSourceParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'getTemplate', params: GetTemplateParams, options?: CallClusterOptions): ReturnType; - // Generic types cannot be properly looked up with ReturnType. Hard code these explicitly. - (request: Request, endpoint: 'index', params: IndexDocumentParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'info', params: InfoParams, options?: CallClusterOptions): ReturnType; - // Generic types cannot be properly looked up with ReturnType. Hard code these explicitly. - (request: Request, endpoint: 'mget', params: MGetParams, options?: CallClusterOptions): Promise>; - (request: Request, endpoint: 'msearch', params: MSearchParams, options?: CallClusterOptions): Promise>; - (request: Request, endpoint: 'msearchTemplate', params: MSearchTemplateParams, options?: CallClusterOptions): Promise>; - (request: Request, endpoint: 'mtermvectors', params: MTermVectorsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'ping', params: PingParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'putScript', params: PutScriptParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'putTemplate', params: PutTemplateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'reindex', params: ReindexParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'reindexRethrottle', params: ReindexRethrottleParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'renderSearchTemplate', params: RenderSearchTemplateParams, options?: CallClusterOptions): ReturnType; - // Generic types cannot be properly looked up with ReturnType. Hard code these explicitly. - (request: Request, endpoint: 'scroll', params: ScrollParams, options?: CallClusterOptions): Promise>; - (request: Request, endpoint: 'search', params: SearchParams, options?: CallClusterOptions): Promise>; - (request: Request, endpoint: 'searchShards', params: SearchShardsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'searchTemplate', params: SearchTemplateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'suggest', params: SuggestParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'termvectors', params: TermvectorsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'update', params: UpdateDocumentParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'updateByQuery', params: UpdateDocumentByQueryParams, options?: CallClusterOptions): ReturnType; - - // cat namespace - (request: Request, endpoint: 'cat.aliases', params: CatAliasesParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.allocation', params: CatAllocationParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.count', params: CatAllocationParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.fielddata', params: CatFielddataParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.health', params: CatHealthParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.help', params: CatHelpParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.indices', params: CatIndicesParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.master', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.nodeattrs', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.nodes', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.pendingTasks', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.plugins', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.recovery', params: CatRecoveryParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.repositories', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.segments', params: CatSegmentsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.shards', params: CatShardsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.snapshots', params: CatSnapshotsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.tasks', params: CatTasksParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.threadPool', params: CatThreadPoolParams, options?: CallClusterOptions): ReturnType; - - // cluster namespace - (request: Request, endpoint: 'cluster.allocationExplain', params: ClusterAllocationExplainParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cluster.getSettings', params: ClusterGetSettingsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cluster.health', params: ClusterHealthParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cluster.pendingTasks', params: ClusterPendingTasksParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cluster.putSettings', params: ClusterPutSettingsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cluster.reroute', params: ClusterRerouteParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cluster.state', params: ClusterStateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cluster.stats', params: ClusterStatsParams, options?: CallClusterOptions): ReturnType; - - // indices namespace - (request: Request, endpoint: 'indices.analyze', params: IndicesAnalyzeParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.clearCache', params: IndicesClearCacheParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.close', params: IndicesCloseParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.create', params: IndicesCreateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.delete', params: IndicesDeleteParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.deleteAlias', params: IndicesDeleteAliasParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.deleteTemplate', params: IndicesDeleteTemplateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.exists', params: IndicesExistsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.existsAlias', params: IndicesExistsAliasParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.existsTemplate', params: IndicesExistsTemplateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.existsType', params: IndicesExistsTypeParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.flush', params: IndicesFlushParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.flushSynced', params: IndicesFlushSyncedParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.forcemerge', params: IndicesForcemergeParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.get', params: IndicesGetParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.getAlias', params: IndicesGetAliasParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.getFieldMapping', params: IndicesGetFieldMappingParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.getMapping', params: IndicesGetMappingParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.getSettings', params: IndicesGetSettingsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.getTemplate', params: IndicesGetTemplateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.getUpgrade', params: IndicesGetUpgradeParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.open', params: IndicesOpenParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.putAlias', params: IndicesPutAliasParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.putMapping', params: IndicesPutMappingParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.putSettings', params: IndicesPutSettingsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.putTemplate', params: IndicesPutTemplateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.recovery', params: IndicesRecoveryParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.refresh', params: IndicesRefreshParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.rollover', params: IndicesRolloverParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.segments', params: IndicesSegmentsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.shardStores', params: IndicesShardStoresParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.shrink', params: IndicesShrinkParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.stats', params: IndicesStatsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.updateAliases', params: IndicesUpdateAliasesParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.upgrade', params: IndicesUpgradeParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.validateQuery', params: IndicesValidateQueryParams, options?: CallClusterOptions): ReturnType; - - // ingest namepsace - (request: Request, endpoint: 'ingest.deletePipeline', params: IngestDeletePipelineParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'ingest.getPipeline', params: IngestGetPipelineParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'ingest.putPipeline', params: IngestPutPipelineParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'ingest.simulate', params: IngestSimulateParams, options?: CallClusterOptions): ReturnType; - - // nodes namespace - (request: Request, endpoint: 'nodes.hotThreads', params: NodesHotThreadsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'nodes.info', params: NodesInfoParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'nodes.stats', params: NodesStatsParams, options?: CallClusterOptions): ReturnType; - - // snapshot namespace - (request: Request, endpoint: 'snapshot.create', params: SnapshotCreateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'snapshot.createRepository', params: SnapshotCreateRepositoryParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'snapshot.delete', params: SnapshotDeleteParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'snapshot.deleteRepository', params: SnapshotDeleteRepositoryParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'snapshot.get', params: SnapshotGetParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'snapshot.getRepository', params: SnapshotGetRepositoryParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'snapshot.restore', params: SnapshotRestoreParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'snapshot.status', params: SnapshotStatusParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'snapshot.verifyRepository', params: SnapshotVerifyRepositoryParams, options?: CallClusterOptions): ReturnType; - - // tasks namespace - (request: Request, endpoint: 'tasks.cancel', params: TasksCancelParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'tasks.get', params: TasksGetParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'tasks.list', params: TasksListParams, options?: CallClusterOptions): ReturnType; - - // other APIs accessed via transport.request - ( - request: Request, - endpoint: 'transport.request', - clientParams: AssistantAPIClientParams, - options?: {} - ): Promise; - ( - request: Request, - endpoint: 'transport.request', - clientParams: DeprecationAPIClientParams, - options?: {} - ): Promise; - - // Catch-all definition - ( - request: Request, - endpoint: string, - clientParams?: any, - options?: CallClusterOptions - ): Promise; - /* eslint-enable */ -} - -export interface CallCluster { - /* eslint-disable */ - (endpoint: 'bulk', params: BulkIndexDocumentsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'clearScroll', params: ClearScrollParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'count', params: CountParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'create', params: CreateDocumentParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'delete', params: DeleteDocumentParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'deleteByQuery', params: DeleteDocumentByQueryParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'deleteScript', params: DeleteScriptParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'deleteTemplate', params: DeleteTemplateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'exists', params: ExistsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'explain', params: ExplainParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'fieldStats', params: FieldStatsParams, options?: CallClusterOptions): ReturnType; - // Generic types cannot be properly looked up with ReturnType. Hard code these explicitly. - (endpoint: 'get', params: GetParams, options?: CallClusterOptions): Promise>; - (endpoint: 'getScript', params: GetScriptParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'getSource', params: GetSourceParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'getTemplate', params: GetTemplateParams, options?: CallClusterOptions): ReturnType; - // Generic types cannot be properly looked up with ReturnType. Hard code these explicitly. - (endpoint: 'index', params: IndexDocumentParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'info', params: InfoParams, options?: CallClusterOptions): ReturnType; - // Generic types cannot be properly looked up with ReturnType. Hard code these explicitly. - (endpoint: 'mget', params: MGetParams, options?: CallClusterOptions): Promise>; - (endpoint: 'msearch', params: MSearchParams, options?: CallClusterOptions): Promise>; - (endpoint: 'msearchTemplate', params: MSearchTemplateParams, options?: CallClusterOptions): Promise>; - (endpoint: 'mtermvectors', params: MTermVectorsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'ping', params: PingParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'putScript', params: PutScriptParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'putTemplate', params: PutTemplateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'reindex', params: ReindexParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'reindexRethrottle', params: ReindexRethrottleParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'renderSearchTemplate', params: RenderSearchTemplateParams, options?: CallClusterOptions): ReturnType; - // Generic types cannot be properly looked up with ReturnType. Hard code these explicitly. - (endpoint: 'scroll', params: ScrollParams, options?: CallClusterOptions): Promise>; - (endpoint: 'search', params: SearchParams, options?: CallClusterOptions): Promise>; - (endpoint: 'searchShards', params: SearchShardsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'searchTemplate', params: SearchTemplateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'suggest', params: SuggestParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'termvectors', params: TermvectorsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'update', params: UpdateDocumentParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'updateByQuery', params: UpdateDocumentByQueryParams, options?: CallClusterOptions): ReturnType; - - // cat namespace - (endpoint: 'cat.aliases', params: CatAliasesParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.allocation', params: CatAllocationParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.count', params: CatAllocationParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.fielddata', params: CatFielddataParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.health', params: CatHealthParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.help', params: CatHelpParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.indices', params: CatIndicesParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.master', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.nodeattrs', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.nodes', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.pendingTasks', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.plugins', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.recovery', params: CatRecoveryParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.repositories', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.segments', params: CatSegmentsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.shards', params: CatShardsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.snapshots', params: CatSnapshotsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.tasks', params: CatTasksParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.threadPool', params: CatThreadPoolParams, options?: CallClusterOptions): ReturnType; - - // cluster namespace - (endpoint: 'cluster.allocationExplain', params: ClusterAllocationExplainParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cluster.getSettings', params: ClusterGetSettingsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cluster.health', params: ClusterHealthParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cluster.pendingTasks', params: ClusterPendingTasksParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cluster.putSettings', params: ClusterPutSettingsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cluster.reroute', params: ClusterRerouteParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cluster.state', params: ClusterStateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cluster.stats', params: ClusterStatsParams, options?: CallClusterOptions): ReturnType; - - // indices namespace - (endpoint: 'indices.analyze', params: IndicesAnalyzeParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.clearCache', params: IndicesClearCacheParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.close', params: IndicesCloseParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.create', params: IndicesCreateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.delete', params: IndicesDeleteParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.deleteAlias', params: IndicesDeleteAliasParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.deleteTemplate', params: IndicesDeleteTemplateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.exists', params: IndicesExistsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.existsAlias', params: IndicesExistsAliasParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.existsTemplate', params: IndicesExistsTemplateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.existsType', params: IndicesExistsTypeParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.flush', params: IndicesFlushParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.flushSynced', params: IndicesFlushSyncedParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.forcemerge', params: IndicesForcemergeParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.get', params: IndicesGetParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.getAlias', params: IndicesGetAliasParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.getFieldMapping', params: IndicesGetFieldMappingParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.getMapping', params: IndicesGetMappingParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.getSettings', params: IndicesGetSettingsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.getTemplate', params: IndicesGetTemplateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.getUpgrade', params: IndicesGetUpgradeParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.open', params: IndicesOpenParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.putAlias', params: IndicesPutAliasParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.putMapping', params: IndicesPutMappingParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.putSettings', params: IndicesPutSettingsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.putTemplate', params: IndicesPutTemplateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.recovery', params: IndicesRecoveryParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.refresh', params: IndicesRefreshParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.rollover', params: IndicesRolloverParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.segments', params: IndicesSegmentsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.shardStores', params: IndicesShardStoresParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.shrink', params: IndicesShrinkParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.stats', params: IndicesStatsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.updateAliases', params: IndicesUpdateAliasesParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.upgrade', params: IndicesUpgradeParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.validateQuery', params: IndicesValidateQueryParams, options?: CallClusterOptions): ReturnType; - - // ingest namespace - (endpoint: 'ingest.deletePipeline', params: IngestDeletePipelineParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'ingest.getPipeline', params: IngestGetPipelineParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'ingest.putPipeline', params: IngestPutPipelineParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'ingest.simulate', params: IngestSimulateParams, options?: CallClusterOptions): ReturnType; - - // nodes namespace - (endpoint: 'nodes.hotThreads', params: NodesHotThreadsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'nodes.info', params: NodesInfoParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'nodes.stats', params: NodesStatsParams, options?: CallClusterOptions): ReturnType; - - // snapshot namespace - (endpoint: 'snapshot.create', params: SnapshotCreateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'snapshot.createRepository', params: SnapshotCreateRepositoryParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'snapshot.delete', params: SnapshotDeleteParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'snapshot.deleteRepository', params: SnapshotDeleteRepositoryParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'snapshot.get', params: SnapshotGetParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'snapshot.getRepository', params: SnapshotGetRepositoryParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'snapshot.restore', params: SnapshotRestoreParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'snapshot.status', params: SnapshotStatusParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'snapshot.verifyRepository', params: SnapshotVerifyRepositoryParams, options?: CallClusterOptions): ReturnType; - - // tasks namespace - (endpoint: 'tasks.cancel', params: TasksCancelParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'tasks.get', params: TasksGetParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'tasks.list', params: TasksListParams, options?: CallClusterOptions): ReturnType; - - // other APIs accessed via transport.request - (endpoint: 'transport.request', clientParams: AssistantAPIClientParams, options?: {}): Promise< - AssistanceAPIResponse - >; - (endpoint: 'transport.request', clientParams: DeprecationAPIClientParams, options?: {}): Promise< - DeprecationAPIResponse - >; - - // Catch-all definition - (endpoint: string, clientParams?: any, options?: CallClusterOptions): Promise; - /* eslint-enable */ -} - -export interface ElasticsearchPlugin { - status: { on: (status: string, cb: () => void) => void }; - getCluster(name: string): Cluster; -} diff --git a/src/legacy/core_plugins/elasticsearch/index.js b/src/legacy/core_plugins/elasticsearch/index.js deleted file mode 100644 index f90f490d68035..0000000000000 --- a/src/legacy/core_plugins/elasticsearch/index.js +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { Cluster } from './server/lib/cluster'; -import { createProxy } from './server/lib/create_proxy'; - -export default function (kibana) { - return new kibana.Plugin({ - require: [], - - async init(server) { - // All methods that ES plugin exposes are synchronous so we should get the first - // value from all observables here to be able to synchronously return and create - // cluster clients afterwards. - const { client } = server.newPlatform.setup.core.elasticsearch.legacy; - const adminCluster = new Cluster(client); - const dataCluster = new Cluster(client); - - const clusters = new Map(); - server.expose('getCluster', (name) => { - if (name === 'admin') { - return adminCluster; - } - - if (name === 'data') { - return dataCluster; - } - - return clusters.get(name); - }); - - server.events.on('stop', () => { - for (const cluster of clusters.values()) { - cluster.close(); - } - - clusters.clear(); - }); - - createProxy(server); - }, - }); -} diff --git a/src/legacy/core_plugins/elasticsearch/package.json b/src/legacy/core_plugins/elasticsearch/package.json deleted file mode 100644 index b5403e1f13de7..0000000000000 --- a/src/legacy/core_plugins/elasticsearch/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "elasticsearch", - "version": "kibana", - "types": "index.d.ts" -} diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/abortable_request_handler.js b/src/legacy/core_plugins/elasticsearch/server/lib/abortable_request_handler.js deleted file mode 100644 index 0b8786f0c2841..0000000000000 --- a/src/legacy/core_plugins/elasticsearch/server/lib/abortable_request_handler.js +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { AbortController } from 'abortcontroller-polyfill/dist/cjs-ponyfill'; - -/* - * A simple utility for generating a handler that provides a signal to the handler that signals when - * the client has closed the connection on this request. - */ -export function abortableRequestHandler(fn) { - return (req, ...args) => { - const controller = new AbortController(); - req.events.once('disconnect', () => { - controller.abort(); - }); - return fn(controller.signal, req, ...args); - }; -} diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/abortable_request_handler.test.js b/src/legacy/core_plugins/elasticsearch/server/lib/abortable_request_handler.test.js deleted file mode 100644 index d79dd4ae4e449..0000000000000 --- a/src/legacy/core_plugins/elasticsearch/server/lib/abortable_request_handler.test.js +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { AbortSignal } from 'abortcontroller-polyfill/dist/cjs-ponyfill'; -import { abortableRequestHandler } from './abortable_request_handler'; - -describe('abortableRequestHandler', () => { - jest.useFakeTimers(); - - it('should call abort if disconnected', () => { - const eventHandlers = new Map(); - const mockRequest = { - events: { - once: jest.fn((key, fn) => eventHandlers.set(key, fn)), - }, - }; - - const handler = jest.fn(); - const onAborted = jest.fn(); - const abortableHandler = abortableRequestHandler(handler); - abortableHandler(mockRequest); - - const [signal, request] = handler.mock.calls[0]; - - expect(signal instanceof AbortSignal).toBe(true); - expect(request).toBe(mockRequest); - - signal.addEventListener('abort', onAborted); - - // Shouldn't be aborted or call onAborted prior to disconnecting - expect(signal.aborted).toBe(false); - expect(onAborted).not.toBeCalled(); - - expect(eventHandlers.has('disconnect')).toBe(true); - eventHandlers.get('disconnect')(); - jest.runAllTimers(); - - // Should be aborted and call onAborted after disconnecting - expect(signal.aborted).toBe(true); - expect(onAborted).toBeCalled(); - }); -}); diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/cluster.ts b/src/legacy/core_plugins/elasticsearch/server/lib/cluster.ts deleted file mode 100644 index 0e7692f6be755..0000000000000 --- a/src/legacy/core_plugins/elasticsearch/server/lib/cluster.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { Request } from 'hapi'; -import { errors } from 'elasticsearch'; -import { LegacyCallAPIOptions, LegacyClusterClient, FakeRequest } from 'kibana/server'; - -export class Cluster { - public readonly errors = errors; - - constructor(private readonly clusterClient: LegacyClusterClient) {} - - public callWithRequest = async ( - req: Request | FakeRequest, - endpoint: string, - clientParams?: Record, - options?: LegacyCallAPIOptions - ) => { - return await this.clusterClient - .asScoped(req) - .callAsCurrentUser(endpoint, clientParams, options); - }; - - public callWithInternalUser = async ( - endpoint: string, - clientParams?: Record, - options?: LegacyCallAPIOptions - ) => { - return await this.clusterClient.callAsInternalUser(endpoint, clientParams, options); - }; - - public close() { - this.clusterClient.close(); - } -} diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/create_proxy.js b/src/legacy/core_plugins/elasticsearch/server/lib/create_proxy.js deleted file mode 100644 index 7302241c46939..0000000000000 --- a/src/legacy/core_plugins/elasticsearch/server/lib/create_proxy.js +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import Joi from 'joi'; -import { abortableRequestHandler } from './abortable_request_handler'; - -export function createProxy(server) { - const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); - - server.route({ - method: 'POST', - path: '/elasticsearch/_msearch', - config: { - payload: { - parse: 'gunzip', - }, - }, - handler: abortableRequestHandler((signal, req, h) => { - const { query, payload } = req; - return callWithRequest( - req, - 'transport.request', - { - path: '/_msearch', - method: 'POST', - query, - body: payload.toString('utf8'), - }, - { signal } - ).finally((r) => h.response(r)); - }), - }); - - server.route({ - method: 'POST', - path: '/elasticsearch/{index}/_search', - config: { - validate: { - params: Joi.object().keys({ - index: Joi.string().required(), - }), - }, - }, - handler: abortableRequestHandler(async (signal, req) => { - const { query, payload: body } = req; - try { - return await callWithRequest( - req, - 'transport.request', - { - path: `/${encodeURIComponent(req.params.index)}/_search`, - method: 'POST', - query, - body, - }, - { signal } - ); - } catch (error) { - return JSON.parse(error.response); - } - }), - }); -} diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index 8827dc53c5275..3cfda0e0696bb 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -33,7 +33,6 @@ import { import { LegacyConfig, ILegacyInternals } from '../../core/server/legacy'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { UiPlugins } from '../../core/server/plugins'; -import { ElasticsearchPlugin } from '../core_plugins/elasticsearch'; // lot of legacy code was assuming this type only had these two methods export type KibanaConfig = Pick; @@ -41,10 +40,7 @@ export type KibanaConfig = Pick; // Extend the defaults with the plugins and server methods we need. declare module 'hapi' { interface PluginProperties { - elasticsearch: ElasticsearchPlugin; - kibana: any; spaces: any; - // add new plugin types here } interface Server { diff --git a/src/legacy/server/kbn_server.js b/src/legacy/server/kbn_server.js index 24d00abb99a05..107e5f6387833 100644 --- a/src/legacy/server/kbn_server.js +++ b/src/legacy/server/kbn_server.js @@ -28,7 +28,6 @@ import httpMixin from './http'; import { coreMixin } from './core'; import { loggingMixin } from './logging'; import warningsMixin from './warnings'; -import { statusMixin } from './status'; import configCompleteMixin from './config/complete'; import { optimizeMixin } from '../../optimize'; import * as Plugins from './plugins'; @@ -90,7 +89,6 @@ export default class KbnServer { loggingMixin, warningsMixin, - statusMixin, // scan translations dirs, register locale files and initialize i18n engine. i18nMixin, diff --git a/src/legacy/server/plugins/lib/plugin.js b/src/legacy/server/plugins/lib/plugin.js index 2b392d13d595a..48389061199ff 100644 --- a/src/legacy/server/plugins/lib/plugin.js +++ b/src/legacy/server/plugins/lib/plugin.js @@ -79,12 +79,7 @@ export class Plugin { ); } - // Many of the plugins are simply adding static assets to the server and we don't need - // to track their "status". Since plugins must have an init() function to even set its status - // we shouldn't even create a status unless the plugin can use it. if (this.externalInit) { - this.status = kbnServer.status.createForPlugin(this); - server.expose('status', this.status); await this.externalInit(server, options); } }; @@ -93,12 +88,6 @@ export class Plugin { plugin: { register, name: id, version }, options: config.has(configPrefix) ? config.get(configPrefix) : null, }); - - // Only change the plugin status to green if the - // initial status has not been changed - if (this.status && this.status.state === 'uninitialized') { - this.status.green('Ready'); - } } async postInit() { diff --git a/src/legacy/server/status/index.js b/src/legacy/server/status/index.js deleted file mode 100644 index ab7ec471a67ff..0000000000000 --- a/src/legacy/server/status/index.js +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import ServerStatus from './server_status'; -import { Metrics } from './lib/metrics'; -import { registerStatusApi, registerStatsApi } from './routes'; -import Oppsy from 'oppsy'; -import { cloneDeep } from 'lodash'; -import { getOSInfo } from './lib/get_os_info'; - -export function statusMixin(kbnServer, server, config) { - kbnServer.status = new ServerStatus(kbnServer.server); - const { usageCollection } = server.newPlatform.setup.plugins; - - const metrics = new Metrics(config, server); - - const oppsy = new Oppsy(server); - oppsy.on('ops', (event) => { - // Oppsy has a bad race condition that will modify this data before - // we ship it off to the buffer. Let's create our copy first. - event = cloneDeep(event); - // Oppsy used to provide this, but doesn't anymore. Grab it ourselves. - server.listener.getConnections((_, count) => { - event.concurrent_connections = count; - - // captures (performs transforms on) the latest event data and stashes - // the metrics for status/stats API payload - metrics.capture(event).then((data) => { - kbnServer.metrics = data; - }); - }); - }); - oppsy.start(config.get('ops.interval')); - - server.events.on('stop', () => { - oppsy.stop(); - }); - - // init routes - registerStatusApi(kbnServer, server, config); - registerStatsApi(usageCollection, server, config, kbnServer); - - // expore shared functionality - server.decorate('server', 'getOSInfo', getOSInfo); -} diff --git a/src/legacy/server/status/lib/__mocks__/_fs_stubs.js b/src/legacy/server/status/lib/__mocks__/_fs_stubs.js deleted file mode 100644 index 2be6402baa5fe..0000000000000 --- a/src/legacy/server/status/lib/__mocks__/_fs_stubs.js +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export function cGroups(hierarchy) { - if (!hierarchy) { - hierarchy = Math.random().toString(36).substring(7); - } - - const cpuAcctDir = `/sys/fs/cgroup/cpuacct/${hierarchy}`; - const cpuDir = `/sys/fs/cgroup/cpu/${hierarchy}`; - - const cGroupContents = [ - '10:freezer:/', - '9:net_cls,net_prio:/', - '8:pids:/', - '7:blkio:/', - '6:memory:/', - '5:devices:/user.slice', - '4:hugetlb:/', - '3:perf_event:/', - '2:cpu,cpuacct,cpuset:/' + hierarchy, - '1:name=systemd:/user.slice/user-1000.slice/session-2359.scope', - ].join('\n'); - - const cpuStatContents = ['nr_periods 0', 'nr_throttled 10', 'throttled_time 20'].join('\n'); - - return { - hierarchy, - cGroupContents, - cpuStatContents, - cpuAcctDir, - cpuDir, - files: { - '/proc/self/cgroup': cGroupContents, - [`${cpuAcctDir}/cpuacct.usage`]: '357753491408', - [`${cpuDir}/cpu.cfs_period_us`]: '100000', - [`${cpuDir}/cpu.cfs_quota_us`]: '5000', - [`${cpuDir}/cpu.stat`]: cpuStatContents, - }, - }; -} - -class FSError extends Error { - constructor(fileName, code) { - super('Stub File System Stub Error: ' + fileName); - this.code = code; - this.stack = null; - } -} - -let _mockFiles = Object.create({}); - -export const setMockFiles = (mockFiles) => { - _mockFiles = Object.create({}); - if (mockFiles) { - const files = Object.keys(mockFiles); - for (const file of files) { - _mockFiles[file] = mockFiles[file]; - } - } -}; - -export const readFileMock = (fileName, callback) => { - if (_mockFiles.hasOwnProperty(fileName)) { - callback(null, _mockFiles[fileName]); - } else { - const err = new FSError(fileName, 'ENOENT'); - callback(err, null); - } -}; diff --git a/src/legacy/server/status/lib/case_conversion.test.ts b/src/legacy/server/status/lib/case_conversion.test.ts deleted file mode 100644 index a231ee0ba4b0f..0000000000000 --- a/src/legacy/server/status/lib/case_conversion.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { keysToSnakeCaseShallow } from './case_conversion'; - -describe('keysToSnakeCaseShallow', () => { - test("should convert all of an object's keys to snake case", () => { - const data = { - camelCase: 'camel_case', - 'kebab-case': 'kebab_case', - snake_case: 'snake_case', - }; - - const result = keysToSnakeCaseShallow(data); - - expect(result.camel_case).toBe('camel_case'); - expect(result.kebab_case).toBe('kebab_case'); - expect(result.snake_case).toBe('snake_case'); - }); -}); diff --git a/src/legacy/server/status/lib/case_conversion.ts b/src/legacy/server/status/lib/case_conversion.ts deleted file mode 100644 index a3ae15028daeb..0000000000000 --- a/src/legacy/server/status/lib/case_conversion.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { mapKeys, snakeCase } from 'lodash'; - -export function keysToSnakeCaseShallow(object: Record) { - return mapKeys(object, (value, key) => snakeCase(key)); -} diff --git a/src/legacy/server/status/lib/cgroup.js b/src/legacy/server/status/lib/cgroup.js deleted file mode 100644 index 4d21cafbedcaa..0000000000000 --- a/src/legacy/server/status/lib/cgroup.js +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import fs from 'fs'; -import { promisify } from 'bluebird'; -import { join as joinPath } from 'path'; - -// Logic from elasticsearch/core/src/main/java/org/elasticsearch/monitor/os/OsProbe.java - -const CONTROL_GROUP_RE = new RegExp('\\d+:([^:]+):(/.*)'); -const CONTROLLER_SEPARATOR_RE = ','; - -const PROC_SELF_CGROUP_FILE = '/proc/self/cgroup'; -const PROC_CGROUP_CPU_DIR = '/sys/fs/cgroup/cpu'; -const PROC_CGROUP_CPUACCT_DIR = '/sys/fs/cgroup/cpuacct'; - -const GROUP_CPUACCT = 'cpuacct'; -const CPUACCT_USAGE_FILE = 'cpuacct.usage'; - -const GROUP_CPU = 'cpu'; -const CPU_FS_PERIOD_US_FILE = 'cpu.cfs_period_us'; -const CPU_FS_QUOTA_US_FILE = 'cpu.cfs_quota_us'; -const CPU_STATS_FILE = 'cpu.stat'; - -const readFile = promisify(fs.readFile); - -export function readControlGroups() { - return readFile(PROC_SELF_CGROUP_FILE).then((data) => { - const response = {}; - - data - .toString() - .split(/\n/) - .forEach((line) => { - const matches = line.match(CONTROL_GROUP_RE); - - if (matches === null) { - return; - } - - const controllers = matches[1].split(CONTROLLER_SEPARATOR_RE); - controllers.forEach((controller) => { - response[controller] = matches[2]; - }); - }); - - return response; - }); -} - -function fileContentsToInteger(path) { - return readFile(path).then((data) => { - return parseInt(data.toString(), 10); - }); -} - -function readCPUAcctUsage(controlGroup) { - return fileContentsToInteger(joinPath(PROC_CGROUP_CPUACCT_DIR, controlGroup, CPUACCT_USAGE_FILE)); -} - -function readCPUFsPeriod(controlGroup) { - return fileContentsToInteger(joinPath(PROC_CGROUP_CPU_DIR, controlGroup, CPU_FS_PERIOD_US_FILE)); -} - -function readCPUFsQuota(controlGroup) { - return fileContentsToInteger(joinPath(PROC_CGROUP_CPU_DIR, controlGroup, CPU_FS_QUOTA_US_FILE)); -} - -export function readCPUStat(controlGroup) { - return new Promise((resolve, reject) => { - const stat = { - number_of_elapsed_periods: -1, - number_of_times_throttled: -1, - time_throttled_nanos: -1, - }; - - readFile(joinPath(PROC_CGROUP_CPU_DIR, controlGroup, CPU_STATS_FILE)) - .then((data) => { - data - .toString() - .split(/\n/) - .forEach((line) => { - const fields = line.split(/\s+/); - - switch (fields[0]) { - case 'nr_periods': - stat.number_of_elapsed_periods = parseInt(fields[1], 10); - break; - - case 'nr_throttled': - stat.number_of_times_throttled = parseInt(fields[1], 10); - break; - - case 'throttled_time': - stat.time_throttled_nanos = parseInt(fields[1], 10); - break; - } - }); - - resolve(stat); - }) - .catch((err) => { - if (err.code === 'ENOENT') { - return resolve(stat); - } - - reject(err); - }); - }); -} - -export function getAllStats(options = {}) { - return new Promise((resolve, reject) => { - readControlGroups() - .then((groups) => { - const cpuPath = options.cpuPath || groups[GROUP_CPU]; - const cpuAcctPath = options.cpuAcctPath || groups[GROUP_CPUACCT]; - - // prevents undefined cgroup paths - if (!cpuPath || !cpuAcctPath) { - return resolve(null); - } - - return Promise.all([ - readCPUAcctUsage(cpuAcctPath), - readCPUFsPeriod(cpuPath), - readCPUFsQuota(cpuPath), - readCPUStat(cpuPath), - ]) - .then(([cpuAcctUsage, cpuFsPeriod, cpuFsQuota, cpuStat]) => { - resolve({ - cpuacct: { - control_group: cpuAcctPath, - usage_nanos: cpuAcctUsage, - }, - - cpu: { - control_group: cpuPath, - cfs_period_micros: cpuFsPeriod, - cfs_quota_micros: cpuFsQuota, - stat: cpuStat, - }, - }); - }) - .catch(rejectUnlessFileNotFound); - }) - .catch(rejectUnlessFileNotFound); - - function rejectUnlessFileNotFound(err) { - if (err.code === 'ENOENT') { - resolve(null); - } else { - reject(err); - } - } - }); -} diff --git a/src/legacy/server/status/lib/cgroup.test.js b/src/legacy/server/status/lib/cgroup.test.js deleted file mode 100644 index 62feba45d1b3c..0000000000000 --- a/src/legacy/server/status/lib/cgroup.test.js +++ /dev/null @@ -1,224 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -jest.mock('fs', () => ({ - readFile: jest.fn(), -})); - -import fs from 'fs'; -import { cGroups as cGroupsFsStub, setMockFiles, readFileMock } from './__mocks__/_fs_stubs'; -import { getAllStats, readControlGroups, readCPUStat } from './cgroup'; - -describe('Control Group', function () { - const fsStub = cGroupsFsStub(); - - beforeAll(() => { - fs.readFile.mockImplementation(readFileMock); - }); - - afterEach(() => { - setMockFiles(); - }); - - describe('readControlGroups', () => { - it('parses the file', async () => { - setMockFiles({ '/proc/self/cgroup': fsStub.cGroupContents }); - const cGroup = await readControlGroups(); - - expect(cGroup).toEqual({ - freezer: '/', - net_cls: '/', - net_prio: '/', - pids: '/', - blkio: '/', - memory: '/', - devices: '/user.slice', - hugetlb: '/', - perf_event: '/', - cpu: `/${fsStub.hierarchy}`, - cpuacct: `/${fsStub.hierarchy}`, - cpuset: `/${fsStub.hierarchy}`, - 'name=systemd': '/user.slice/user-1000.slice/session-2359.scope', - }); - }); - }); - - describe('readCPUStat', () => { - it('parses the file', async () => { - setMockFiles({ '/sys/fs/cgroup/cpu/fakeGroup/cpu.stat': fsStub.cpuStatContents }); - const cpuStat = await readCPUStat('fakeGroup'); - - expect(cpuStat).toEqual({ - number_of_elapsed_periods: 0, - number_of_times_throttled: 10, - time_throttled_nanos: 20, - }); - }); - - it('returns default stats for missing file', async () => { - setMockFiles(); - const cpuStat = await readCPUStat('fakeGroup'); - - expect(cpuStat).toEqual({ - number_of_elapsed_periods: -1, - number_of_times_throttled: -1, - time_throttled_nanos: -1, - }); - }); - }); - - describe('getAllStats', () => { - it('can override the cpu group path', async () => { - setMockFiles({ - '/proc/self/cgroup': fsStub.cGroupContents, - [`${fsStub.cpuAcctDir}/cpuacct.usage`]: '357753491408', - '/sys/fs/cgroup/cpu/docker/cpu.cfs_period_us': '100000', - '/sys/fs/cgroup/cpu/docker/cpu.cfs_quota_us': '5000', - '/sys/fs/cgroup/cpu/docker/cpu.stat': fsStub.cpuStatContents, - }); - - const stats = await getAllStats({ cpuPath: '/docker' }); - - expect(stats).toEqual({ - cpuacct: { - control_group: `/${fsStub.hierarchy}`, - usage_nanos: 357753491408, - }, - cpu: { - control_group: '/docker', - cfs_period_micros: 100000, - cfs_quota_micros: 5000, - stat: { - number_of_elapsed_periods: 0, - number_of_times_throttled: 10, - time_throttled_nanos: 20, - }, - }, - }); - }); - - it('handles an undefined control group', async () => { - setMockFiles({ - '/proc/self/cgroup': '', - [`${fsStub.cpuAcctDir}/cpuacct.usage`]: '357753491408', - [`${fsStub.cpuDir}/cpu.stat`]: fsStub.cpuStatContents, - [`${fsStub.cpuDir}/cpu.cfs_period_us`]: '100000', - [`${fsStub.cpuDir}/cpu.cfs_quota_us`]: '5000', - }); - - const stats = await getAllStats(); - - expect(stats).toBe(null); - }); - - it('can override the cpuacct group path', async () => { - setMockFiles({ - '/proc/self/cgroup': fsStub.cGroupContents, - '/sys/fs/cgroup/cpuacct/docker/cpuacct.usage': '357753491408', - [`${fsStub.cpuDir}/cpu.cfs_period_us`]: '100000', - [`${fsStub.cpuDir}/cpu.cfs_quota_us`]: '5000', - [`${fsStub.cpuDir}/cpu.stat`]: fsStub.cpuStatContents, - }); - - const stats = await getAllStats({ cpuAcctPath: '/docker' }); - - expect(stats).toEqual({ - cpuacct: { - control_group: '/docker', - usage_nanos: 357753491408, - }, - cpu: { - control_group: `/${fsStub.hierarchy}`, - cfs_period_micros: 100000, - cfs_quota_micros: 5000, - stat: { - number_of_elapsed_periods: 0, - number_of_times_throttled: 10, - time_throttled_nanos: 20, - }, - }, - }); - }); - - it('extracts control group stats', async () => { - setMockFiles(fsStub.files); - const stats = await getAllStats(); - - expect(stats).toEqual({ - cpuacct: { - control_group: `/${fsStub.hierarchy}`, - usage_nanos: 357753491408, - }, - cpu: { - control_group: `/${fsStub.hierarchy}`, - cfs_period_micros: 100000, - cfs_quota_micros: 5000, - stat: { - number_of_elapsed_periods: 0, - number_of_times_throttled: 10, - time_throttled_nanos: 20, - }, - }, - }); - }); - - it('returns null when all files are missing', async () => { - setMockFiles(); - const stats = await getAllStats(); - expect(stats).toBeNull(); - }); - - it('returns null if CPU accounting files are missing', async () => { - setMockFiles({ - '/proc/self/cgroup': fsStub.cGroupContents, - [`${fsStub.cpuDir}/cpu.stat`]: fsStub.cpuStatContents, - }); - const stats = await getAllStats(); - - expect(stats).toBeNull(); - }); - - it('returns -1 stat values if cpuStat file is missing', async () => { - setMockFiles({ - '/proc/self/cgroup': fsStub.cGroupContents, - [`${fsStub.cpuAcctDir}/cpuacct.usage`]: '357753491408', - [`${fsStub.cpuDir}/cpu.cfs_period_us`]: '100000', - [`${fsStub.cpuDir}/cpu.cfs_quota_us`]: '5000', - }); - const stats = await getAllStats(); - - expect(stats).toEqual({ - cpu: { - cfs_period_micros: 100000, - cfs_quota_micros: 5000, - control_group: `/${fsStub.hierarchy}`, - stat: { - number_of_elapsed_periods: -1, - number_of_times_throttled: -1, - time_throttled_nanos: -1, - }, - }, - cpuacct: { - control_group: `/${fsStub.hierarchy}`, - usage_nanos: 357753491408, - }, - }); - }); - }); -}); diff --git a/src/legacy/server/status/lib/get_kibana_info_for_stats.js b/src/legacy/server/status/lib/get_kibana_info_for_stats.js deleted file mode 100644 index 62628a2c40ff9..0000000000000 --- a/src/legacy/server/status/lib/get_kibana_info_for_stats.js +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { get } from 'lodash'; - -const snapshotRegex = /-snapshot/i; - -/** - * This provides a meta data attribute along with Kibana stats. - * - * @param {Object} kbnServer manager of Kibana services - see `src/legacy/server/kbn_server` in Kibana core - * @param {Object} config Server config - * @param {String} host Kibana host - * @return {Object} The object containing a "kibana" field and source instance details. - */ -export function getKibanaInfoForStats(server, kbnServer) { - const config = server.config(); - const status = kbnServer.status.toJSON(); - - return { - uuid: config.get('server.uuid'), - name: config.get('server.name'), - index: config.get('kibana.index'), - host: config.get('server.host'), - locale: config.get('i18n.locale'), - transport_address: `${config.get('server.host')}:${config.get('server.port')}`, - version: kbnServer.version.replace(snapshotRegex, ''), - snapshot: snapshotRegex.test(kbnServer.version), - status: get(status, 'overall.state'), - }; -} diff --git a/src/legacy/server/status/lib/get_os_info.js b/src/legacy/server/status/lib/get_os_info.js deleted file mode 100644 index e3835fec34c88..0000000000000 --- a/src/legacy/server/status/lib/get_os_info.js +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import os from 'os'; -import getos from 'getos'; -import { promisify } from 'util'; - -/** - * Returns an object of OS information/ - */ -export async function getOSInfo() { - const osInfo = { - platform: os.platform(), - // Include the platform name in the release to avoid grouping unrelated platforms together. - // release 1.0 across windows, linux, and darwin don't mean anything useful. - platformRelease: `${os.platform()}-${os.release()}`, - }; - - // Get distribution information for linux - if (os.platform() === 'linux') { - try { - const distro = await promisify(getos)(); - osInfo.distro = distro.dist; - // Include distro name in release for same reason as above. - osInfo.distroRelease = `${distro.dist}-${distro.release}`; - } catch (e) { - // ignore errors - } - } - - return osInfo; -} diff --git a/src/legacy/server/status/lib/get_os_info.test.js b/src/legacy/server/status/lib/get_os_info.test.js deleted file mode 100644 index 11af7e1588090..0000000000000 --- a/src/legacy/server/status/lib/get_os_info.test.js +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -jest.mock('os', () => ({ - platform: jest.fn(), - release: jest.fn(), -})); -jest.mock('getos'); - -import os from 'os'; -import getos from 'getos'; - -import { getOSInfo } from './get_os_info'; - -describe('getOSInfo', () => { - it('returns basic OS info on non-linux', async () => { - os.platform.mockImplementation(() => 'darwin'); - os.release.mockImplementation(() => '1.0.0'); - - const osInfo = await getOSInfo(); - - expect(osInfo).toEqual({ - platform: 'darwin', - platformRelease: 'darwin-1.0.0', - }); - }); - - it('returns basic OS info and distro info on linux', async () => { - os.platform.mockImplementation(() => 'linux'); - os.release.mockImplementation(() => '4.9.93-linuxkit-aufs'); - - // Mock getos response - getos.mockImplementation((cb) => - cb(null, { - os: 'linux', - dist: 'Ubuntu Linux', - codename: 'precise', - release: '12.04', - }) - ); - - const osInfo = await getOSInfo(); - - expect(osInfo).toEqual({ - platform: 'linux', - platformRelease: 'linux-4.9.93-linuxkit-aufs', - // linux distro info - distro: 'Ubuntu Linux', - distroRelease: 'Ubuntu Linux-12.04', - }); - }); -}); diff --git a/src/legacy/server/status/lib/metrics.js b/src/legacy/server/status/lib/metrics.js deleted file mode 100644 index 478bf0829b1aa..0000000000000 --- a/src/legacy/server/status/lib/metrics.js +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import os from 'os'; -import v8 from 'v8'; -import { get, isObject, merge } from 'lodash'; -import { keysToSnakeCaseShallow } from './case_conversion'; -import { getAllStats as cGroupStats } from './cgroup'; -import { getOSInfo } from './get_os_info'; - -const requestDefaults = { - disconnects: 0, - statusCodes: {}, - total: 0, -}; - -export class Metrics { - constructor(config, server) { - this.config = config; - this.server = server; - this.checkCGroupStats = true; - } - - static getStubMetrics() { - return { - process: { - memory: { - heap: {}, - }, - }, - os: { - cpu: {}, - memory: {}, - }, - response_times: {}, - requests: {}, - }; - } - - async capture(hapiEvent) { - const timestamp = new Date().toISOString(); - const event = await this.captureEvent(hapiEvent); - const cgroup = await this.captureCGroupsIfAvailable(); - - const metrics = { - last_updated: timestamp, - collection_interval_in_millis: this.config.get('ops.interval'), - }; - - return merge(metrics, event, cgroup); - } - - async captureEvent(hapiEvent) { - const heapStats = v8.getHeapStatistics(); - const port = this.config.get('server.port'); - const avgInMillis = get(hapiEvent, ['responseTimes', port, 'avg']); // sadly, it's possible for this to be NaN - const maxInMillis = get(hapiEvent, ['responseTimes', port, 'max']); - - return { - process: { - memory: { - heap: { - // https://nodejs.org/docs/latest-v8.x/api/process.html#process_process_memoryusage - total_in_bytes: get(hapiEvent, 'psmem.heapTotal'), - used_in_bytes: get(hapiEvent, 'psmem.heapUsed'), - size_limit: heapStats.heap_size_limit, - }, - resident_set_size_in_bytes: get(hapiEvent, 'psmem.rss'), - }, - event_loop_delay: get(hapiEvent, 'psdelay'), - pid: process.pid, - uptime_in_millis: process.uptime() * 1000, - }, - os: { - load: { - '1m': get(hapiEvent, 'osload.0'), - '5m': get(hapiEvent, 'osload.1'), - '15m': get(hapiEvent, 'osload.2'), - }, - memory: { - total_in_bytes: os.totalmem(), - free_in_bytes: os.freemem(), - used_in_bytes: get(hapiEvent, 'osmem.total') - get(hapiEvent, 'osmem.free'), - }, - uptime_in_millis: os.uptime() * 1000, - ...(await getOSInfo()), - }, - response_times: { - avg_in_millis: isNaN(avgInMillis) ? undefined : avgInMillis, // convert NaN to undefined - max_in_millis: maxInMillis, - }, - requests: { - ...requestDefaults, - ...keysToSnakeCaseShallow(get(hapiEvent, ['requests', port])), - }, - concurrent_connections: hapiEvent.concurrent_connections, - }; - } - - async captureCGroups() { - try { - const cgroup = await cGroupStats({ - cpuPath: this.config.get('ops.cGroupOverrides.cpuPath'), - cpuAcctPath: this.config.get('ops.cGroupOverrides.cpuAcctPath'), - }); - - if (isObject(cgroup)) { - return { - os: { - cgroup, - }, - }; - } - } catch (e) { - this.server.log(['error', 'metrics', 'cgroup'], e); - } - } - - async captureCGroupsIfAvailable() { - if (this.checkCGroupStats === true) { - const cgroup = await this.captureCGroups(); - - if (isObject(cgroup)) { - return cgroup; - } - - this.checkCGroupStats = false; - } - } -} diff --git a/src/legacy/server/status/lib/metrics.test.js b/src/legacy/server/status/lib/metrics.test.js deleted file mode 100644 index cc9c2607a2b59..0000000000000 --- a/src/legacy/server/status/lib/metrics.test.js +++ /dev/null @@ -1,245 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -jest.mock('fs', () => ({ - readFile: jest.fn(), -})); - -jest.mock('os', () => ({ - freemem: jest.fn(), - totalmem: jest.fn(), - uptime: jest.fn(), - platform: jest.fn(), - release: jest.fn(), -})); - -jest.mock('process', () => ({ - uptime: jest.fn(), -})); - -import fs from 'fs'; -import os from 'os'; -import _ from 'lodash'; -import sinon from 'sinon'; -import { cGroups as cGroupsFsStub, setMockFiles, readFileMock } from './__mocks__/_fs_stubs'; -import { Metrics } from './metrics'; - -describe('Metrics', function () { - fs.readFile.mockImplementation(readFileMock); - - const sampleConfig = { - ops: { - interval: 5000, - }, - server: { - port: 5603, - }, - }; - const config = { get: (path) => _.get(sampleConfig, path) }; - - let metrics; - - beforeEach(() => { - const server = { log: sinon.mock() }; - - metrics = new Metrics(config, server); - }); - - afterEach(() => { - setMockFiles(); - }); - - describe('capture', () => { - it('merges all metrics', async () => { - setMockFiles(); - sinon - .stub(metrics, 'captureEvent') - .returns({ a: [{ b: 2 }, { d: 4 }], process: { uptime_ms: 1980 } }); - sinon.stub(metrics, 'captureCGroupsIfAvailable').returns({ a: [{ c: 3 }, { e: 5 }] }); - sinon.stub(Date.prototype, 'toISOString').returns('2017-04-14T18:35:41.534Z'); - - const capturedMetrics = await metrics.capture(); - expect(capturedMetrics).toMatchObject({ - last_updated: '2017-04-14T18:35:41.534Z', - collection_interval_in_millis: 5000, - a: [ - { b: 2, c: 3 }, - { d: 4, e: 5 }, - ], - process: { uptime_ms: 1980 }, - }); - }); - }); - - describe('captureEvent', () => { - it('parses the hapi event', async () => { - sinon.stub(os, 'uptime').returns(12000); - sinon.stub(process, 'uptime').returns(5000); - - os.freemem.mockImplementation(() => 12); - os.totalmem.mockImplementation(() => 24); - - const pidMock = jest.fn(); - pidMock.mockReturnValue(8675309); - Object.defineProperty(process, 'pid', { get: pidMock }); // - - const hapiEvent = { - requests: { 5603: { total: 22, disconnects: 0, statusCodes: { 200: 22 } } }, - responseTimes: { 5603: { avg: 1.8636363636363635, max: 4 } }, - osload: [2.20751953125, 2.02294921875, 1.89794921875], - osmem: { total: 17179869184, free: 102318080 }, - osup: 1008991, - psup: 7.168, - psmem: { rss: 193716224, heapTotal: 168194048, heapUsed: 130553400, external: 1779619 }, - concurrent_connections: 0, - psdelay: 1.6091690063476562, - host: 'blahblah.local', - }; - - expect(await metrics.captureEvent(hapiEvent)).toMatchObject({ - concurrent_connections: 0, - os: { - load: { - '15m': 1.89794921875, - '1m': 2.20751953125, - '5m': 2.02294921875, - }, - memory: { - free_in_bytes: 12, - total_in_bytes: 24, - }, - uptime_in_millis: 12000000, - }, - process: { - memory: { - heap: { - total_in_bytes: 168194048, - used_in_bytes: 130553400, - }, - resident_set_size_in_bytes: 193716224, - }, - pid: 8675309, - }, - requests: { - disconnects: 0, - total: 22, - }, - response_times: { - avg_in_millis: 1.8636363636363635, - max_in_millis: 4, - }, - }); - }); - - it('parses event with missing fields / NaN for responseTimes.avg', async () => { - const hapiEvent = { - requests: { - 5603: { total: 22, disconnects: 0, statusCodes: { 200: 22 } }, - }, - responseTimes: { 5603: { avg: NaN, max: 4 } }, - host: 'blahblah.local', - }; - - expect(await metrics.captureEvent(hapiEvent)).toMatchObject({ - process: { memory: { heap: {} }, pid: 8675309, uptime_in_millis: 5000000 }, - os: { - load: {}, - memory: { free_in_bytes: 12, total_in_bytes: 24 }, - }, - response_times: { max_in_millis: 4 }, - requests: { total: 22, disconnects: 0 }, - }); - }); - }); - - describe('captureCGroups', () => { - afterEach(() => { - setMockFiles(); - }); - - it('returns undefined if cgroups do not exist', async () => { - setMockFiles(); - - const stats = await metrics.captureCGroups(); - - expect(stats).toBe(undefined); - }); - - it('returns cgroups', async () => { - const fsStub = cGroupsFsStub(); - setMockFiles(fsStub.files); - - const capturedMetrics = await metrics.captureCGroups(); - - expect(capturedMetrics).toMatchObject({ - os: { - cgroup: { - cpuacct: { - control_group: `/${fsStub.hierarchy}`, - usage_nanos: 357753491408, - }, - cpu: { - control_group: `/${fsStub.hierarchy}`, - cfs_period_micros: 100000, - cfs_quota_micros: 5000, - stat: { - number_of_elapsed_periods: 0, - number_of_times_throttled: 10, - time_throttled_nanos: 20, - }, - }, - }, - }, - }); - }); - }); - - describe('captureCGroupsIfAvailable', () => { - afterEach(() => { - setMockFiles(); - }); - - it('marks cgroups as unavailable and prevents subsequent calls', async () => { - setMockFiles(); - sinon.spy(metrics, 'captureCGroups'); - - expect(metrics.checkCGroupStats).toBe(true); - - await metrics.captureCGroupsIfAvailable(); - expect(metrics.checkCGroupStats).toBe(false); - - await metrics.captureCGroupsIfAvailable(); - sinon.assert.calledOnce(metrics.captureCGroups); - }); - - it('allows subsequent calls if cgroups are available', async () => { - const fsStub = cGroupsFsStub(); - setMockFiles(fsStub.files); - sinon.spy(metrics, 'captureCGroups'); - - expect(metrics.checkCGroupStats).toBe(true); - - await metrics.captureCGroupsIfAvailable(); - expect(metrics.checkCGroupStats).toBe(true); - - await metrics.captureCGroupsIfAvailable(); - sinon.assert.calledTwice(metrics.captureCGroups); - }); - }); -}); diff --git a/src/legacy/server/status/routes/api/register_stats.js b/src/legacy/server/status/routes/api/register_stats.js deleted file mode 100644 index 2cd780d21f681..0000000000000 --- a/src/legacy/server/status/routes/api/register_stats.js +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import Joi from 'joi'; -import boom from 'boom'; -import { defaultsDeep } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { wrapAuthConfig } from '../../wrap_auth_config'; -import { getKibanaInfoForStats } from '../../lib'; - -const STATS_NOT_READY_MESSAGE = i18n.translate('server.stats.notReadyMessage', { - defaultMessage: 'Stats are not ready yet. Please try again later.', -}); - -/* - * API for Kibana meta info and accumulated operations stats - * Including ?extended in the query string fetches Elasticsearch cluster_uuid and usageCollection data - * - Requests to set isExtended = true - * GET /api/stats?extended=true - * GET /api/stats?extended - * - No value or 'false' is isExtended = false - * - Any other value causes a statusCode 400 response (Bad Request) - * Including ?exclude_usage in the query string excludes the usage stats from the response. Same value semantics as ?extended - */ -export function registerStatsApi(usageCollection, server, config, kbnServer) { - const wrapAuth = wrapAuthConfig(config.get('status.allowAnonymous')); - - const getClusterUuid = async (callCluster) => { - const { cluster_uuid: uuid } = await callCluster('info', { filterPath: 'cluster_uuid' }); - return uuid; - }; - - const getUsage = async (callCluster) => { - const usage = await usageCollection.bulkFetchUsage(callCluster); - return usageCollection.toObject(usage); - }; - - let lastMetrics = null; - /* kibana_stats gets singled out from the collector set as it is used - * for health-checking Kibana and fetch does not rely on fetching data - * from ES */ - server.newPlatform.start.core.metrics.getOpsMetrics$().subscribe((metrics) => { - lastMetrics = { - ...metrics, - timestamp: new Date().toISOString(), - }; - }); - - server.route( - wrapAuth({ - method: 'GET', - path: '/api/stats', - config: { - validate: { - query: Joi.object({ - extended: Joi.string().valid('', 'true', 'false'), - legacy: Joi.string().valid('', 'true', 'false'), - exclude_usage: Joi.string().valid('', 'true', 'false'), - }), - }, - tags: ['api'], - }, - async handler(req) { - const isExtended = req.query.extended !== undefined && req.query.extended !== 'false'; - const isLegacy = req.query.legacy !== undefined && req.query.legacy !== 'false'; - const shouldGetUsage = - req.query.exclude_usage === undefined || req.query.exclude_usage === 'false'; - - let extended; - if (isExtended) { - const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('admin'); - const callCluster = (...args) => callWithRequest(req, ...args); - const collectorsReady = await usageCollection.areAllCollectorsReady(); - - if (shouldGetUsage && !collectorsReady) { - return boom.serverUnavailable(STATS_NOT_READY_MESSAGE); - } - - const usagePromise = shouldGetUsage ? getUsage(callCluster) : Promise.resolve({}); - try { - const [usage, clusterUuid] = await Promise.all([ - usagePromise, - getClusterUuid(callCluster), - ]); - - let modifiedUsage = usage; - if (isLegacy) { - // In an effort to make telemetry more easily augmented, we need to ensure - // we can passthrough the data without every part of the process needing - // to know about the change; however, to support legacy use cases where this - // wasn't true, we need to be backwards compatible with how the legacy data - // looked and support those use cases here. - modifiedUsage = Object.keys(usage).reduce((accum, usageKey) => { - if (usageKey === 'kibana') { - accum = { - ...accum, - ...usage[usageKey], - }; - } else if (usageKey === 'reporting') { - accum = { - ...accum, - xpack: { - ...accum.xpack, - reporting: usage[usageKey], - }, - }; - } else { - // I don't think we need to it this for the above conditions, but do it for most as it will - // match the behavior done in monitoring/bulk_uploader - defaultsDeep(accum, { [usageKey]: usage[usageKey] }); - } - - return accum; - }, {}); - - extended = { - usage: modifiedUsage, - clusterUuid, - }; - } else { - extended = usageCollection.toApiFieldNames({ - usage: modifiedUsage, - clusterUuid, - }); - } - } catch (e) { - throw boom.boomify(e); - } - } - - if (!lastMetrics) { - return boom.serverUnavailable(STATS_NOT_READY_MESSAGE); - } - const kibanaStats = usageCollection.toApiFieldNames({ - ...lastMetrics, - kibana: getKibanaInfoForStats(server, kbnServer), - last_updated: new Date().toISOString(), - collection_interval_in_millis: config.get('ops.interval'), - }); - - return { - ...kibanaStats, - ...extended, - }; - }, - }) - ); -} diff --git a/src/legacy/server/status/routes/api/register_status.js b/src/legacy/server/status/routes/api/register_status.js deleted file mode 100644 index 259a00667810f..0000000000000 --- a/src/legacy/server/status/routes/api/register_status.js +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { wrapAuthConfig } from '../../wrap_auth_config'; - -const matchSnapshot = /-SNAPSHOT$/; - -export function registerStatusApi(kbnServer, server, config) { - const wrapAuth = wrapAuthConfig(config.get('status.allowAnonymous')); - - server.route( - wrapAuth({ - method: 'GET', - path: '/api/status', - config: { - tags: ['api'], - }, - async handler() { - return { - name: config.get('server.name'), - uuid: config.get('server.uuid'), - version: { - number: config.get('pkg.version').replace(matchSnapshot, ''), - build_hash: config.get('pkg.buildSha'), - build_number: config.get('pkg.buildNum'), - build_snapshot: matchSnapshot.test(config.get('pkg.version')), - }, - status: kbnServer.status.toJSON(), - metrics: kbnServer.metrics, - }; - }, - }) - ); -} diff --git a/src/legacy/server/status/samples.js b/src/legacy/server/status/samples.js deleted file mode 100644 index 9c41e29945a77..0000000000000 --- a/src/legacy/server/status/samples.js +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; - -function Samples(max) { - this.vals = {}; - this.max = max || Infinity; - this.length = 0; -} - -Samples.prototype.add = function (sample) { - const vals = this.vals; - const length = (this.length = Math.min(this.length + 1, this.max)); - - _.forOwn(sample, function (val, name) { - if (val == null) val = null; - - if (!vals[name]) vals[name] = new Array(length); - vals[name].unshift([Date.now(), val]); - vals[name].length = length; - }); -}; - -Samples.prototype.toJSON = function () { - return this.vals; -}; - -export default Samples; diff --git a/src/legacy/server/status/server_status.js b/src/legacy/server/status/server_status.js deleted file mode 100644 index 81d07de55faaf..0000000000000 --- a/src/legacy/server/status/server_status.js +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; - -import * as states from './states'; -import Status from './status'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { pkg } from '../../../core/server/utils'; - -export default class ServerStatus { - constructor(server) { - this.server = server; - this._created = {}; - } - - create(id) { - const status = new Status(id, this.server); - this._created[status.id] = status; - return status; - } - - createForPlugin(plugin) { - if (plugin.version === 'kibana') plugin.version = pkg.version; - const status = this.create(`plugin:${plugin.id}@${plugin.version}`); - status.plugin = plugin; - return status; - } - - each(fn) { - const self = this; - _.forOwn(self._created, function (status, i, list) { - if (status.state !== 'disabled') { - fn.call(self, status, i, list); - } - }); - } - - get(id) { - return this._created[id]; - } - - getForPluginId(pluginId) { - return _.find(this._created, (s) => s.plugin && s.plugin.id === pluginId); - } - - getState(id) { - const status = this.get(id); - if (!status) return undefined; - return status.state || 'uninitialized'; - } - - getStateForPluginId(pluginId) { - const status = this.getForPluginId(pluginId); - if (!status) return undefined; - return status.state || 'uninitialized'; - } - - overall() { - const state = Object - // take all created status objects - .values(this._created) - // get the state descriptor for each status - .map((status) => states.get(status.state)) - // reduce to the state with the highest severity, defaulting to green - .reduce((a, b) => (a.severity > b.severity ? a : b), states.get('green')); - - const statuses = _.filter(this._created, { state: state.id }); - const since = _.get(_.sortBy(statuses, 'since'), [0, 'since']); - - return { - state: state.id, - title: state.title, - nickname: _.sample(state.nicknames), - icon: state.icon, - uiColor: states.get(state.id).uiColor, - since: since, - }; - } - - isGreen() { - return this.overall().state === 'green'; - } - - notGreen() { - return !this.isGreen(); - } - - toString() { - const overall = this.overall(); - return `${overall.title} – ${overall.nickname}`; - } - - toJSON() { - return { - overall: this.overall(), - statuses: _.values(this._created), - }; - } -} diff --git a/src/legacy/server/status/server_status.test.js b/src/legacy/server/status/server_status.test.js deleted file mode 100644 index bf94d693b1310..0000000000000 --- a/src/legacy/server/status/server_status.test.js +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { find } from 'lodash'; -import sinon from 'sinon'; - -import * as states from './states'; -import Status from './status'; -import ServerStatus from './server_status'; - -describe('ServerStatus class', function () { - const plugin = { id: 'name', version: '1.2.3' }; - - let server; - let serverStatus; - - beforeEach(function () { - server = { expose: sinon.stub(), logWithMetadata: sinon.stub() }; - serverStatus = new ServerStatus(server); - }); - - describe('#create(id)', () => { - it('should create a new plugin with an id', () => { - const status = serverStatus.create('someid'); - expect(status).toBeInstanceOf(Status); - }); - }); - - describe('#createForPlugin(plugin)', function () { - it('should create a new status by plugin', function () { - const status = serverStatus.createForPlugin(plugin); - expect(status).toBeInstanceOf(Status); - }); - }); - - describe('#get(id)', () => { - it('exposes statuses by their id', () => { - const status = serverStatus.create('statusid'); - expect(serverStatus.get('statusid')).toBe(status); - }); - - it('does not get the status for a plugin', () => { - serverStatus.createForPlugin(plugin); - expect(serverStatus.get(plugin)).toBe(undefined); - }); - }); - - describe('#getForPluginId(plugin)', function () { - it('exposes plugin status for the plugin', function () { - const status = serverStatus.createForPlugin(plugin); - expect(serverStatus.getForPluginId(plugin.id)).toBe(status); - }); - - it('does not get plain statuses by their id', function () { - serverStatus.create('someid'); - expect(serverStatus.getForPluginId('someid')).toBe(undefined); - }); - }); - - describe('#getState(id)', function () { - it('should expose the state of a status by id', function () { - const status = serverStatus.create('someid'); - status.green(); - expect(serverStatus.getState('someid')).toBe('green'); - }); - }); - - describe('#getStateForPluginId(plugin)', function () { - it('should expose the state of a plugin by id', function () { - const status = serverStatus.createForPlugin(plugin); - status.green(); - expect(serverStatus.getStateForPluginId(plugin.id)).toBe('green'); - }); - }); - - describe('#overall()', function () { - it('considers each status to produce a summary', function () { - const status = serverStatus.createForPlugin(plugin); - - expect(serverStatus.overall().state).toBe('uninitialized'); - - const match = function (overall, state) { - expect(overall).toHaveProperty('state', state.id); - expect(overall).toHaveProperty('title', state.title); - expect(overall).toHaveProperty('icon', state.icon); - expect(overall).toHaveProperty('uiColor', state.uiColor); - expect(state.nicknames).toContain(overall.nickname); - }; - - status.green(); - match(serverStatus.overall(), states.get('green')); - - status.yellow(); - match(serverStatus.overall(), states.get('yellow')); - - status.red(); - match(serverStatus.overall(), states.get('red')); - }); - }); - - describe('#toJSON()', function () { - it('serializes to overall status and individuals', function () { - const pluginOne = { id: 'one', version: '1.0.0' }; - const pluginTwo = { id: 'two', version: '2.0.0' }; - const pluginThree = { id: 'three', version: 'kibana' }; - - const service = serverStatus.create('some service'); - const p1 = serverStatus.createForPlugin(pluginOne); - const p2 = serverStatus.createForPlugin(pluginTwo); - const p3 = serverStatus.createForPlugin(pluginThree); - - service.green(); - p1.yellow(); - p2.red(); - - const json = JSON.parse(JSON.stringify(serverStatus)); - expect(json).toHaveProperty('overall'); - expect(json.overall.state).toEqual(serverStatus.overall().state); - expect(json.statuses).toHaveLength(4); - - const out = (status) => find(json.statuses, { id: status.id }); - expect(out(service)).toHaveProperty('state', 'green'); - expect(out(p1)).toHaveProperty('state', 'yellow'); - expect(out(p2)).toHaveProperty('state', 'red'); - expect(out(p3)).toHaveProperty('id'); - expect(out(p3).id).not.toContain('undefined'); - }); - }); -}); diff --git a/src/legacy/server/status/states.js b/src/legacy/server/status/states.js deleted file mode 100644 index 4a34684571c3c..0000000000000 --- a/src/legacy/server/status/states.js +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; -import { i18n } from '@kbn/i18n'; - -export const getAll = () => [ - { - id: 'red', - title: i18n.translate('server.status.redTitle', { - defaultMessage: 'Red', - }), - icon: 'danger', - uiColor: 'danger', - severity: 1000, - nicknames: ['Danger Will Robinson! Danger!'], - }, - { - id: 'uninitialized', - title: i18n.translate('server.status.uninitializedTitle', { - defaultMessage: 'Uninitialized', - }), - icon: 'spinner', - uiColor: 'default', - severity: 900, - nicknames: ['Initializing'], - }, - { - id: 'yellow', - title: i18n.translate('server.status.yellowTitle', { - defaultMessage: 'Yellow', - }), - icon: 'warning', - uiColor: 'warning', - severity: 800, - nicknames: ['S.N.A.F.U', "I'll be back", 'brb'], - }, - { - id: 'green', - title: i18n.translate('server.status.greenTitle', { - defaultMessage: 'Green', - }), - icon: 'success', - uiColor: 'secondary', - severity: 0, - nicknames: ['Looking good'], - }, - { - id: 'disabled', - title: i18n.translate('server.status.disabledTitle', { - defaultMessage: 'Disabled', - }), - severity: -1, - icon: 'toggle-off', - uiColor: 'default', - nicknames: ['Am I even a thing?'], - }, -]; - -export const getAllById = () => _.keyBy(exports.getAll(), 'id'); - -export const defaults = { - icon: 'question', - severity: Infinity, -}; - -export function get(id) { - return exports.getAllById()[id] || _.defaults({ id: id }, exports.defaults); -} diff --git a/src/legacy/server/status/status.js b/src/legacy/server/status/status.js deleted file mode 100644 index 10e94da3ac352..0000000000000 --- a/src/legacy/server/status/status.js +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import * as states from './states'; -import { EventEmitter } from 'events'; - -export default class Status extends EventEmitter { - constructor(id, server) { - super(); - - if (!id || typeof id !== 'string') { - throw new TypeError('Status constructor requires an `id` string'); - } - - this.id = id; - this.since = new Date(); - this.state = 'uninitialized'; - this.message = 'uninitialized'; - - this.on('change', function (previous, previousMsg) { - this.since = new Date(); - - const tags = ['status', this.id, this.state === 'red' ? 'error' : 'info']; - - server.logWithMetadata( - tags, - `Status changed from ${previous} to ${this.state}${ - this.message ? ' - ' + this.message : '' - }`, - { - state: this.state, - message: this.message, - prevState: previous, - prevMsg: previousMsg, - } - ); - }); - } - - toJSON() { - return { - id: this.id, - state: this.state, - icon: states.get(this.state).icon, - message: this.message, - uiColor: states.get(this.state).uiColor, - since: this.since, - }; - } - - on(eventName, handler) { - super.on(eventName, handler); - - if (eventName === this.state) { - setImmediate(() => handler(this.state, this.message)); - } - } - - once(eventName, handler) { - if (eventName === this.state) { - setImmediate(() => handler(this.state, this.message)); - } else { - super.once(eventName, handler); - } - } -} - -states.getAll().forEach(function (state) { - Status.prototype[state.id] = function (message) { - if (this.state === 'disabled') return; - - const previous = this.state; - const previousMsg = this.message; - - this.error = null; - this.message = message || state.title; - this.state = state.id; - - if (message instanceof Error) { - this.error = message; - this.message = message.message; - } - - if (previous === this.state && previousMsg === this.message) { - // noop - return; - } - - this.emit(state.id, previous, previousMsg, this.state, this.message); - this.emit('change', previous, previousMsg, this.state, this.message); - }; -}); diff --git a/src/legacy/server/status/status.test.js b/src/legacy/server/status/status.test.js deleted file mode 100644 index def7b5a2182e1..0000000000000 --- a/src/legacy/server/status/status.test.js +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import sinon from 'sinon'; -import ServerStatus from './server_status'; - -describe('Status class', function () { - const plugin = { id: 'test', version: '1.2.3' }; - - let server; - let serverStatus; - - beforeEach(function () { - server = { expose: sinon.stub(), logWithMetadata: sinon.stub() }; - serverStatus = new ServerStatus(server); - }); - - it('should have an "uninitialized" state initially', () => { - expect(serverStatus.createForPlugin(plugin)).toHaveProperty('state', 'uninitialized'); - }); - - it('emits change when the status is set', function (done) { - const status = serverStatus.createForPlugin(plugin); - - status.once('change', function (prevState, prevMsg, newState, newMsg) { - expect(newState).toBe('green'); - expect(newMsg).toBe('GREEN'); - expect(prevState).toBe('uninitialized'); - - status.once('change', function (prevState, prevMsg, newState, newMsg) { - expect(newState).toBe('red'); - expect(newMsg).toBe('RED'); - expect(prevState).toBe('green'); - expect(prevMsg).toBe('GREEN'); - - done(); - }); - - status.red('RED'); - }); - - status.green('GREEN'); - }); - - it('should only trigger the change listener when something changes', function () { - const status = serverStatus.createForPlugin(plugin); - const stub = sinon.stub(); - status.on('change', stub); - status.green('Ready'); - status.green('Ready'); - status.red('Not Ready'); - sinon.assert.calledTwice(stub); - }); - - it('should create a JSON representation of the status', function () { - const status = serverStatus.createForPlugin(plugin); - status.green('Ready'); - - const json = status.toJSON(); - expect(json.id).toEqual(status.id); - expect(json.state).toEqual('green'); - expect(json.message).toEqual('Ready'); - }); - - it('should call on handler if status is already matched', function (done) { - const status = serverStatus.createForPlugin(plugin); - const msg = 'Test Ready'; - status.green(msg); - - status.on('green', function (prev, prevMsg) { - expect(arguments.length).toBe(2); - expect(prev).toBe('green'); - expect(prevMsg).toBe(msg); - expect(status.message).toBe(msg); - done(); - }); - }); - - it('should call once handler if status is already matched', function (done) { - const status = serverStatus.createForPlugin(plugin); - const msg = 'Test Ready'; - status.green(msg); - - status.once('green', function (prev, prevMsg) { - expect(arguments.length).toBe(2); - expect(prev).toBe('green'); - expect(prevMsg).toBe(msg); - expect(status.message).toBe(msg); - done(); - }); - }); - - function testState(color) { - it(`should change the state to ${color} when #${color}() is called`, function () { - const status = serverStatus.createForPlugin(plugin); - const message = 'testing ' + color; - status[color](message); - expect(status).toHaveProperty('state', color); - expect(status).toHaveProperty('message', message); - }); - - it(`should trigger the "change" listener when #${color}() is called`, function (done) { - const status = serverStatus.createForPlugin(plugin); - const message = 'testing ' + color; - status.on('change', function (prev, prevMsg) { - expect(status.state).toBe(color); - expect(status.message).toBe(message); - - expect(prev).toBe('uninitialized'); - expect(prevMsg).toBe('uninitialized'); - done(); - }); - status[color](message); - }); - - it(`should trigger the "${color}" listener when #${color}() is called`, function (done) { - const status = serverStatus.createForPlugin(plugin); - const message = 'testing ' + color; - status.on(color, function () { - expect(status.state).toBe(color); - expect(status.message).toBe(message); - done(); - }); - status[color](message); - }); - } - - testState('green'); - testState('yellow'); - testState('red'); -}); diff --git a/src/legacy/server/status/wrap_auth_config.js b/src/legacy/server/status/wrap_auth_config.js deleted file mode 100644 index 04e71a02d30de..0000000000000 --- a/src/legacy/server/status/wrap_auth_config.js +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { assign, identity } from 'lodash'; - -export const wrapAuthConfig = (allowAnonymous) => { - if (allowAnonymous) { - return (options) => assign(options, { config: { auth: false } }); - } - return identity; -}; diff --git a/src/legacy/server/status/wrap_auth_config.test.js b/src/legacy/server/status/wrap_auth_config.test.js deleted file mode 100644 index fa0230a96a587..0000000000000 --- a/src/legacy/server/status/wrap_auth_config.test.js +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { wrapAuthConfig } from './wrap_auth_config'; - -describe('Status wrapAuthConfig', () => { - let options; - - beforeEach(() => { - options = { - method: 'GET', - path: '/status', - handler: function (request, h) { - return h.response(); - }, - }; - }); - - it('should return a function', () => { - expect(typeof wrapAuthConfig()).toBe('function'); - expect(typeof wrapAuthConfig(true)).toBe('function'); - expect(typeof wrapAuthConfig(false)).toBe('function'); - }); - - it('should not add auth config by default', () => { - const wrapAuth = wrapAuthConfig(); - const wrapped = wrapAuth(options); - expect(wrapped).not.toHaveProperty('config'); - }); - - it('should not add auth config if allowAnonymous is false', () => { - const wrapAuth = wrapAuthConfig(false); - const wrapped = wrapAuth(options); - expect(wrapped).not.toHaveProperty('config'); - }); - - it('should add auth config if allowAnonymous is true', () => { - const wrapAuth = wrapAuthConfig(true); - const wrapped = wrapAuth(options); - expect(wrapped).toHaveProperty('config'); - expect(wrapped.config).toHaveProperty('auth'); - expect(wrapped.config.auth).toBe(false); - }); -}); diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts index c79c7900148ea..d3b3a73a4b50f 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts @@ -113,15 +113,13 @@ describe('IndexPatterns', () => { test('caches saved objects', async () => { await indexPatterns.getIds(); await indexPatterns.getTitles(); - await indexPatterns.getFields(['id', 'title']); expect(savedObjectsClient.find).toHaveBeenCalledTimes(1); }); test('can refresh the saved objects caches', async () => { await indexPatterns.getIds(); await indexPatterns.getTitles(true); - await indexPatterns.getFields(['id', 'title'], true); - expect(savedObjectsClient.find).toHaveBeenCalledTimes(3); + expect(savedObjectsClient.find).toHaveBeenCalledTimes(2); }); test('deletes the index pattern', async () => { diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index 88a7e9f6cef4c..47484f8ec75bb 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -41,8 +41,6 @@ const indexPatternCache = createIndexPatternCache(); const MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS = 3; const savedObjectType = 'index-pattern'; -type IndexPatternCachedFieldType = 'id' | 'title'; - export interface IndexPatternSavedObjectAttrs { title: string; } @@ -116,22 +114,6 @@ export class IndexPatternsService { return this.savedObjectsCache.map((obj) => obj?.attributes?.title); }; - getFields = async (fields: IndexPatternCachedFieldType[], refresh: boolean = false) => { - if (!this.savedObjectsCache || refresh) { - await this.refreshSavedObjectsCache(); - } - if (!this.savedObjectsCache) { - return []; - } - return this.savedObjectsCache.map((obj: Record) => { - const result: Partial> = {}; - fields.forEach( - (f: IndexPatternCachedFieldType) => (result[f] = obj[f] || obj?.attributes?.[f]) - ); - return result; - }); - }; - getFieldsForTimePattern = (options: GetFieldsOptions = {}) => { return this.apiClient.getFieldsForTimePattern(options); }; diff --git a/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.test.ts b/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.test.ts index fd788d3339295..d3a95b32cd425 100644 --- a/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.test.ts @@ -139,6 +139,19 @@ describe('calculateHistogramInterval', () => { }) ).toEqual(0.02); }); + + test('should correctly fallback to the default value for empty string', () => { + expect( + calculateHistogramInterval({ + ...params, + maxBucketsUserInput: '', + values: { + min: 0.1, + max: 0.9, + }, + }) + ).toBe(0.01); + }); }); }); }); diff --git a/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.ts b/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.ts index f4e42fa8881ef..378340e876296 100644 --- a/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.ts +++ b/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.ts @@ -27,7 +27,7 @@ interface IntervalValuesRange { export interface CalculateHistogramIntervalParams { interval: string; maxBucketsUiSettings: number; - maxBucketsUserInput?: number; + maxBucketsUserInput?: number | ''; intervalBase?: number; values?: IntervalValuesRange; } @@ -77,12 +77,7 @@ const calculateForGivenInterval = ( - The lower power of 10, times 2 - The lower power of 10, times 5 **/ -const calculateAutoInterval = ( - diff: number, - maxBucketsUiSettings: CalculateHistogramIntervalParams['maxBucketsUiSettings'], - maxBucketsUserInput: CalculateHistogramIntervalParams['maxBucketsUserInput'] -) => { - const maxBars = Math.min(maxBucketsUiSettings, maxBucketsUserInput ?? maxBucketsUiSettings); +const calculateAutoInterval = (diff: number, maxBars: number) => { const exactInterval = diff / maxBars; const lowerPower = Math.pow(10, Math.floor(Math.log10(exactInterval))); @@ -122,7 +117,11 @@ export const calculateHistogramInterval = ({ if (diff) { calculatedInterval = isAuto - ? calculateAutoInterval(diff, maxBucketsUiSettings, maxBucketsUserInput) + ? calculateAutoInterval( + diff, + // Mind maxBucketsUserInput can be an empty string, hence we need to ensure it here + Math.min(maxBucketsUiSettings, maxBucketsUserInput || maxBucketsUiSettings) + ) : calculateForGivenInterval(diff, calculatedInterval, maxBucketsUiSettings); } } diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx index f159cac664a9e..8e1151b387fee 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx @@ -546,13 +546,16 @@ export class QueryStringInputUI extends Component { this.updateSuggestions.cancel(); this.componentIsUnmounting = true; window.removeEventListener('resize', this.handleAutoHeight); - window.removeEventListener('scroll', this.handleListUpdate); + window.removeEventListener('scroll', this.handleListUpdate, { capture: true }); } - handleListUpdate = () => - this.setState({ + handleListUpdate = () => { + if (this.componentIsUnmounting) return; + + return this.setState({ queryBarRect: this.queryBarInputDivRefInstance.current?.getBoundingClientRect(), }); + }; handleAutoHeight = () => { if (this.inputRef !== null && document.activeElement === this.inputRef) { diff --git a/src/plugins/discover/public/application/_discover.scss b/src/plugins/discover/public/application/_discover.scss index 69df2a75b8d75..bc704439d161b 100644 --- a/src/plugins/discover/public/application/_discover.scss +++ b/src/plugins/discover/public/application/_discover.scss @@ -5,6 +5,11 @@ overflow: hidden; } +.dscAppContainer { + > * { + position: relative; + } +} discover-app { flex-grow: 1; } @@ -17,9 +22,12 @@ discover-app { // SASSTODO: replace the z-index value with a variable .dscWrapper { + padding-left: $euiSizeXL; padding-right: $euiSizeS; - padding-left: 21px; z-index: 1; + @include euiBreakpoint('xs', 's', 'm') { + padding-left: $euiSizeS; + } } @include euiPanel('.dscWrapper__content'); @@ -104,14 +112,51 @@ discover-app { top: $euiSizeXS; } -[fixed-scroll] { +.dscTableFixedScroll { overflow-x: auto; padding-bottom: 0; - + .fixed-scroll-scroller { + + .dscTableFixedScroll__scroller { position: fixed; bottom: 0; overflow-x: auto; overflow-y: hidden; } } + +.dscCollapsibleSidebar { + position: relative; + z-index: $euiZLevel1; + + .dscCollapsibleSidebar__collapseButton { + position: absolute; + top: 0; + right: -$euiSizeXL + 4; + cursor: pointer; + z-index: -1; + min-height: $euiSizeM; + min-width: $euiSizeM; + padding: $euiSizeXS * .5; + } + + &.closed { + width: 0 !important; + border-right-width: 0; + border-left-width: 0; + .dscCollapsibleSidebar__collapseButton { + right: -$euiSizeL + 4; + } + } +} + +@include euiBreakpoint('xs', 's', 'm') { + .dscCollapsibleSidebar { + &.closed { + display: none; + } + + .dscCollapsibleSidebar__collapseButton { + display: none; + } + } +} diff --git a/src/plugins/discover/public/application/angular/directives/__snapshots__/no_results.test.js.snap b/src/plugins/discover/public/application/angular/directives/__snapshots__/no_results.test.js.snap index e7aea41e2d08e..e69e10e29e801 100644 --- a/src/plugins/discover/public/application/angular/directives/__snapshots__/no_results.test.js.snap +++ b/src/plugins/discover/public/application/angular/directives/__snapshots__/no_results.test.js.snap @@ -167,132 +167,6 @@ Array [ ] `; -exports[`DiscoverNoResults props shardFailures doesn't render failures list when there are no failures 1`] = ` -Array [ -
, -
-
-
-
- -
-
-
, -] -`; - -exports[`DiscoverNoResults props shardFailures renders failures list when there are failures 1`] = ` -Array [ -
, -
-
-
-
- -
-
-
-

- Address shard failures -

-

- The following shard failures occurred: -

-
-
- - Index ‘A’ - - , shard ‘1’ -
-
-
-
-              
-                {"reason":"Awful error"}
-              
-            
-
-
-
-
-
- - Index ‘B’ - - , shard ‘2’ -
-
-
-
-              
-                {"reason":"Bad error"}
-              
-            
-
-
-
-
-
, -] -`; - exports[`DiscoverNoResults props timeFieldName renders time range feedback 1`] = ` Array [
* { - visibility: hidden; - } - - .kbnCollapsibleSidebar__collapseButton { - visibility: visible; - - .chevron-cont:before { - content: "\F138"; - } - } - } -} - -@include euiBreakpoint('xs', 's', 'm') { - .collapsible-sidebar { - &.closed { - display: none; - } - - .kbnCollapsibleSidebar__collapseButton { - display: none; - } - } -} diff --git a/src/plugins/discover/public/application/angular/directives/collapsible_sidebar/_depth.scss b/src/plugins/discover/public/application/angular/directives/collapsible_sidebar/_depth.scss deleted file mode 100644 index 4bc59001f9931..0000000000000 --- a/src/plugins/discover/public/application/angular/directives/collapsible_sidebar/_depth.scss +++ /dev/null @@ -1,13 +0,0 @@ -/** - * 1. The local nav contains tooltips which should pop over the filter bar. - * 2. The filter and local nav components should always appear above the dashboard grid items. - * 3. The filter and local nav components should always appear above the discover content. - * 4. The sidebar collapser button should appear above the main Discover content but below the top elements. - * 5. Dragged panels in dashboard should always appear above other panels. - */ -$kbnFilterBarDepth: 4; /* 1 */ -$kbnLocalNavDepth: 5; /* 1 */ -$kbnDashboardGridDepth: 1; /* 2 */ -$kbnDashboardDraggingGridDepth: 2; /* 5 */ -$kbnDiscoverWrapperDepth: 1; /* 3 */ -$kbnDiscoverSidebarDepth: 2; /* 4 */ diff --git a/src/plugins/discover/public/application/angular/directives/collapsible_sidebar/_index.scss b/src/plugins/discover/public/application/angular/directives/collapsible_sidebar/_index.scss deleted file mode 100644 index 1409920d11aa7..0000000000000 --- a/src/plugins/discover/public/application/angular/directives/collapsible_sidebar/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import 'depth'; -@import 'collapsible_sidebar'; diff --git a/src/plugins/discover/public/application/angular/directives/collapsible_sidebar/collapsible_sidebar.ts b/src/plugins/discover/public/application/angular/directives/collapsible_sidebar/collapsible_sidebar.ts deleted file mode 100644 index 16fbb0af9f3fd..0000000000000 --- a/src/plugins/discover/public/application/angular/directives/collapsible_sidebar/collapsible_sidebar.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; -import $ from 'jquery'; -import { IScope } from 'angular'; - -interface LazyScope extends IScope { - [key: string]: any; -} - -export function CollapsibleSidebarProvider() { - // simply a list of all of all of angulars .col-md-* classes except 12 - const listOfWidthClasses = _.times(11, function (i) { - return 'col-md-' + i; - }); - - return { - restrict: 'C', - link: ($scope: LazyScope, $elem: any) => { - let isCollapsed = false; - const $collapser = $( - `` - ); - // If the collapsable element has an id, also set aria-controls - if ($elem.attr('id')) { - $collapser.attr('aria-controls', $elem.attr('id')); - } - const $icon = $(''); - $collapser.append($icon); - const $siblings = $elem.siblings(); - - const siblingsClass = listOfWidthClasses.reduce((prev: string, className: string) => { - if (prev) return prev; - return $siblings.hasClass(className) && className; - }, ''); - - // If there is are only two elements we can assume the other one will take 100% of the width. - const hasSingleSibling = $siblings.length === 1 && siblingsClass; - - $collapser.on('click', function () { - if (isCollapsed) { - isCollapsed = false; - $elem.removeClass('closed'); - $icon.addClass('fa-chevron-circle-left'); - $icon.removeClass('fa-chevron-circle-right'); - $collapser.attr('aria-expanded', 'true'); - } else { - isCollapsed = true; - $elem.addClass('closed'); - $icon.removeClass('fa-chevron-circle-left'); - $icon.addClass('fa-chevron-circle-right'); - $collapser.attr('aria-expanded', 'false'); - } - - if (hasSingleSibling) { - $siblings.toggleClass(siblingsClass + ' col-md-12'); - } - - if ($scope.toggleSidebar) $scope.toggleSidebar(); - }); - - $collapser.appendTo($elem); - }, - }; -} diff --git a/src/plugins/discover/public/application/angular/directives/debounce/debounce.js b/src/plugins/discover/public/application/angular/directives/debounce/debounce.js index 586e8ed4fab59..8ce2b042c0efe 100644 --- a/src/plugins/discover/public/application/angular/directives/debounce/debounce.js +++ b/src/plugins/discover/public/application/angular/directives/debounce/debounce.js @@ -21,7 +21,7 @@ import _ from 'lodash'; // Debounce service, angularized version of lodash debounce // borrowed heavily from https://github.com/shahata/angular-debounce -export function DebounceProviderTimeout($timeout) { +export function createDebounceProviderTimeout($timeout) { return function (func, wait, options) { let timeout; let args; @@ -66,7 +66,3 @@ export function DebounceProviderTimeout($timeout) { return debounce; }; } - -export function DebounceProvider(debounce) { - return debounce; -} diff --git a/src/plugins/discover/public/application/angular/directives/debounce/debounce.test.ts b/src/plugins/discover/public/application/angular/directives/debounce/debounce.test.ts index ccdee153002e4..0cdc214cf97f5 100644 --- a/src/plugins/discover/public/application/angular/directives/debounce/debounce.test.ts +++ b/src/plugins/discover/public/application/angular/directives/debounce/debounce.test.ts @@ -24,7 +24,7 @@ import 'angular-sanitize'; import 'angular-route'; // @ts-ignore -import { DebounceProvider } from './index'; +import { createDebounceProviderTimeout } from './debounce'; import { coreMock } from '../../../../../../../core/public/mocks'; import { initializeInnerAngularModule } from '../../../../get_inner_angular'; import { navigationPluginMock } from '../../../../../../navigation/public/mocks'; @@ -33,7 +33,6 @@ import { initAngularBootstrap } from '../../../../../../kibana_legacy/public'; describe('debounce service', function () { let debounce: (fn: () => void, timeout: number, options?: any) => any; - let debounceFromProvider: (fn: () => void, timeout: number, options?: any) => any; let $timeout: ITimeoutService; let spy: SinonSpy; @@ -51,22 +50,17 @@ describe('debounce service', function () { angular.mock.module('app/discover'); - angular.mock.inject( - ($injector: auto.IInjectorService, _$timeout_: ITimeoutService, Private: any) => { - $timeout = _$timeout_; + angular.mock.inject(($injector: auto.IInjectorService, _$timeout_: ITimeoutService) => { + $timeout = _$timeout_; - debounce = $injector.get('debounce'); - debounceFromProvider = Private(DebounceProvider); - } - ); + debounce = createDebounceProviderTimeout($timeout); + }); }); it('should have a cancel method', function () { const bouncer = debounce(() => {}, 100); - const bouncerFromProvider = debounceFromProvider(() => {}, 100); expect(bouncer).toHaveProperty('cancel'); - expect(bouncerFromProvider).toHaveProperty('cancel'); }); describe('delayed execution', function () { @@ -77,7 +71,6 @@ describe('debounce service', function () { it('should delay execution', function () { const bouncer = debounce(spy, 100); - const bouncerFromProvider = debounceFromProvider(spy, 100); bouncer(); sinon.assert.notCalled(spy); @@ -85,16 +78,10 @@ describe('debounce service', function () { sinon.assert.calledOnce(spy); spy.resetHistory(); - - bouncerFromProvider(); - sinon.assert.notCalled(spy); - $timeout.flush(); - sinon.assert.calledOnce(spy); }); it('should fire on leading edge', function () { const bouncer = debounce(spy, 100, { leading: true }); - const bouncerFromProvider = debounceFromProvider(spy, 100, { leading: true }); bouncer(); sinon.assert.calledOnce(spy); @@ -102,19 +89,10 @@ describe('debounce service', function () { sinon.assert.calledTwice(spy); spy.resetHistory(); - - bouncerFromProvider(); - sinon.assert.calledOnce(spy); - $timeout.flush(); - sinon.assert.calledTwice(spy); }); it('should only fire on leading edge', function () { const bouncer = debounce(spy, 100, { leading: true, trailing: false }); - const bouncerFromProvider = debounceFromProvider(spy, 100, { - leading: true, - trailing: false, - }); bouncer(); sinon.assert.calledOnce(spy); @@ -122,17 +100,11 @@ describe('debounce service', function () { sinon.assert.calledOnce(spy); spy.resetHistory(); - - bouncerFromProvider(); - sinon.assert.calledOnce(spy); - $timeout.flush(); - sinon.assert.calledOnce(spy); }); it('should reset delayed execution', function () { const cancelSpy = sinon.spy($timeout, 'cancel'); const bouncer = debounce(spy, 100); - const bouncerFromProvider = debounceFromProvider(spy, 100); bouncer(); sandbox.clock.tick(1); @@ -145,15 +117,6 @@ describe('debounce service', function () { spy.resetHistory(); cancelSpy.resetHistory(); - - bouncerFromProvider(); - sandbox.clock.tick(1); - - bouncerFromProvider(); - sinon.assert.notCalled(spy); - $timeout.flush(); - sinon.assert.calledOnce(spy); - sinon.assert.calledOnce(cancelSpy); }); }); @@ -161,7 +124,6 @@ describe('debounce service', function () { it('should cancel the $timeout', function () { const cancelSpy = sinon.spy($timeout, 'cancel'); const bouncer = debounce(spy, 100); - const bouncerFromProvider = debounceFromProvider(spy, 100); bouncer(); bouncer.cancel(); @@ -170,12 +132,6 @@ describe('debounce service', function () { $timeout.verifyNoPendingTasks(); cancelSpy.resetHistory(); - - bouncerFromProvider(); - bouncerFromProvider.cancel(); - sinon.assert.calledOnce(cancelSpy); - // throws if pending timeouts - $timeout.verifyNoPendingTasks(); }); }); }); diff --git a/src/plugins/discover/public/application/angular/directives/debounce/index.js b/src/plugins/discover/public/application/angular/directives/debounce/index.js index 35b8339263626..3c51895f19828 100644 --- a/src/plugins/discover/public/application/angular/directives/debounce/index.js +++ b/src/plugins/discover/public/application/angular/directives/debounce/index.js @@ -17,6 +17,4 @@ * under the License. */ -import './debounce'; - -export { DebounceProvider } from './debounce'; +export { createDebounceProviderTimeout } from './debounce'; diff --git a/src/plugins/discover/public/application/angular/directives/fixed_scroll.js b/src/plugins/discover/public/application/angular/directives/fixed_scroll.js index 182b4aeca9a23..e2d5f10a0faf7 100644 --- a/src/plugins/discover/public/application/angular/directives/fixed_scroll.js +++ b/src/plugins/discover/public/application/angular/directives/fixed_scroll.js @@ -19,7 +19,7 @@ import $ from 'jquery'; import _ from 'lodash'; -import { DebounceProvider } from './debounce'; +import { createDebounceProviderTimeout } from './debounce'; const SCROLLER_HEIGHT = 20; @@ -28,124 +28,128 @@ const SCROLLER_HEIGHT = 20; * to the target element's real scrollbar. This is useful when the target element's horizontal scrollbar * might be waaaay down the page, like the doc table on Discover. */ -export function FixedScrollProvider(Private) { - const debounce = Private(DebounceProvider); - +export function FixedScrollProvider($timeout) { return { restrict: 'A', link: function ($scope, $el) { - let $window = $(window); - let $scroller = $('
').height(SCROLLER_HEIGHT); - - /** - * Remove the listeners bound in listen() - * @type {function} - */ - let unlisten = _.noop; - - /** - * Listen for scroll events on the $scroller and the $el, sets unlisten() - * - * unlisten must be called before calling or listen() will throw an Error - * - * Since the browser emits "scroll" events after setting scrollLeft - * the listeners also prevent tug-of-war - * - * @throws {Error} If unlisten was not called first - * @return {undefined} - */ - function listen() { - if (unlisten !== _.noop) { - throw new Error( - 'fixedScroll listeners were not cleaned up properly before re-listening!' - ); - } - - let blockTo; - function bind($from, $to) { - function handler() { - if (blockTo === $to) return (blockTo = null); - $to.scrollLeft((blockTo = $from).scrollLeft()); - } - - $from.on('scroll', handler); - return function () { - $from.off('scroll', handler); - }; - } - - unlisten = _.flow(bind($el, $scroller), bind($scroller, $el), function () { - unlisten = _.noop; - }); - } - - /** - * Revert DOM changes and event listeners - * @return {undefined} - */ - function cleanUp() { - unlisten(); - $scroller.detach(); - $el.css('padding-bottom', 0); - } - - /** - * Modify the DOM and attach event listeners based on need. - * Is called many times to re-setup, must be idempotent - * @return {undefined} - */ - function setup() { - cleanUp(); - - const containerWidth = $el.width(); - const contentWidth = $el.prop('scrollWidth'); - const containerHorizOverflow = contentWidth - containerWidth; - - const elTop = $el.offset().top - $window.scrollTop(); - const elBottom = elTop + $el.height(); - const windowVertOverflow = elBottom - $window.height(); - - const requireScroller = containerHorizOverflow > 0 && windowVertOverflow > 0; - if (!requireScroller) return; - - // push the content away from the scroller - $el.css('padding-bottom', SCROLLER_HEIGHT); - - // fill the scroller with a dummy element that mimics the content - $scroller - .width(containerWidth) - .html($('
').css({ width: contentWidth, height: SCROLLER_HEIGHT })) - .insertAfter($el); + return createFixedScroll($scope, $timeout)($el); + }, + }; +} - // listen for scroll events - listen(); +export function createFixedScroll($scope, $timeout) { + const debounce = createDebounceProviderTimeout($timeout); + return function (el) { + const $el = typeof el.css === 'function' ? el : $(el); + let $window = $(window); + let $scroller = $('
').height(SCROLLER_HEIGHT); + + /** + * Remove the listeners bound in listen() + * @type {function} + */ + let unlisten = _.noop; + + /** + * Listen for scroll events on the $scroller and the $el, sets unlisten() + * + * unlisten must be called before calling or listen() will throw an Error + * + * Since the browser emits "scroll" events after setting scrollLeft + * the listeners also prevent tug-of-war + * + * @throws {Error} If unlisten was not called first + * @return {undefined} + */ + function listen() { + if (unlisten !== _.noop) { + throw new Error('fixedScroll listeners were not cleaned up properly before re-listening!'); } - let width; - let scrollWidth; - function checkWidth() { - const newScrollWidth = $el.prop('scrollWidth'); - const newWidth = $el.width(); - - if (scrollWidth !== newScrollWidth || width !== newWidth) { - $scope.$apply(setup); - - scrollWidth = newScrollWidth; - width = newWidth; + let blockTo; + function bind($from, $to) { + function handler() { + if (blockTo === $to) return (blockTo = null); + $to.scrollLeft((blockTo = $from).scrollLeft()); } - } - const debouncedCheckWidth = debounce(checkWidth, 100, { - invokeApply: false, - }); - $scope.$watch(debouncedCheckWidth); + $from.on('scroll', handler); + return function () { + $from.off('scroll', handler); + }; + } - // cleanup when the scope is destroyed - $scope.$on('$destroy', function () { - cleanUp(); - debouncedCheckWidth.cancel(); - $scroller = $window = null; + unlisten = _.flow(bind($el, $scroller), bind($scroller, $el), function () { + unlisten = _.noop; }); - }, + } + + /** + * Revert DOM changes and event listeners + * @return {undefined} + */ + function cleanUp() { + unlisten(); + $scroller.detach(); + $el.css('padding-bottom', 0); + } + + /** + * Modify the DOM and attach event listeners based on need. + * Is called many times to re-setup, must be idempotent + * @return {undefined} + */ + function setup() { + cleanUp(); + + const containerWidth = $el.width(); + const contentWidth = $el.prop('scrollWidth'); + const containerHorizOverflow = contentWidth - containerWidth; + + const elTop = $el.offset().top - $window.scrollTop(); + const elBottom = elTop + $el.height(); + const windowVertOverflow = elBottom - $window.height(); + + const requireScroller = containerHorizOverflow > 0 && windowVertOverflow > 0; + if (!requireScroller) return; + + // push the content away from the scroller + $el.css('padding-bottom', SCROLLER_HEIGHT); + + // fill the scroller with a dummy element that mimics the content + $scroller + .width(containerWidth) + .html($('
').css({ width: contentWidth, height: SCROLLER_HEIGHT })) + .insertAfter($el); + + // listen for scroll events + listen(); + } + + let width; + let scrollWidth; + function checkWidth() { + const newScrollWidth = $el.prop('scrollWidth'); + const newWidth = $el.width(); + + if (scrollWidth !== newScrollWidth || width !== newWidth) { + $scope.$apply(setup); + + scrollWidth = newScrollWidth; + width = newWidth; + } + } + + const debouncedCheckWidth = debounce(checkWidth, 100, { + invokeApply: false, + }); + $scope.$watch(debouncedCheckWidth); + + function destroy() { + cleanUp(); + debouncedCheckWidth.cancel(); + $scroller = $window = null; + } + return destroy; }; } diff --git a/src/plugins/discover/public/application/angular/directives/fixed_scroll.test.js b/src/plugins/discover/public/application/angular/directives/fixed_scroll.test.js index 65255d6c0c4a4..e44bb45cf2431 100644 --- a/src/plugins/discover/public/application/angular/directives/fixed_scroll.test.js +++ b/src/plugins/discover/public/application/angular/directives/fixed_scroll.test.js @@ -23,17 +23,12 @@ import $ from 'jquery'; import sinon from 'sinon'; -import { PrivateProvider, initAngularBootstrap } from '../../../../../kibana_legacy/public'; +import { initAngularBootstrap } from '../../../../../kibana_legacy/public'; import { FixedScrollProvider } from './fixed_scroll'; -import { DebounceProviderTimeout } from './debounce/debounce'; const testModuleName = 'fixedScroll'; -angular - .module(testModuleName, []) - .provider('Private', PrivateProvider) - .service('debounce', ['$timeout', DebounceProviderTimeout]) - .directive('fixedScroll', FixedScrollProvider); +angular.module(testModuleName, []).directive('fixedScroll', FixedScrollProvider); describe('FixedScroll directive', function () { const sandbox = sinon.createSandbox(); @@ -127,7 +122,7 @@ describe('FixedScroll directive', function () { return { $container: $el, $content: $content, - $scroller: $parent.find('.fixed-scroll-scroller'), + $scroller: $parent.find('.dscTableFixedScroll__scroller'), }; }; }); diff --git a/src/plugins/discover/public/application/angular/directives/no_results.js b/src/plugins/discover/public/application/angular/directives/no_results.js index 965c1271c2f2c..d8a39d9178e93 100644 --- a/src/plugins/discover/public/application/angular/directives/no_results.js +++ b/src/plugins/discover/public/application/angular/directives/no_results.js @@ -24,7 +24,6 @@ import PropTypes from 'prop-types'; import { EuiCallOut, EuiCode, - EuiCodeBlock, EuiDescriptionList, EuiFlexGroup, EuiFlexItem, @@ -37,72 +36,12 @@ import { getServices } from '../../../kibana_services'; // eslint-disable-next-line react/prefer-stateless-function export class DiscoverNoResults extends Component { static propTypes = { - shardFailures: PropTypes.array, timeFieldName: PropTypes.string, queryLanguage: PropTypes.string, }; render() { - const { shardFailures, timeFieldName, queryLanguage } = this.props; - - let shardFailuresMessage; - - if (shardFailures && shardFailures.length) { - const failures = shardFailures.map((failure, index) => ( -
- - - - - ), - failureShard: `‘${failure.shard}’`, - }} - /> - - - - - {JSON.stringify(failure.reason)} - - {index < shardFailures.length - 1 ? : undefined} -
- )); - - shardFailuresMessage = ( - - - - -

- -

- -

- -

- - {failures} -
-
- ); - } + const { timeFieldName, queryLanguage } = this.props; let timeFieldMessage; @@ -264,8 +203,6 @@ export class DiscoverNoResults extends Component { iconType="help" data-test-subj="discoverNoResults" /> - - {shardFailuresMessage} {timeFieldMessage} {luceneQueryMessage} diff --git a/src/plugins/discover/public/application/angular/directives/no_results.test.js b/src/plugins/discover/public/application/angular/directives/no_results.test.js index 7de792c612993..60c50048a39ef 100644 --- a/src/plugins/discover/public/application/angular/directives/no_results.test.js +++ b/src/plugins/discover/public/application/angular/directives/no_results.test.js @@ -42,35 +42,6 @@ beforeEach(() => { describe('DiscoverNoResults', () => { describe('props', () => { - describe('shardFailures', () => { - test('renders failures list when there are failures', () => { - const shardFailures = [ - { - index: 'A', - shard: '1', - reason: { reason: 'Awful error' }, - }, - { - index: 'B', - shard: '2', - reason: { reason: 'Bad error' }, - }, - ]; - - const component = renderWithIntl(); - - expect(component).toMatchSnapshot(); - }); - - test(`doesn't render failures list when there are no failures`, () => { - const shardFailures = []; - - const component = renderWithIntl(); - - expect(component).toMatchSnapshot(); - }); - }); - describe('timeFieldName', () => { test('renders time range feedback', () => { const component = renderWithIntl(); diff --git a/src/plugins/discover/public/application/angular/discover.html b/src/plugins/discover/public/application/angular/discover.html deleted file mode 100644 index e0e452aaa41c5..0000000000000 --- a/src/plugins/discover/public/application/angular/discover.html +++ /dev/null @@ -1,160 +0,0 @@ - -

{{screenTitle}}

- - - - - -
-
-
-
- - -
-
- -
- - - - - -
- - - -
- -
- -
- - - - - -
- - - - -
- -
-

- - - - - -
-
-
-
-
-
-
diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index b75ac75e5f2ed..7871cc4b16464 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -29,12 +29,11 @@ import { getState, splitState } from './discover_state'; import { RequestAdapter } from '../../../../inspector/public'; import { SavedObjectSaveModal, showSaveModal } from '../../../../saved_objects/public'; import { getSortArray, getSortForSearchSource } from './doc_table'; +import { createFixedScroll } from './directives/fixed_scroll'; import * as columnActions from './doc_table/actions/columns'; - -import indexTemplate from './discover.html'; +import indexTemplateLegacy from './discover_legacy.html'; import { showOpenSearchPanel } from '../components/top_nav/show_open_search_panel'; import { addHelpMenuToAppChrome } from '../components/help_menu/help_menu_util'; -import '../components/fetch_error'; import { getPainlessError } from './get_painless_error'; import { discoverResponseHandler } from './response_handler'; import { @@ -71,7 +70,6 @@ import { indexPatterns as indexPatternsUtils, connectToQueryState, syncQueryStateWithUrl, - search, } from '../../../../data/public'; import { getIndexPatternId } from '../helpers/get_index_pattern_id'; import { addFatalError } from '../../../../kibana_legacy/public'; @@ -115,7 +113,7 @@ app.config(($routeProvider) => { }; const discoverRoute = { ...defaults, - template: indexTemplate, + template: indexTemplateLegacy, reloadOnSearch: false, resolve: { savedObjects: function ($route, Promise) { @@ -308,18 +306,10 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise mode: 'absolute', }); }; - $scope.intervalOptions = search.aggs.intervalOptions; $scope.minimumVisibleRows = 50; $scope.fetchStatus = fetchStatuses.UNINITIALIZED; $scope.showSaveQuery = uiCapabilities.discover.saveQuery; - $scope.$watch( - () => uiCapabilities.discover.saveQuery, - (newCapability) => { - $scope.showSaveQuery = newCapability; - } - ); - let abortController; $scope.$on('$destroy', () => { if (abortController) abortController.abort(); @@ -471,7 +461,6 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise ]; }; $scope.topNavMenu = getTopNavLinks(); - $scope.setHeaderActionMenu = getHeaderActionMenuMounter(); $scope.searchSource .setField('index', $scope.indexPattern) @@ -515,8 +504,6 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise ]); } - $scope.screenTitle = savedSearch.title; - const getFieldCounts = async () => { // the field counts aren't set until we have the data back, // so we wait for the fetch to be done before proceeding @@ -612,6 +599,9 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise timefield: getTimeField(), savedSearch: savedSearch, indexPatternList: $route.current.locals.savedObjects.ip.list, + config: config, + fixedScroll: createFixedScroll($scope, $timeout), + setHeaderActionMenu: getHeaderActionMenuMounter(), }; const shouldSearchOnPageLoad = () => { @@ -771,6 +761,7 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise if (!init.complete) return; $scope.fetchCounter++; $scope.fetchError = undefined; + $scope.minimumVisibleRows = 50; if (!validateTimeRange(timefilter.getTime(), toastNotifications)) { $scope.resultState = 'none'; return; @@ -868,9 +859,6 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise tabifiedData, getDimensions($scope.vis.data.aggs.aggs, $scope.timeRange) ); - if ($scope.vis.data.aggs.aggs[1]) { - $scope.bucketInterval = $scope.vis.data.aggs.aggs[1].buckets.getInterval(); - } $scope.updateTime(); } diff --git a/src/plugins/discover/public/application/angular/discover_legacy.html b/src/plugins/discover/public/application/angular/discover_legacy.html new file mode 100644 index 0000000000000..8582f71c0cb88 --- /dev/null +++ b/src/plugins/discover/public/application/angular/discover_legacy.html @@ -0,0 +1,36 @@ + + + + diff --git a/src/plugins/discover/public/application/angular/discover_state.ts b/src/plugins/discover/public/application/angular/discover_state.ts index ac0dc054485f0..5ddb6a92b5fd4 100644 --- a/src/plugins/discover/public/application/angular/discover_state.ts +++ b/src/plugins/discover/public/application/angular/discover_state.ts @@ -55,6 +55,10 @@ export interface AppState { * Array of the used sorting [[field,direction],...] */ sort?: string[][]; + /** + * id of the used saved query + */ + savedQuery?: string; } interface GetStateParams { diff --git a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx b/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx new file mode 100644 index 0000000000000..ad2b674af014c --- /dev/null +++ b/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx @@ -0,0 +1,131 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import angular, { auto, ICompileService, IScope } from 'angular'; +import { render } from 'react-dom'; +import React, { useRef, useEffect } from 'react'; +import { getServices, IIndexPattern } from '../../../kibana_services'; +import { IndexPatternField } from '../../../../../data/common/index_patterns'; +export type AngularScope = IScope; + +export interface AngularDirective { + template: string; +} + +/** + * Compiles and injects the give angular template into the given dom node + * returns a function to cleanup the injected angular element + */ +export async function injectAngularElement( + domNode: Element, + template: string, + scopeProps: any, + getInjector: () => Promise +): Promise<() => void> { + const $injector = await getInjector(); + const rootScope: AngularScope = $injector.get('$rootScope'); + const $compile: ICompileService = $injector.get('$compile'); + const newScope = Object.assign(rootScope.$new(), scopeProps); + + const $target = angular.element(domNode); + const $element = angular.element(template); + + newScope.$apply(() => { + const linkFn = $compile($element); + $target.empty().append($element); + linkFn(newScope); + }); + + return () => { + newScope.$destroy(); + }; +} + +/** + * Converts a given legacy angular directive to a render function + * for usage in a react component. Note that the rendering is async + */ +export function convertDirectiveToRenderFn( + directive: AngularDirective, + getInjector: () => Promise +) { + return (domNode: Element, props: any) => { + let rejected = false; + + const cleanupFnPromise = injectAngularElement(domNode, directive.template, props, getInjector); + cleanupFnPromise.catch(() => { + rejected = true; + render(
error
, domNode); + }); + + return () => { + if (!rejected) { + // for cleanup + // http://roubenmeschian.com/rubo/?p=51 + cleanupFnPromise.then((cleanup) => cleanup()); + } + }; + }; +} + +export interface DocTableLegacyProps { + columns: string[]; + searchDescription?: string; + searchTitle?: string; + onFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; + rows: Array>; + indexPattern: IIndexPattern; + minimumVisibleRows: number; + onAddColumn: (column: string) => void; + onSort: (sort: string[][]) => void; + onMoveColumn: (columns: string, newIdx: number) => void; + onRemoveColumn: (column: string) => void; + sort?: string[][]; +} + +export function DocTableLegacy(renderProps: DocTableLegacyProps) { + const renderFn = convertDirectiveToRenderFn( + { + template: ``, + }, + () => getServices().getEmbeddableInjector() + ); + const ref = useRef(null); + useEffect(() => { + if (ref && ref.current) { + return renderFn(ref.current, renderProps); + } + }, [renderFn, renderProps]); + return
; +} diff --git a/src/plugins/discover/public/application/angular/doc_table/doc_table.ts b/src/plugins/discover/public/application/angular/doc_table/doc_table.ts index f972c158ff3dd..735ee9f555740 100644 --- a/src/plugins/discover/public/application/angular/doc_table/doc_table.ts +++ b/src/plugins/discover/public/application/angular/doc_table/doc_table.ts @@ -50,10 +50,6 @@ export function createDocTableDirective(pagerFactory: any, $filter: any) { inspectorAdapters: '=?', }, link: ($scope: LazyScope, $el: JQuery) => { - $scope.$watch('minimumVisibleRows', (minimumVisibleRows: number) => { - $scope.limit = Math.max(minimumVisibleRows || 50, $scope.limit || 50); - }); - $scope.persist = { sorting: $scope.sorting, columns: $scope.columns, @@ -77,7 +73,7 @@ export function createDocTableDirective(pagerFactory: any, $filter: any) { if (!hits) return; // Reset infinite scroll limit - $scope.limit = 50; + $scope.limit = $scope.minimumVisibleRows || 50; if (hits.length === 0) { dispatchRenderComplete($el[0]); diff --git a/src/plugins/discover/public/application/components/create_discover_legacy_directive.ts b/src/plugins/discover/public/application/components/create_discover_legacy_directive.ts new file mode 100644 index 0000000000000..a3502cbb211fa --- /dev/null +++ b/src/plugins/discover/public/application/components/create_discover_legacy_directive.ts @@ -0,0 +1,56 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { DiscoverLegacy } from './discover_legacy'; + +export function createDiscoverLegacyDirective(reactDirective: any) { + return reactDirective(DiscoverLegacy, [ + ['addColumn', { watchDepth: 'reference' }], + ['fetch', { watchDepth: 'reference' }], + ['fetchCounter', { watchDepth: 'reference' }], + ['fetchError', { watchDepth: 'reference' }], + ['fieldCounts', { watchDepth: 'reference' }], + ['histogramData', { watchDepth: 'reference' }], + ['hits', { watchDepth: 'reference' }], + ['indexPattern', { watchDepth: 'reference' }], + ['minimumVisibleRows', { watchDepth: 'reference' }], + ['onAddFilter', { watchDepth: 'reference' }], + ['onChangeInterval', { watchDepth: 'reference' }], + ['onMoveColumn', { watchDepth: 'reference' }], + ['onRemoveColumn', { watchDepth: 'reference' }], + ['onSetColumns', { watchDepth: 'reference' }], + ['onSkipBottomButtonClick', { watchDepth: 'reference' }], + ['onSort', { watchDepth: 'reference' }], + ['opts', { watchDepth: 'reference' }], + ['resetQuery', { watchDepth: 'reference' }], + ['resultState', { watchDepth: 'reference' }], + ['rows', { watchDepth: 'reference' }], + ['savedSearch', { watchDepth: 'reference' }], + ['searchSource', { watchDepth: 'reference' }], + ['setIndexPattern', { watchDepth: 'reference' }], + ['showSaveQuery', { watchDepth: 'reference' }], + ['state', { watchDepth: 'reference' }], + ['timefilterUpdateHandler', { watchDepth: 'reference' }], + ['timeRange', { watchDepth: 'reference' }], + ['topNavMenu', { watchDepth: 'reference' }], + ['updateQuery', { watchDepth: 'reference' }], + ['updateSavedQueryId', { watchDepth: 'reference' }], + ['vis', { watchDepth: 'reference' }], + ]); +} diff --git a/src/plugins/discover/public/application/components/discover_legacy.tsx b/src/plugins/discover/public/application/components/discover_legacy.tsx new file mode 100644 index 0000000000000..1a98843649259 --- /dev/null +++ b/src/plugins/discover/public/application/components/discover_legacy.tsx @@ -0,0 +1,324 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { useState, useCallback, useEffect } from 'react'; +import classNames from 'classnames'; +import { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; +import { IUiSettingsClient, MountPoint } from 'kibana/public'; +import { HitsCounter } from './hits_counter'; +import { TimechartHeader } from './timechart_header'; +import { DiscoverSidebar } from './sidebar'; +import { getServices, IIndexPattern } from '../../kibana_services'; +// @ts-ignore +import { DiscoverNoResults } from '../angular/directives/no_results'; +import { DiscoverUninitialized } from '../angular/directives/uninitialized'; +import { DiscoverHistogram } from '../angular/directives/histogram'; +import { LoadingSpinner } from './loading_spinner/loading_spinner'; +import { DiscoverFetchError, FetchError } from './fetch_error/fetch_error'; +import { DocTableLegacy } from '../angular/doc_table/create_doc_table_react'; +import { SkipBottomButton } from './skip_bottom_button'; +import { + IndexPatternField, + search, + ISearchSource, + TimeRange, + Query, + IndexPatternAttributes, +} from '../../../../data/public'; +import { Chart } from '../angular/helpers/point_series'; +import { AppState } from '../angular/discover_state'; +import { SavedSearch } from '../../saved_searches'; + +import { SavedObject } from '../../../../../core/types'; +import { Vis } from '../../../../visualizations/public'; +import { TopNavMenuData } from '../../../../navigation/public'; + +export interface DiscoverLegacyProps { + addColumn: (column: string) => void; + fetch: () => void; + fetchCounter: number; + fetchError: FetchError; + fieldCounts: Record; + histogramData: Chart; + hits: number; + indexPattern: IIndexPattern; + minimumVisibleRows: number; + onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; + onChangeInterval: (interval: string) => void; + onMoveColumn: (columns: string, newIdx: number) => void; + onRemoveColumn: (column: string) => void; + onSetColumns: (columns: string[]) => void; + onSkipBottomButtonClick: () => void; + onSort: (sort: string[][]) => void; + opts: { + savedSearch: SavedSearch; + config: IUiSettingsClient; + indexPatternList: Array>; + timefield: string; + sampleSize: number; + fixedScroll: (el: HTMLElement) => void; + setHeaderActionMenu: (menuMount: MountPoint | undefined) => void; + }; + resetQuery: () => void; + resultState: string; + rows: Array>; + searchSource: ISearchSource; + setIndexPattern: (id: string) => void; + showSaveQuery: boolean; + state: AppState; + timefilterUpdateHandler: (ranges: { from: number; to: number }) => void; + timeRange?: { from: string; to: string }; + topNavMenu: TopNavMenuData[]; + updateQuery: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void; + updateSavedQueryId: (savedQueryId?: string) => void; + vis?: Vis; +} + +export function DiscoverLegacy({ + addColumn, + fetch, + fetchCounter, + fetchError, + fieldCounts, + histogramData, + hits, + indexPattern, + minimumVisibleRows, + onAddFilter, + onChangeInterval, + onMoveColumn, + onRemoveColumn, + onSkipBottomButtonClick, + onSort, + opts, + resetQuery, + resultState, + rows, + searchSource, + setIndexPattern, + showSaveQuery, + state, + timefilterUpdateHandler, + timeRange, + topNavMenu, + updateQuery, + updateSavedQueryId, + vis, +}: DiscoverLegacyProps) { + const [isSidebarClosed, setIsSidebarClosed] = useState(false); + const { TopNavMenu } = getServices().navigation.ui; + const { savedSearch, indexPatternList } = opts; + const bucketAggConfig = vis?.data?.aggs?.aggs[1]; + const bucketInterval = + bucketAggConfig && search.aggs.isDateHistogramBucketAggConfig(bucketAggConfig) + ? bucketAggConfig.buckets?.getInterval() + : undefined; + const [fixedScrollEl, setFixedScrollEl] = useState(); + + useEffect(() => (fixedScrollEl ? opts.fixedScroll(fixedScrollEl) : undefined), [ + fixedScrollEl, + opts, + ]); + const fixedScrollRef = useCallback( + (node: HTMLElement) => { + if (node !== null) { + setFixedScrollEl(node); + } + }, + [setFixedScrollEl] + ); + const sidebarClassName = classNames({ + closed: isSidebarClosed, + }); + + const mainSectionClassName = classNames({ + 'col-md-10': !isSidebarClosed, + 'col-md-12': isSidebarClosed, + }); + + return ( + +
+

{savedSearch.title}

+ +
+
+
+ {!isSidebarClosed && ( +
+ +
+ )} + setIsSidebarClosed(!isSidebarClosed)} + data-test-subj="collapseSideBarButton" + aria-controls="discover-sidebar" + aria-expanded={isSidebarClosed ? 'false' : 'true'} + aria-label="Toggle sidebar" + className="dscCollapsibleSidebar__collapseButton" + /> +
+
+ {resultState === 'none' && ( + + )} + {resultState === 'uninitialized' && } + {/* @TODO: Solved in the Angular way to satisfy functional test - should be improved*/} + + {fetchError && } +
+ +
+
+ {resultState === 'ready' && ( +
+ + 0 ? hits : 0} + showResetButton={!!(savedSearch && savedSearch.id)} + onResetQuery={resetQuery} + /> + {opts.timefield && ( + + )} + + {opts.timefield && ( +
+ {vis && rows.length !== 0 && ( +
+ +
+ )} +
+ )} + +
+
+

+ +

+ {rows && rows.length && ( +
+ + + ​ + + {rows.length === opts.sampleSize && ( +
+ + + window.scrollTo(0, 0)}> + + +
+ )} +
+ )} +
+
+
+ )} +
+
+
+
+
+ ); +} diff --git a/src/plugins/discover/public/application/components/fetch_error/fetch_error.tsx b/src/plugins/discover/public/application/components/fetch_error/fetch_error.tsx index 880a493983adf..dc8f1238eac6f 100644 --- a/src/plugins/discover/public/application/components/fetch_error/fetch_error.tsx +++ b/src/plugins/discover/public/application/components/fetch_error/fetch_error.tsx @@ -20,18 +20,20 @@ import './fetch_error.scss'; import React, { Fragment } from 'react'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiCallOut, EuiCodeBlock, EuiSpacer } from '@elastic/eui'; -import { getAngularModule, getServices } from '../../../kibana_services'; +import { getServices } from '../../../kibana_services'; + +export interface FetchError { + lang: string; + script: string; + message: string; + error: string; +} interface Props { - fetchError: { - lang: string; - script: string; - message: string; - error: string; - }; + fetchError: FetchError; } -const DiscoverFetchError = ({ fetchError }: Props) => { +export const DiscoverFetchError = ({ fetchError }: Props) => { if (!fetchError) { return null; } @@ -92,9 +94,3 @@ const DiscoverFetchError = ({ fetchError }: Props) => { ); }; - -export function createFetchErrorDirective(reactDirective: any) { - return reactDirective(DiscoverFetchError); -} - -getAngularModule().directive('discoverFetchError', createFetchErrorDirective); diff --git a/src/plugins/discover/public/application/components/fetch_error/index.js b/src/plugins/discover/public/application/components/fetch_error/index.ts similarity index 100% rename from src/plugins/discover/public/application/components/fetch_error/index.js rename to src/plugins/discover/public/application/components/fetch_error/index.ts diff --git a/src/plugins/discover/public/application/components/hits_counter/hits_counter_directive.ts b/src/plugins/discover/public/application/components/hits_counter/hits_counter_directive.ts deleted file mode 100644 index 8d45e28370cad..0000000000000 --- a/src/plugins/discover/public/application/components/hits_counter/hits_counter_directive.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { HitsCounter } from './hits_counter'; - -export function createHitsCounterDirective(reactDirective: any) { - return reactDirective(HitsCounter, [ - ['hits', { watchDepth: 'reference' }], - ['showResetButton', { watchDepth: 'reference' }], - ['onResetQuery', { watchDepth: 'reference' }], - ]); -} diff --git a/src/plugins/discover/public/application/components/hits_counter/index.ts b/src/plugins/discover/public/application/components/hits_counter/index.ts index 58e7a9eda7f51..0ce95f061df17 100644 --- a/src/plugins/discover/public/application/components/hits_counter/index.ts +++ b/src/plugins/discover/public/application/components/hits_counter/index.ts @@ -18,4 +18,3 @@ */ export { HitsCounter } from './hits_counter'; -export { createHitsCounterDirective } from './hits_counter_directive'; diff --git a/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.tsx b/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.tsx index 44b922bf0f708..4e1754638d479 100644 --- a/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.tsx +++ b/src/plugins/discover/public/application/components/loading_spinner/loading_spinner.tsx @@ -18,24 +18,18 @@ */ import React from 'react'; import { EuiLoadingSpinner, EuiTitle, EuiSpacer } from '@elastic/eui'; -import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; export function LoadingSpinner() { return ( - - <> - -

- -

-
- - - -
+ <> + +

+ +

+
+ + + ); } - -export function createLoadingSpinnerDirective(reactDirective: any) { - return reactDirective(LoadingSpinner); -} diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx index 850624888b24a..2407cff181901 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx @@ -68,7 +68,7 @@ export interface DiscoverSidebarProps { /** * Currently selected index pattern */ - selectedIndexPattern: IndexPattern; + selectedIndexPattern?: IndexPattern; /** * Callback function to select another index pattern */ diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_directive.ts b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_directive.ts deleted file mode 100644 index b271c920e5e01..0000000000000 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_directive.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { DiscoverSidebar } from './discover_sidebar'; - -export function createDiscoverSidebarDirective(reactDirective: any) { - return reactDirective(DiscoverSidebar, [ - ['columns', { watchDepth: 'reference' }], - ['fieldCounts', { watchDepth: 'reference' }], - ['hits', { watchDepth: 'reference' }], - ['indexPatternList', { watchDepth: 'reference' }], - ['onAddField', { watchDepth: 'reference' }], - ['onAddFilter', { watchDepth: 'reference' }], - ['onRemoveField', { watchDepth: 'reference' }], - ['selectedIndexPattern', { watchDepth: 'reference' }], - ['setIndexPattern', { watchDepth: 'reference' }], - ]); -} diff --git a/src/plugins/discover/public/application/components/sidebar/index.ts b/src/plugins/discover/public/application/components/sidebar/index.ts index 1b837840b52f6..aec8dfc86e817 100644 --- a/src/plugins/discover/public/application/components/sidebar/index.ts +++ b/src/plugins/discover/public/application/components/sidebar/index.ts @@ -18,4 +18,3 @@ */ export { DiscoverSidebar } from './discover_sidebar'; -export { createDiscoverSidebarDirective } from './discover_sidebar_directive'; diff --git a/src/plugins/discover/public/application/components/sidebar/lib/get_details.ts b/src/plugins/discover/public/application/components/sidebar/lib/get_details.ts index 13051f88c9591..22a6e7a628555 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/get_details.ts +++ b/src/plugins/discover/public/application/components/sidebar/lib/get_details.ts @@ -25,8 +25,11 @@ export function getDetails( field: IndexPatternField, hits: Array>, columns: string[], - indexPattern: IndexPattern + indexPattern?: IndexPattern ) { + if (!indexPattern) { + return {}; + } const details = { ...fieldCalculator.getFieldValueCounts({ hits, diff --git a/src/plugins/discover/public/application/components/sidebar/lib/get_index_pattern_field_list.ts b/src/plugins/discover/public/application/components/sidebar/lib/get_index_pattern_field_list.ts index c96a8f5ce17b9..eff7c2ec3c1c8 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/get_index_pattern_field_list.ts +++ b/src/plugins/discover/public/application/components/sidebar/lib/get_index_pattern_field_list.ts @@ -20,8 +20,8 @@ import { difference } from 'lodash'; import { IndexPattern, IndexPatternField } from 'src/plugins/data/public'; export function getIndexPatternFieldList( - indexPattern: IndexPattern, - fieldCounts: Record + indexPattern?: IndexPattern, + fieldCounts?: Record ) { if (!indexPattern || !fieldCounts) return []; diff --git a/src/plugins/discover/public/application/components/skip_bottom_button/index.ts b/src/plugins/discover/public/application/components/skip_bottom_button/index.ts index 2feaa35e0d61f..b3d93e40be0bd 100644 --- a/src/plugins/discover/public/application/components/skip_bottom_button/index.ts +++ b/src/plugins/discover/public/application/components/skip_bottom_button/index.ts @@ -18,4 +18,3 @@ */ export { SkipBottomButton } from './skip_bottom_button'; -export { createSkipBottomButtonDirective } from './skip_bottom_button_directive'; diff --git a/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button_directive.ts b/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button_directive.ts deleted file mode 100644 index 27f17b25fd447..0000000000000 --- a/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button_directive.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { SkipBottomButton } from './skip_bottom_button'; - -export function createSkipBottomButtonDirective(reactDirective: any) { - return reactDirective(SkipBottomButton, [['onClick', { watchDepth: 'reference' }]]); -} diff --git a/src/plugins/discover/public/application/components/timechart_header/index.ts b/src/plugins/discover/public/application/components/timechart_header/index.ts index 43473319c318c..34bed2cd72a74 100644 --- a/src/plugins/discover/public/application/components/timechart_header/index.ts +++ b/src/plugins/discover/public/application/components/timechart_header/index.ts @@ -18,4 +18,3 @@ */ export { TimechartHeader } from './timechart_header'; -export { createTimechartHeaderDirective } from './timechart_header_directive'; diff --git a/src/plugins/discover/public/application/components/timechart_header/timechart_header.test.tsx b/src/plugins/discover/public/application/components/timechart_header/timechart_header.test.tsx index a4c10e749d868..7889b05a88415 100644 --- a/src/plugins/discover/public/application/components/timechart_header/timechart_header.test.tsx +++ b/src/plugins/discover/public/application/components/timechart_header/timechart_header.test.tsx @@ -29,8 +29,10 @@ describe('timechart header', function () { beforeAll(() => { props = { - from: 'May 14, 2020 @ 11:05:13.590', - to: 'May 14, 2020 @ 11:20:13.590', + timeRange: { + from: 'May 14, 2020 @ 11:05:13.590', + to: 'May 14, 2020 @ 11:20:13.590', + }, stateInterval: 's', options: [ { @@ -47,9 +49,11 @@ describe('timechart header', function () { }, ], onChangeInterval: jest.fn(), - showScaledInfo: undefined, - bucketIntervalDescription: 'second', - bucketIntervalScale: undefined, + bucketInterval: { + scaled: undefined, + description: 'second', + scale: undefined, + }, }; }); @@ -58,8 +62,8 @@ describe('timechart header', function () { expect(component.find(EuiIconTip).length).toBe(0); }); - it('TimechartHeader renders an info text by providing the showScaledInfo property', () => { - props.showScaledInfo = true; + it('TimechartHeader renders an info when bucketInterval.scale is set to true', () => { + props.bucketInterval!.scaled = true; component = mountWithIntl(); expect(component.find(EuiIconTip).length).toBe(1); }); diff --git a/src/plugins/discover/public/application/components/timechart_header/timechart_header.tsx b/src/plugins/discover/public/application/components/timechart_header/timechart_header.tsx index 8789847058aff..1451106827ee0 100644 --- a/src/plugins/discover/public/application/components/timechart_header/timechart_header.tsx +++ b/src/plugins/discover/public/application/components/timechart_header/timechart_header.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { EuiFlexGroup, EuiFlexItem, @@ -27,16 +27,28 @@ import { } from '@elastic/eui'; import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import moment from 'moment'; export interface TimechartHeaderProps { /** - * the query from date string + * Format of date to be displayed */ - from: string; + dateFormat?: string; /** - * the query to date string + * Interval for the buckets of the recent request */ - to: string; + bucketInterval?: { + scaled?: boolean; + description?: string; + scale?: number; + }; + /** + * Range of dates to be displayed + */ + timeRange?: { + from: string; + to: string; + }; /** * Interval Options */ @@ -49,31 +61,29 @@ export interface TimechartHeaderProps { * selected interval */ stateInterval: string; - /** - * displays the scaled info of the interval - */ - showScaledInfo: boolean | undefined; - /** - * scaled info description - */ - bucketIntervalDescription: string; - /** - * bucket interval scale - */ - bucketIntervalScale: number | undefined; } export function TimechartHeader({ - from, - to, + bucketInterval, + dateFormat, + timeRange, options, onChangeInterval, stateInterval, - showScaledInfo, - bucketIntervalDescription, - bucketIntervalScale, }: TimechartHeaderProps) { const [interval, setInterval] = useState(stateInterval); + const toMoment = useCallback( + (datetime: string) => { + if (!datetime) { + return ''; + } + if (!dateFormat) { + return datetime; + } + return moment(datetime).format(dateFormat); + }, + [dateFormat] + ); useEffect(() => { setInterval(stateInterval); @@ -84,6 +94,10 @@ export function TimechartHeader({ onChangeInterval(e.target.value); }; + if (!timeRange || !bucketInterval) { + return null; + } + return ( @@ -95,7 +109,7 @@ export function TimechartHeader({ delay="long" > - {`${from} - ${to} ${ + {`${toMoment(timeRange.from)} - ${toMoment(timeRange.to)} ${ interval !== 'auto' ? i18n.translate('discover.timechartHeader.timeIntervalSelect.per', { defaultMessage: 'per', @@ -125,7 +139,7 @@ export function TimechartHeader({ value={interval} onChange={handleIntervalChange} append={ - showScaledInfo ? ( + bucketInterval.scaled ? ( 1 + bucketInterval!.scale && bucketInterval!.scale > 1 ? i18n.translate('discover.bucketIntervalTooltip.tooLargeBucketsText', { defaultMessage: 'buckets that are too large', }) : i18n.translate('discover.bucketIntervalTooltip.tooManyBucketsText', { defaultMessage: 'too many buckets', }), - bucketIntervalDescription, + bucketIntervalDescription: bucketInterval.description, }, })} color="warning" diff --git a/src/plugins/discover/public/application/components/timechart_header/timechart_header_directive.ts b/src/plugins/discover/public/application/components/timechart_header/timechart_header_directive.ts deleted file mode 100644 index 027236cd46521..0000000000000 --- a/src/plugins/discover/public/application/components/timechart_header/timechart_header_directive.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { TimechartHeader } from './timechart_header'; - -export function createTimechartHeaderDirective(reactDirective: any) { - return reactDirective(TimechartHeader, [ - ['from', { watchDepth: 'reference' }], - ['to', { watchDepth: 'reference' }], - ['options', { watchDepth: 'reference' }], - ['onChangeInterval', { watchDepth: 'reference' }], - ['stateInterval', { watchDepth: 'reference' }], - ['showScaledInfo', { watchDepth: 'reference' }], - ['bucketIntervalDescription', { watchDepth: 'reference' }], - ['bucketIntervalScale', { watchDepth: 'reference' }], - ]); -} diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts index 12562d8571a25..fdb14b3f1f63e 100644 --- a/src/plugins/discover/public/build_services.ts +++ b/src/plugins/discover/public/build_services.ts @@ -44,6 +44,7 @@ import { createSavedSearchesLoader, SavedSearch } from './saved_searches'; import { getHistory } from './kibana_services'; import { KibanaLegacyStart } from '../../kibana_legacy/public'; import { UrlForwardingStart } from '../../url_forwarding/public'; +import { NavigationPublicPluginStart } from '../../navigation/public'; export interface DiscoverServices { addBasePath: (path: string) => string; @@ -58,6 +59,7 @@ export interface DiscoverServices { indexPatterns: IndexPatternsContract; inspector: InspectorPublicPluginStart; metadata: { branch: string }; + navigation: NavigationPublicPluginStart; share?: SharePluginStart; kibanaLegacy: KibanaLegacyStart; urlForwarding: UrlForwardingStart; @@ -65,6 +67,7 @@ export interface DiscoverServices { toastNotifications: ToastsStart; getSavedSearchById: (id: string) => Promise; getSavedSearchUrlById: (id: string) => Promise; + getEmbeddableInjector: any; uiSettings: IUiSettingsClient; visualizations: VisualizationsStart; } @@ -72,7 +75,8 @@ export interface DiscoverServices { export async function buildServices( core: CoreStart, plugins: DiscoverStartPlugins, - context: PluginInitializerContext + context: PluginInitializerContext, + getEmbeddableInjector: any ): Promise { const services: SavedObjectKibanaServices = { savedObjectsClient: core.savedObjects.client, @@ -92,6 +96,7 @@ export async function buildServices( docLinks: core.docLinks, theme: plugins.charts.theme, filterManager: plugins.data.query.filterManager, + getEmbeddableInjector, getSavedSearchById: async (id: string) => savedObjectService.get(id), getSavedSearchUrlById: async (id: string) => savedObjectService.urlFor(id), history: getHistory, @@ -100,6 +105,7 @@ export async function buildServices( metadata: { branch: context.env.packageInfo.branch, }, + navigation: plugins.navigation, share: plugins.share, kibanaLegacy: plugins.kibanaLegacy, urlForwarding: plugins.urlForwarding, diff --git a/src/plugins/discover/public/get_inner_angular.ts b/src/plugins/discover/public/get_inner_angular.ts index 85b0752f13463..1ca0bb20e8723 100644 --- a/src/plugins/discover/public/get_inner_angular.ts +++ b/src/plugins/discover/public/get_inner_angular.ts @@ -40,16 +40,10 @@ import { createTableRowDirective } from './application/angular/doc_table/compone import { createPagerFactory } from './application/angular/doc_table/lib/pager/pager_factory'; import { createInfiniteScrollDirective } from './application/angular/doc_table/infinite_scroll'; import { createDocViewerDirective } from './application/angular/doc_viewer'; -import { CollapsibleSidebarProvider } from './application/angular/directives/collapsible_sidebar/collapsible_sidebar'; -// @ts-ignore -import { FixedScrollProvider } from './application/angular/directives/fixed_scroll'; -// @ts-ignore -import { DebounceProviderTimeout } from './application/angular/directives/debounce/debounce'; import { createRenderCompleteDirective } from './application/angular/directives/render_complete'; import { initAngularBootstrap, configureAppAngularModule, - KbnAccessibleClickProvider, PrivateProvider, PromiseServiceCreator, registerListenEventListener, @@ -57,14 +51,10 @@ import { createTopNavDirective, createTopNavHelper, } from '../../kibana_legacy/public'; -import { createDiscoverSidebarDirective } from './application/components/sidebar'; -import { createHitsCounterDirective } from '././application/components/hits_counter'; -import { createLoadingSpinnerDirective } from '././application/components/loading_spinner/loading_spinner'; -import { createTimechartHeaderDirective } from './application/components/timechart_header'; import { createContextErrorMessageDirective } from './application/components/context_error_message'; import { DiscoverStartPlugins } from './plugin'; import { getScopedHistory } from './kibana_services'; -import { createSkipBottomButtonDirective } from './application/components/skip_bottom_button'; +import { createDiscoverLegacyDirective } from './application/components/create_discover_legacy_directive'; /** * returns the main inner angular module, it contains all the parts of Angular Discover @@ -88,11 +78,9 @@ export function getInnerAngularModule( export function getInnerAngularModuleEmbeddable( name: string, core: CoreStart, - deps: DiscoverStartPlugins, - context: PluginInitializerContext + deps: DiscoverStartPlugins ) { - const module = initializeInnerAngularModule(name, core, deps.navigation, deps.data, true); - return module; + return initializeInnerAngularModule(name, core, deps.navigation, deps.data, true); } let initialized = false; @@ -129,8 +117,7 @@ export function initializeInnerAngularModule( ]) .config(watchMultiDecorator) .directive('icon', (reactDirective) => reactDirective(EuiIcon)) - .directive('renderComplete', createRenderCompleteDirective) - .service('debounce', ['$timeout', DebounceProviderTimeout]); + .directive('renderComplete', createRenderCompleteDirective); } return angular @@ -149,18 +136,9 @@ export function initializeInnerAngularModule( ]) .config(watchMultiDecorator) .run(registerListenEventListener) - .directive('icon', (reactDirective) => reactDirective(EuiIcon)) - .directive('kbnAccessibleClick', KbnAccessibleClickProvider) - .directive('collapsibleSidebar', CollapsibleSidebarProvider) - .directive('fixedScroll', FixedScrollProvider) .directive('renderComplete', createRenderCompleteDirective) - .directive('discoverSidebar', createDiscoverSidebarDirective) - .directive('skipBottomButton', createSkipBottomButtonDirective) - .directive('hitsCounter', createHitsCounterDirective) - .directive('loadingSpinner', createLoadingSpinnerDirective) - .directive('timechartHeader', createTimechartHeaderDirective) - .directive('contextErrorMessage', createContextErrorMessageDirective) - .service('debounce', ['$timeout', DebounceProviderTimeout]); + .directive('discoverLegacy', createDiscoverLegacyDirective) + .directive('contextErrorMessage', createContextErrorMessageDirective); } function createLocalPromiseModule() { diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.ts index dd9b57b568e42..440bd3fdf86d3 100644 --- a/src/plugins/discover/public/plugin.ts +++ b/src/plugins/discover/public/plugin.ts @@ -327,7 +327,12 @@ export class DiscoverPlugin if (this.servicesInitialized) { return { core, plugins }; } - const services = await buildServices(core, plugins, this.initializerContext); + const services = await buildServices( + core, + plugins, + this.initializerContext, + this.getEmbeddableInjector + ); setServices(services); this.servicesInitialized = true; @@ -380,12 +385,7 @@ export class DiscoverPlugin const { core, plugins } = await this.initializeServices(); getServices().kibanaLegacy.loadFontAwesome(); const { getInnerAngularModuleEmbeddable } = await import('./get_inner_angular'); - getInnerAngularModuleEmbeddable( - embeddableAngularName, - core, - plugins, - this.initializerContext - ); + getInnerAngularModuleEmbeddable(embeddableAngularName, core, plugins); const mountpoint = document.createElement('div'); this.embeddableInjector = angular.bootstrap(mountpoint, [embeddableAngularName]); } diff --git a/src/plugins/discover/public/saved_searches/types.ts b/src/plugins/discover/public/saved_searches/types.ts index d601d087afcee..13361cb647ddc 100644 --- a/src/plugins/discover/public/saved_searches/types.ts +++ b/src/plugins/discover/public/saved_searches/types.ts @@ -28,6 +28,7 @@ export interface SavedSearch { columns: string[]; sort: SortOrder[]; destroy: () => void; + lastSavedTitle?: string; } export interface SavedSearchLoader { get: (id: string) => Promise; diff --git a/src/plugins/es_ui_shared/public/request/use_request.test.helpers.tsx b/src/plugins/es_ui_shared/public/request/use_request.test.helpers.tsx index 7a42ed7fad427..b175066b81c8e 100644 --- a/src/plugins/es_ui_shared/public/request/use_request.test.helpers.tsx +++ b/src/plugins/es_ui_shared/public/request/use_request.test.helpers.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { act } from 'react-dom/test-utils'; import { mount, ReactWrapper } from 'enzyme'; import sinon from 'sinon'; @@ -111,6 +111,13 @@ export const createUseRequestHelpers = (): UseRequestHelpers => { requestConfig ); + // Force a re-render of the component to stress-test the useRequest hook and verify its + // state remains unaffected. + const [, setState] = useState(false); + useEffect(() => { + setState(true); + }, []); + hookResult.isInitialRequest = isInitialRequest; hookResult.isLoading = isLoading; hookResult.error = error; diff --git a/src/plugins/es_ui_shared/public/request/use_request.ts b/src/plugins/es_ui_shared/public/request/use_request.ts index e04f84a67b8a3..9d40291423cac 100644 --- a/src/plugins/es_ui_shared/public/request/use_request.ts +++ b/src/plugins/es_ui_shared/public/request/use_request.ts @@ -49,7 +49,7 @@ export const useRequest = ( // Consumers can use isInitialRequest to implement a polling UX. const requestCountRef = useRef(0); - const isInitialRequest = requestCountRef.current === 0; + const isInitialRequestRef = useRef(true); const pollIntervalIdRef = useRef(null); const clearPollInterval = useCallback(() => { @@ -98,6 +98,9 @@ export const useRequest = ( return; } + // Surface to consumers that at least one request has resolved. + isInitialRequestRef.current = false; + setError(responseError); // If there's an error, keep the data from the last request in case it's still useful to the user. if (!responseError) { @@ -146,7 +149,7 @@ export const useRequest = ( }, [clearPollInterval]); return { - isInitialRequest, + isInitialRequest: isInitialRequestRef.current, isLoading, error, data, diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/scripting_help/test_script.tsx b/src/plugins/index_pattern_management/public/components/field_editor/components/scripting_help/test_script.tsx index 77c6698fdc337..d5f04810daf56 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/scripting_help/test_script.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/components/scripting_help/test_script.tsx @@ -81,7 +81,7 @@ export class TestScript extends Component { } previewScript = async (searchContext?: { query?: Query | undefined }) => { - const { indexPattern, lang, name, script, executeScript } = this.props; + const { indexPattern, name, script, executeScript } = this.props; if (!script || script.length === 0) { return; @@ -104,7 +104,6 @@ export class TestScript extends Component { const scriptResponse = await executeScript({ name: name as string, - lang, script, indexPatternTitle: indexPattern.title, query, @@ -122,7 +121,7 @@ export class TestScript extends Component { this.setState({ isLoading: false, - previewData: scriptResponse.hits.hits.map((hit: any) => ({ + previewData: scriptResponse.hits?.hits.map((hit: any) => ({ _id: hit._id, ...hit._source, ...hit.fields, diff --git a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx index 4857a402cc4b2..2b484d1d837bf 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx @@ -784,7 +784,6 @@ export class FieldEditor extends PureComponent => { - // Using _msearch because _search with index name in path dorks everything up - const header = { - index: indexPatternTitle, - ignore_unavailable: true, - }; - - const search = { - query: { - match_all: {}, - } as Query['query'], - script_fields: { - [name]: { - script: { - lang, - source: script, - }, - }, - }, - _source: undefined as string[] | undefined, - size: 10, - timeout: '30s', - }; - - if (additionalFields.length > 0) { - search._source = additionalFields; - } - - if (query) { - search.query = query; - } - - const body = `${JSON.stringify(header)}\n${JSON.stringify(search)}\n`; - const esResp = await http.fetch('/elasticsearch/_msearch', { method: 'POST', body }); - // unwrap _msearch response - return esResp.responses[0]; + return http + .post('/internal/index-pattern-management/preview_scripted_field', { + body: JSON.stringify({ + index: indexPatternTitle, + name, + script, + query, + additionalFields, + }), + }) + .then((res) => ({ + status: res.statusCode, + hits: res.body.hits, + })) + .catch((err) => ({ + status: err.statusCode, + error: err.body.attributes.error, + })); }; export const isScriptValid = async ({ name, - lang, script, indexPatternTitle, http, }: { name: string; - lang: string; script: string; indexPatternTitle: string; http: HttpStart; }) => { const scriptResponse = await executeScript({ name, - lang, script, indexPatternTitle, http, diff --git a/src/plugins/index_pattern_management/public/components/field_editor/types.ts b/src/plugins/index_pattern_management/public/components/field_editor/types.ts index 7519cc05e7fae..d716b9d557282 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/types.ts +++ b/src/plugins/index_pattern_management/public/components/field_editor/types.ts @@ -28,7 +28,6 @@ export interface Sample { export interface ExecuteScriptParams { name: string; - lang: string; script: string; indexPatternTitle: string; query?: Query['query']; @@ -38,7 +37,7 @@ export interface ExecuteScriptParams { export interface ExecuteScriptResult { status: number; - hits: { hits: any[] }; + hits?: { hits: any[] }; error?: any; } diff --git a/src/plugins/index_pattern_management/server/plugin.ts b/src/plugins/index_pattern_management/server/plugin.ts index ecca45cbcc453..2bed6761ef362 100644 --- a/src/plugins/index_pattern_management/server/plugin.ts +++ b/src/plugins/index_pattern_management/server/plugin.ts @@ -18,7 +18,8 @@ */ import { PluginInitializerContext, CoreSetup, Plugin } from 'src/core/server'; -import { schema } from '@kbn/config-schema'; + +import { registerPreviewScriptedFieldRoute, registerResolveIndexRoute } from './routes'; export class IndexPatternManagementPlugin implements Plugin { constructor(initializerContext: PluginInitializerContext) {} @@ -26,42 +27,8 @@ export class IndexPatternManagementPlugin implements Plugin { public setup(core: CoreSetup) { const router = core.http.createRouter(); - router.get( - { - path: '/internal/index-pattern-management/resolve_index/{query}', - validate: { - params: schema.object({ - query: schema.string(), - }), - query: schema.object({ - expand_wildcards: schema.maybe( - schema.oneOf([ - schema.literal('all'), - schema.literal('open'), - schema.literal('closed'), - schema.literal('hidden'), - schema.literal('none'), - ]) - ), - }), - }, - }, - async (context, req, res) => { - const queryString = req.query.expand_wildcards - ? { expand_wildcards: req.query.expand_wildcards } - : null; - const result = await context.core.elasticsearch.legacy.client.callAsCurrentUser( - 'transport.request', - { - method: 'GET', - path: `/_resolve/index/${encodeURIComponent(req.params.query)}${ - queryString ? '?' + new URLSearchParams(queryString).toString() : '' - }`, - } - ); - return res.ok({ body: result }); - } - ); + registerPreviewScriptedFieldRoute(router); + registerResolveIndexRoute(router); } public start() {} diff --git a/src/legacy/server/status/routes/index.js b/src/plugins/index_pattern_management/server/routes/index.ts similarity index 87% rename from src/legacy/server/status/routes/index.js rename to src/plugins/index_pattern_management/server/routes/index.ts index 12736a76d4915..14d53f10970d5 100644 --- a/src/legacy/server/status/routes/index.js +++ b/src/plugins/index_pattern_management/server/routes/index.ts @@ -17,5 +17,5 @@ * under the License. */ -export { registerStatusApi } from './api/register_status'; -export { registerStatsApi } from './api/register_stats'; +export * from './preview_scripted_field'; +export * from './resolve_index'; diff --git a/src/plugins/index_pattern_management/server/routes/preview_scripted_field.test.ts b/src/plugins/index_pattern_management/server/routes/preview_scripted_field.test.ts new file mode 100644 index 0000000000000..5de6ddf351c02 --- /dev/null +++ b/src/plugins/index_pattern_management/server/routes/preview_scripted_field.test.ts @@ -0,0 +1,181 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreSetup, RequestHandlerContext } from 'src/core/server'; +import { coreMock, httpServerMock } from '../../../../../src/core/server/mocks'; +import { registerPreviewScriptedFieldRoute } from './preview_scripted_field'; + +describe('preview_scripted_field route', () => { + let mockCoreSetup: MockedKeys; + + beforeEach(() => { + mockCoreSetup = coreMock.createSetup(); + }); + + it('handler calls /_search with the given request', async () => { + const response = { body: { responses: [{ hits: { _id: 'hi' } }] } }; + const mockClient = { search: jest.fn().mockResolvedValue(response) }; + const mockContext = { + core: { + elasticsearch: { client: { asCurrentUser: mockClient } }, + }, + }; + const mockBody = { + index: 'kibana_sample_data_logs', + name: 'my_scripted_field', + script: `doc['foo'].value`, + }; + const mockQuery = {}; + const mockRequest = httpServerMock.createKibanaRequest({ + body: mockBody, + query: mockQuery, + }); + const mockResponse = httpServerMock.createResponseFactory(); + + registerPreviewScriptedFieldRoute(mockCoreSetup.http.createRouter()); + + const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value; + const handler = mockRouter.post.mock.calls[0][1]; + await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse); + + expect(mockClient.search.mock.calls[0][0]).toMatchInlineSnapshot(` + Object { + "_source": undefined, + "body": Object { + "query": Object { + "match_all": Object {}, + }, + "script_fields": Object { + "my_scripted_field": Object { + "script": Object { + "lang": "painless", + "source": "doc['foo'].value", + }, + }, + }, + }, + "index": "kibana_sample_data_logs", + "size": 10, + "timeout": "30s", + } + `); + + expect(mockResponse.ok).toBeCalled(); + expect(mockResponse.ok.mock.calls[0][0]).toEqual({ body: response }); + }); + + it('uses optional parameters when they are provided', async () => { + const response = { body: { responses: [{ hits: { _id: 'hi' } }] } }; + const mockClient = { search: jest.fn().mockResolvedValue(response) }; + const mockContext = { + core: { + elasticsearch: { client: { asCurrentUser: mockClient } }, + }, + }; + const mockBody = { + index: 'kibana_sample_data_logs', + name: 'my_scripted_field', + script: `doc['foo'].value`, + query: { + bool: { some: 'query' }, + }, + additionalFields: ['a', 'b', 'c'], + }; + const mockQuery = {}; + const mockRequest = httpServerMock.createKibanaRequest({ + body: mockBody, + query: mockQuery, + }); + const mockResponse = httpServerMock.createResponseFactory(); + + registerPreviewScriptedFieldRoute(mockCoreSetup.http.createRouter()); + + const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value; + const handler = mockRouter.post.mock.calls[0][1]; + await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse); + + expect(mockClient.search.mock.calls[0][0]).toMatchInlineSnapshot(` + Object { + "_source": Array [ + "a", + "b", + "c", + ], + "body": Object { + "query": Object { + "bool": Object { + "some": "query", + }, + }, + "script_fields": Object { + "my_scripted_field": Object { + "script": Object { + "lang": "painless", + "source": "doc['foo'].value", + }, + }, + }, + }, + "index": "kibana_sample_data_logs", + "size": 10, + "timeout": "30s", + } + `); + }); + + it('handler throws an error if the search throws an error', async () => { + const response = { + statusCode: 400, + message: 'oops', + }; + const mockClient = { search: jest.fn().mockReturnValue(Promise.reject(response)) }; + const mockContext = { + core: { + elasticsearch: { client: { asCurrentUser: mockClient } }, + }, + }; + const mockBody = { searches: [{ header: {}, body: {} }] }; + const mockQuery = {}; + const mockRequest = httpServerMock.createKibanaRequest({ + body: mockBody, + query: mockQuery, + }); + const mockResponse = httpServerMock.createResponseFactory(); + + registerPreviewScriptedFieldRoute(mockCoreSetup.http.createRouter()); + + const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value; + const handler = mockRouter.post.mock.calls[0][1]; + await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse); + + expect(mockClient.search).toBeCalled(); + expect(mockResponse.customError).toBeCalled(); + + const error: any = mockResponse.customError.mock.calls[0][0]; + expect(error.statusCode).toBe(400); + expect(error.body).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "error": "oops", + }, + "message": "oops", + } + `); + }); +}); diff --git a/src/plugins/index_pattern_management/server/routes/preview_scripted_field.ts b/src/plugins/index_pattern_management/server/routes/preview_scripted_field.ts new file mode 100644 index 0000000000000..849263748aeaa --- /dev/null +++ b/src/plugins/index_pattern_management/server/routes/preview_scripted_field.ts @@ -0,0 +1,74 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '@kbn/config-schema'; +import { IRouter } from 'src/core/server'; + +export function registerPreviewScriptedFieldRoute(router: IRouter): void { + router.post( + { + path: '/internal/index-pattern-management/preview_scripted_field', + validate: { + body: schema.object({ + index: schema.string(), + name: schema.string(), + script: schema.string(), + query: schema.maybe(schema.object({}, { unknowns: 'allow' })), + additionalFields: schema.maybe(schema.arrayOf(schema.string())), + }), + }, + }, + async (context, request, res) => { + const client = context.core.elasticsearch.client.asCurrentUser; + const { index, name, script, query, additionalFields } = request.body; + + try { + const response = await client.search({ + index, + _source: additionalFields && additionalFields.length > 0 ? additionalFields : undefined, + size: 10, + timeout: '30s', + body: { + query: query ?? { match_all: {} }, + script_fields: { + [name]: { + script: { + lang: 'painless', + source: script, + }, + }, + }, + }, + }); + + return res.ok({ body: response }); + } catch (err) { + return res.customError({ + statusCode: err.statusCode || 500, + body: { + message: err.message, + attributes: { + error: err.body?.error || err.message, + }, + }, + }); + } + } + ); +} diff --git a/src/plugins/index_pattern_management/server/routes/resolve_index.ts b/src/plugins/index_pattern_management/server/routes/resolve_index.ts new file mode 100644 index 0000000000000..1d3d89a94e391 --- /dev/null +++ b/src/plugins/index_pattern_management/server/routes/resolve_index.ts @@ -0,0 +1,60 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '@kbn/config-schema'; +import { IRouter } from 'src/core/server'; + +export function registerResolveIndexRoute(router: IRouter): void { + router.get( + { + path: '/internal/index-pattern-management/resolve_index/{query}', + validate: { + params: schema.object({ + query: schema.string(), + }), + query: schema.object({ + expand_wildcards: schema.maybe( + schema.oneOf([ + schema.literal('all'), + schema.literal('open'), + schema.literal('closed'), + schema.literal('hidden'), + schema.literal('none'), + ]) + ), + }), + }, + }, + async (context, req, res) => { + const queryString = req.query.expand_wildcards + ? { expand_wildcards: req.query.expand_wildcards } + : null; + const result = await context.core.elasticsearch.legacy.client.callAsCurrentUser( + 'transport.request', + { + method: 'GET', + path: `/_resolve/index/${encodeURIComponent(req.params.query)}${ + queryString ? '?' + new URLSearchParams(queryString).toString() : '' + }`, + } + ); + return res.ok({ body: result }); + } + ); +} diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/README.md b/src/plugins/kibana_usage_collection/server/collectors/application_usage/README.md index 1ffd01fc6fde7..cb80538fd1718 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/README.md +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/README.md @@ -28,10 +28,10 @@ This collection occurs by default for every application registered via the menti ## Developer notes -In order to keep the count of the events, this collector uses 2 Saved Objects: +In order to keep the count of the events, this collector uses 3 Saved Objects: -1. `application_usage_transactional`: It stores each individually reported event (up to 90 days old). Grouped by `timestamp` and `appId`. -2. `application_usage_totals`: It stores the sum of all the events older than 90 days old per `appId`. +1. `application_usage_transactional`: It stores each individually reported event. Grouped by `timestamp` and `appId`. The reason for having these documents instead of editing `application_usage_daily` documents on very report is to provide faster response to the requests to `/api/ui_metric/report` (creating new documents instead of finding and editing existing ones) and to avoid conflicts when multiple users reach to the API concurrently. +2. `application_usage_daily`: Periodically, documents from `application_usage_transactional` are aggregated to daily summaries and deleted. Also grouped by `timestamp` and `appId`. +3. `application_usage_totals`: It stores the sum of all the events older than 90 days old, grouped by `appId`. -Both of them use the shared fields `appId: 'keyword'`, `numberOfClicks: 'long'` and `minutesOnScreen: 'float'`. `application_usage_transactional` also stores `timestamp: { type: 'date' }`. -but they are currently not added in the mappings because we don't use them for search purposes, and we need to be thoughtful with the number of mapped fields in the SavedObjects index ([#43673](https://github.com/elastic/kibana/issues/43673)). +All the types use the shared fields `appId: 'keyword'`, `numberOfClicks: 'long'` and `minutesOnScreen: 'float'`, but they are currently not added in the mappings because we don't use them for search purposes, and we need to be thoughtful with the number of mapped fields in the SavedObjects index ([#43673](https://github.com/elastic/kibana/issues/43673)). `application_usage_transactional` and `application_usage_daily` also store `timestamp: { type: 'date' }`. diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/index.test.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/index.test.ts deleted file mode 100644 index 5658b38120596..0000000000000 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/index.test.ts +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { savedObjectsRepositoryMock } from '../../../../../core/server/mocks'; -import { - CollectorOptions, - createUsageCollectionSetupMock, -} from '../../../../usage_collection/server/usage_collection.mock'; - -import { registerApplicationUsageCollector } from './'; -import { - ROLL_INDICES_INTERVAL, - SAVED_OBJECTS_TOTAL_TYPE, - SAVED_OBJECTS_TRANSACTIONAL_TYPE, -} from './telemetry_application_usage_collector'; - -describe('telemetry_application_usage', () => { - jest.useFakeTimers(); - - let collector: CollectorOptions; - - const usageCollectionMock = createUsageCollectionSetupMock(); - usageCollectionMock.makeUsageCollector.mockImplementation((config) => { - collector = config; - return createUsageCollectionSetupMock().makeUsageCollector(config); - }); - - const getUsageCollector = jest.fn(); - const registerType = jest.fn(); - const callCluster = jest.fn(); - - beforeAll(() => - registerApplicationUsageCollector(usageCollectionMock, registerType, getUsageCollector) - ); - afterAll(() => jest.clearAllTimers()); - - test('registered collector is set', () => { - expect(collector).not.toBeUndefined(); - }); - - test('if no savedObjectClient initialised, return undefined', async () => { - expect(await collector.fetch(callCluster)).toBeUndefined(); - jest.runTimersToTime(ROLL_INDICES_INTERVAL); - }); - - test('when savedObjectClient is initialised, return something', async () => { - const savedObjectClient = savedObjectsRepositoryMock.create(); - savedObjectClient.find.mockImplementation( - async () => - ({ - saved_objects: [], - total: 0, - } as any) - ); - getUsageCollector.mockImplementation(() => savedObjectClient); - - jest.runTimersToTime(ROLL_INDICES_INTERVAL); // Force rollTotals to run - - expect(await collector.fetch(callCluster)).toStrictEqual({}); - expect(savedObjectClient.bulkCreate).not.toHaveBeenCalled(); - }); - - test('paging in findAll works', async () => { - const savedObjectClient = savedObjectsRepositoryMock.create(); - let total = 201; - savedObjectClient.find.mockImplementation(async (opts) => { - if (opts.type === SAVED_OBJECTS_TOTAL_TYPE) { - return { - saved_objects: [ - { - id: 'appId', - attributes: { - appId: 'appId', - minutesOnScreen: 10, - numberOfClicks: 10, - }, - }, - ], - total: 1, - } as any; - } - if ((opts.page || 1) > 2) { - return { saved_objects: [], total }; - } - const doc = { - id: 'test-id', - attributes: { - appId: 'appId', - timestamp: new Date().toISOString(), - minutesOnScreen: 1, - numberOfClicks: 1, - }, - }; - const savedObjects = new Array(opts.perPage).fill(doc); - total = savedObjects.length * 2 + 1; - return { saved_objects: savedObjects, total }; - }); - - getUsageCollector.mockImplementation(() => savedObjectClient); - - jest.runTimersToTime(ROLL_INDICES_INTERVAL); // Force rollTotals to run - - expect(await collector.fetch(callCluster)).toStrictEqual({ - appId: { - clicks_total: total - 1 + 10, - clicks_7_days: total - 1, - clicks_30_days: total - 1, - clicks_90_days: total - 1, - minutes_on_screen_total: total - 1 + 10, - minutes_on_screen_7_days: total - 1, - minutes_on_screen_30_days: total - 1, - minutes_on_screen_90_days: total - 1, - }, - }); - expect(savedObjectClient.bulkCreate).toHaveBeenCalledWith( - [ - { - id: 'appId', - type: SAVED_OBJECTS_TOTAL_TYPE, - attributes: { - appId: 'appId', - minutesOnScreen: total - 1 + 10, - numberOfClicks: total - 1 + 10, - }, - }, - ], - { overwrite: true } - ); - expect(savedObjectClient.delete).toHaveBeenCalledTimes(total - 1); - expect(savedObjectClient.delete).toHaveBeenCalledWith( - SAVED_OBJECTS_TRANSACTIONAL_TYPE, - 'test-id' - ); - }); -}); diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups.test.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups.test.ts new file mode 100644 index 0000000000000..f8bc17fc40df0 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups.test.ts @@ -0,0 +1,299 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { rollDailyData, rollTotals } from './rollups'; +import { savedObjectsRepositoryMock, loggingSystemMock } from '../../../../../core/server/mocks'; +import { SavedObjectsErrorHelpers } from '../../../../../core/server'; +import { + SAVED_OBJECTS_DAILY_TYPE, + SAVED_OBJECTS_TOTAL_TYPE, + SAVED_OBJECTS_TRANSACTIONAL_TYPE, +} from './saved_objects_types'; + +describe('rollDailyData', () => { + const logger = loggingSystemMock.createLogger(); + + test('returns undefined if no savedObjectsClient initialised yet', async () => { + await expect(rollDailyData(logger, undefined)).resolves.toBe(undefined); + }); + + test('handle empty results', async () => { + const savedObjectClient = savedObjectsRepositoryMock.create(); + savedObjectClient.find.mockImplementation(async ({ type, page = 1, perPage = 10 }) => { + switch (type) { + case SAVED_OBJECTS_TRANSACTIONAL_TYPE: + return { saved_objects: [], total: 0, page, per_page: perPage }; + default: + throw new Error(`Unexpected type [${type}]`); + } + }); + await expect(rollDailyData(logger, savedObjectClient)).resolves.toBe(undefined); + expect(savedObjectClient.get).not.toBeCalled(); + expect(savedObjectClient.bulkCreate).not.toBeCalled(); + expect(savedObjectClient.delete).not.toBeCalled(); + }); + + test('migrate some docs', async () => { + const savedObjectClient = savedObjectsRepositoryMock.create(); + let timesCalled = 0; + savedObjectClient.find.mockImplementation(async ({ type, page = 1, perPage = 10 }) => { + switch (type) { + case SAVED_OBJECTS_TRANSACTIONAL_TYPE: + if (timesCalled++ > 0) { + return { saved_objects: [], total: 0, page, per_page: perPage }; + } + return { + saved_objects: [ + { + id: 'test-id-1', + type, + score: 0, + references: [], + attributes: { + appId: 'appId', + timestamp: '2020-01-01T10:31:00.000Z', + minutesOnScreen: 0.5, + numberOfClicks: 1, + }, + }, + { + id: 'test-id-2', + type, + score: 0, + references: [], + attributes: { + appId: 'appId', + timestamp: '2020-01-01T11:31:00.000Z', + minutesOnScreen: 1.5, + numberOfClicks: 2, + }, + }, + ], + total: 2, + page, + per_page: perPage, + }; + default: + throw new Error(`Unexpected type [${type}]`); + } + }); + + savedObjectClient.get.mockImplementation(async (type, id) => { + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + }); + + await expect(rollDailyData(logger, savedObjectClient)).resolves.toBe(undefined); + expect(savedObjectClient.get).toHaveBeenCalledTimes(1); + expect(savedObjectClient.get).toHaveBeenCalledWith( + SAVED_OBJECTS_DAILY_TYPE, + 'appId:2020-01-01' + ); + expect(savedObjectClient.bulkCreate).toHaveBeenCalledTimes(1); + expect(savedObjectClient.bulkCreate).toHaveBeenCalledWith( + [ + { + type: SAVED_OBJECTS_DAILY_TYPE, + id: 'appId:2020-01-01', + attributes: { + appId: 'appId', + timestamp: '2020-01-01T00:00:00.000Z', + minutesOnScreen: 2.0, + numberOfClicks: 3, + }, + }, + ], + { overwrite: true } + ); + expect(savedObjectClient.delete).toHaveBeenCalledTimes(2); + expect(savedObjectClient.delete).toHaveBeenCalledWith( + SAVED_OBJECTS_TRANSACTIONAL_TYPE, + 'test-id-1' + ); + expect(savedObjectClient.delete).toHaveBeenCalledWith( + SAVED_OBJECTS_TRANSACTIONAL_TYPE, + 'test-id-2' + ); + }); + + test('error getting the daily document', async () => { + const savedObjectClient = savedObjectsRepositoryMock.create(); + let timesCalled = 0; + savedObjectClient.find.mockImplementation(async ({ type, page = 1, perPage = 10 }) => { + switch (type) { + case SAVED_OBJECTS_TRANSACTIONAL_TYPE: + if (timesCalled++ > 0) { + return { saved_objects: [], total: 0, page, per_page: perPage }; + } + return { + saved_objects: [ + { + id: 'test-id-1', + type, + score: 0, + references: [], + attributes: { + appId: 'appId', + timestamp: '2020-01-01T10:31:00.000Z', + minutesOnScreen: 0.5, + numberOfClicks: 1, + }, + }, + ], + total: 1, + page, + per_page: perPage, + }; + default: + throw new Error(`Unexpected type [${type}]`); + } + }); + + savedObjectClient.get.mockImplementation(async (type, id) => { + throw new Error('Something went terribly wrong'); + }); + + await expect(rollDailyData(logger, savedObjectClient)).resolves.toBe(undefined); + expect(savedObjectClient.get).toHaveBeenCalledTimes(1); + expect(savedObjectClient.get).toHaveBeenCalledWith( + SAVED_OBJECTS_DAILY_TYPE, + 'appId:2020-01-01' + ); + expect(savedObjectClient.bulkCreate).toHaveBeenCalledTimes(0); + expect(savedObjectClient.delete).toHaveBeenCalledTimes(0); + }); +}); + +describe('rollTotals', () => { + const logger = loggingSystemMock.createLogger(); + + test('returns undefined if no savedObjectsClient initialised yet', async () => { + await expect(rollTotals(logger, undefined)).resolves.toBe(undefined); + }); + + test('handle empty results', async () => { + const savedObjectClient = savedObjectsRepositoryMock.create(); + savedObjectClient.find.mockImplementation(async ({ type, page = 1, perPage = 10 }) => { + switch (type) { + case SAVED_OBJECTS_DAILY_TYPE: + case SAVED_OBJECTS_TOTAL_TYPE: + return { saved_objects: [], total: 0, page, per_page: perPage }; + default: + throw new Error(`Unexpected type [${type}]`); + } + }); + await expect(rollTotals(logger, savedObjectClient)).resolves.toBe(undefined); + expect(savedObjectClient.bulkCreate).toHaveBeenCalledTimes(0); + expect(savedObjectClient.delete).toHaveBeenCalledTimes(0); + }); + + test('migrate some documents', async () => { + const savedObjectClient = savedObjectsRepositoryMock.create(); + savedObjectClient.find.mockImplementation(async ({ type, page = 1, perPage = 10 }) => { + switch (type) { + case SAVED_OBJECTS_DAILY_TYPE: + return { + saved_objects: [ + { + id: 'appId-2:2020-01-01', + type, + score: 0, + references: [], + attributes: { + appId: 'appId-2', + timestamp: '2020-01-01T10:31:00.000Z', + minutesOnScreen: 0.5, + numberOfClicks: 1, + }, + }, + { + id: 'appId-1:2020-01-01', + type, + score: 0, + references: [], + attributes: { + appId: 'appId-1', + timestamp: '2020-01-01T11:31:00.000Z', + minutesOnScreen: 1.5, + numberOfClicks: 2, + }, + }, + ], + total: 2, + page, + per_page: perPage, + }; + case SAVED_OBJECTS_TOTAL_TYPE: + return { + saved_objects: [ + { + id: 'appId-1', + type, + score: 0, + references: [], + attributes: { + appId: 'appId-1', + minutesOnScreen: 0.5, + numberOfClicks: 1, + }, + }, + ], + total: 1, + page, + per_page: perPage, + }; + default: + throw new Error(`Unexpected type [${type}]`); + } + }); + await expect(rollTotals(logger, savedObjectClient)).resolves.toBe(undefined); + expect(savedObjectClient.bulkCreate).toHaveBeenCalledTimes(1); + expect(savedObjectClient.bulkCreate).toHaveBeenCalledWith( + [ + { + type: SAVED_OBJECTS_TOTAL_TYPE, + id: 'appId-1', + attributes: { + appId: 'appId-1', + minutesOnScreen: 2.0, + numberOfClicks: 3, + }, + }, + { + type: SAVED_OBJECTS_TOTAL_TYPE, + id: 'appId-2', + attributes: { + appId: 'appId-2', + minutesOnScreen: 0.5, + numberOfClicks: 1, + }, + }, + ], + { overwrite: true } + ); + expect(savedObjectClient.delete).toHaveBeenCalledTimes(2); + expect(savedObjectClient.delete).toHaveBeenCalledWith( + SAVED_OBJECTS_DAILY_TYPE, + 'appId-2:2020-01-01' + ); + expect(savedObjectClient.delete).toHaveBeenCalledWith( + SAVED_OBJECTS_DAILY_TYPE, + 'appId-1:2020-01-01' + ); + }); +}); diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups.ts new file mode 100644 index 0000000000000..3020147e95d98 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/rollups.ts @@ -0,0 +1,202 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ISavedObjectsRepository, SavedObject, Logger } from 'kibana/server'; +import moment from 'moment'; +import { + ApplicationUsageDaily, + ApplicationUsageTotal, + ApplicationUsageTransactional, + SAVED_OBJECTS_DAILY_TYPE, + SAVED_OBJECTS_TOTAL_TYPE, + SAVED_OBJECTS_TRANSACTIONAL_TYPE, +} from './saved_objects_types'; +import { SavedObjectsErrorHelpers } from '../../../../../../src/core/server'; + +/** + * For Rolling the daily data, we only care about the stored attributes and the version (to avoid overwriting via concurrent requests) + */ +type ApplicationUsageDailyWithVersion = Pick< + SavedObject, + 'version' | 'attributes' +>; + +/** + * Aggregates all the transactional events into daily aggregates + * @param logger + * @param savedObjectsClient + */ +export async function rollDailyData(logger: Logger, savedObjectsClient?: ISavedObjectsRepository) { + if (!savedObjectsClient) { + return; + } + + try { + let toCreate: Map; + do { + toCreate = new Map(); + const { saved_objects: rawApplicationUsageTransactional } = await savedObjectsClient.find< + ApplicationUsageTransactional + >({ + type: SAVED_OBJECTS_TRANSACTIONAL_TYPE, + perPage: 1000, // Process 1000 at a time as a compromise of speed and overload + }); + + for (const doc of rawApplicationUsageTransactional) { + const { + attributes: { appId, minutesOnScreen, numberOfClicks, timestamp }, + } = doc; + const dayId = moment(timestamp).format('YYYY-MM-DD'); + const dailyId = `${appId}:${dayId}`; + const existingDoc = + toCreate.get(dailyId) || (await getDailyDoc(savedObjectsClient, dailyId, appId, dayId)); + toCreate.set(dailyId, { + ...existingDoc, + attributes: { + ...existingDoc.attributes, + minutesOnScreen: existingDoc.attributes.minutesOnScreen + minutesOnScreen, + numberOfClicks: existingDoc.attributes.numberOfClicks + numberOfClicks, + }, + }); + } + if (toCreate.size > 0) { + await savedObjectsClient.bulkCreate( + [...toCreate.entries()].map(([id, { attributes, version }]) => ({ + type: SAVED_OBJECTS_DAILY_TYPE, + id, + attributes, + version, // Providing version to ensure via conflict matching that only 1 Kibana instance (or interval) is taking care of the updates + })), + { overwrite: true } + ); + await Promise.all( + rawApplicationUsageTransactional.map( + ({ id }) => savedObjectsClient.delete(SAVED_OBJECTS_TRANSACTIONAL_TYPE, id) // There is no bulkDelete :( + ) + ); + } + } while (toCreate.size > 0); + } catch (err) { + logger.warn(`Failed to rollup transactional to daily entries`); + logger.warn(err); + } +} + +/** + * Gets daily doc from the SavedObjects repository. Creates a new one if not found + * @param savedObjectsClient + * @param id The ID of the document to retrieve (typically, `${appId}:${dayId}`) + * @param appId The application ID + * @param dayId The date of the document in the format YYYY-MM-DD + */ +async function getDailyDoc( + savedObjectsClient: ISavedObjectsRepository, + id: string, + appId: string, + dayId: string +): Promise { + try { + return await savedObjectsClient.get(SAVED_OBJECTS_DAILY_TYPE, id); + } catch (err) { + if (SavedObjectsErrorHelpers.isNotFoundError(err)) { + return { + attributes: { + appId, + // Concatenating the day in YYYY-MM-DD form to T00:00:00Z to reduce the TZ effects + timestamp: moment(`${moment(dayId).format('YYYY-MM-DD')}T00:00:00Z`).toISOString(), + minutesOnScreen: 0, + numberOfClicks: 0, + }, + }; + } + throw err; + } +} + +/** + * Moves all the daily documents into aggregated "total" documents as we don't care about any granularity after 90 days + * @param logger + * @param savedObjectsClient + */ +export async function rollTotals(logger: Logger, savedObjectsClient?: ISavedObjectsRepository) { + if (!savedObjectsClient) { + return; + } + + try { + const [ + { saved_objects: rawApplicationUsageTotals }, + { saved_objects: rawApplicationUsageDaily }, + ] = await Promise.all([ + savedObjectsClient.find({ + perPage: 10000, + type: SAVED_OBJECTS_TOTAL_TYPE, + }), + savedObjectsClient.find({ + perPage: 10000, + type: SAVED_OBJECTS_DAILY_TYPE, + filter: `${SAVED_OBJECTS_DAILY_TYPE}.attributes.timestamp < now-90d`, + }), + ]); + + const existingTotals = rawApplicationUsageTotals.reduce( + (acc, { attributes: { appId, numberOfClicks, minutesOnScreen } }) => { + return { + ...acc, + // No need to sum because there should be 1 document per appId only + [appId]: { appId, numberOfClicks, minutesOnScreen }, + }; + }, + {} as Record + ); + + const totals = rawApplicationUsageDaily.reduce((acc, { attributes }) => { + const { appId, numberOfClicks, minutesOnScreen } = attributes; + + const existing = acc[appId] || { minutesOnScreen: 0, numberOfClicks: 0 }; + + return { + ...acc, + [appId]: { + appId, + numberOfClicks: numberOfClicks + existing.numberOfClicks, + minutesOnScreen: minutesOnScreen + existing.minutesOnScreen, + }, + }; + }, existingTotals); + + await Promise.all([ + Object.entries(totals).length && + savedObjectsClient.bulkCreate( + Object.entries(totals).map(([id, entry]) => ({ + type: SAVED_OBJECTS_TOTAL_TYPE, + id, + attributes: entry, + })), + { overwrite: true } + ), + ...rawApplicationUsageDaily.map( + ({ id }) => savedObjectsClient.delete(SAVED_OBJECTS_DAILY_TYPE, id) // There is no bulkDelete :( + ), + ]); + } catch (err) { + logger.warn(`Failed to rollup daily entries to totals`); + logger.warn(err); + } +} diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/saved_objects_types.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/saved_objects_types.ts index 551c6e230972e..861dc98c0c465 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/saved_objects_types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/saved_objects_types.ts @@ -19,19 +19,34 @@ import { SavedObjectAttributes, SavedObjectsServiceSetup } from 'kibana/server'; +/** + * Used for accumulating the totals of all the stats older than 90d + */ export interface ApplicationUsageTotal extends SavedObjectAttributes { appId: string; minutesOnScreen: number; numberOfClicks: number; } +export const SAVED_OBJECTS_TOTAL_TYPE = 'application_usage_totals'; +/** + * Used for storing each of the reports received from the users' browsers + */ export interface ApplicationUsageTransactional extends ApplicationUsageTotal { timestamp: string; } +export const SAVED_OBJECTS_TRANSACTIONAL_TYPE = 'application_usage_transactional'; + +/** + * Used to aggregate the transactional events into daily summaries so we can purge the granular events + */ +export type ApplicationUsageDaily = ApplicationUsageTransactional; +export const SAVED_OBJECTS_DAILY_TYPE = 'application_usage_daily'; export function registerMappings(registerType: SavedObjectsServiceSetup['registerType']) { + // Type for storing ApplicationUsageTotal registerType({ - name: 'application_usage_totals', + name: SAVED_OBJECTS_TOTAL_TYPE, hidden: false, namespaceType: 'agnostic', mappings: { @@ -42,15 +57,28 @@ export function registerMappings(registerType: SavedObjectsServiceSetup['registe }, }); + // Type for storing ApplicationUsageDaily registerType({ - name: 'application_usage_transactional', + name: SAVED_OBJECTS_DAILY_TYPE, hidden: false, namespaceType: 'agnostic', mappings: { dynamic: false, properties: { + // This type requires `timestamp` to be indexed so we can use it when rolling up totals (timestamp < now-90d) timestamp: { type: 'date' }, }, }, }); + + // Type for storing ApplicationUsageTransactional (declaring empty mappings because we don't use the internal fields for query/aggregations) + registerType({ + name: SAVED_OBJECTS_TRANSACTIONAL_TYPE, + hidden: false, + namespaceType: 'agnostic', + mappings: { + dynamic: false, + properties: {}, + }, + }); } diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.test.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.test.ts new file mode 100644 index 0000000000000..709736a37d802 --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.test.ts @@ -0,0 +1,213 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { savedObjectsRepositoryMock, loggingSystemMock } from '../../../../../core/server/mocks'; +import { + CollectorOptions, + createUsageCollectionSetupMock, +} from '../../../../usage_collection/server/usage_collection.mock'; + +import { + ROLL_INDICES_START, + ROLL_TOTAL_INDICES_INTERVAL, + registerApplicationUsageCollector, +} from './telemetry_application_usage_collector'; +import { + SAVED_OBJECTS_DAILY_TYPE, + SAVED_OBJECTS_TOTAL_TYPE, + SAVED_OBJECTS_TRANSACTIONAL_TYPE, +} from './saved_objects_types'; + +describe('telemetry_application_usage', () => { + jest.useFakeTimers(); + + const logger = loggingSystemMock.createLogger(); + + let collector: CollectorOptions; + + const usageCollectionMock = createUsageCollectionSetupMock(); + usageCollectionMock.makeUsageCollector.mockImplementation((config) => { + collector = config; + return createUsageCollectionSetupMock().makeUsageCollector(config); + }); + + const getUsageCollector = jest.fn(); + const registerType = jest.fn(); + const callCluster = jest.fn(); + + beforeAll(() => + registerApplicationUsageCollector(logger, usageCollectionMock, registerType, getUsageCollector) + ); + afterAll(() => jest.clearAllTimers()); + + test('registered collector is set', () => { + expect(collector).not.toBeUndefined(); + }); + + test('if no savedObjectClient initialised, return undefined', async () => { + expect(collector.isReady()).toBe(false); + expect(await collector.fetch(callCluster)).toBeUndefined(); + jest.runTimersToTime(ROLL_INDICES_START); + }); + + test('when savedObjectClient is initialised, return something', async () => { + const savedObjectClient = savedObjectsRepositoryMock.create(); + savedObjectClient.find.mockImplementation( + async () => + ({ + saved_objects: [], + total: 0, + } as any) + ); + getUsageCollector.mockImplementation(() => savedObjectClient); + + jest.runTimersToTime(ROLL_TOTAL_INDICES_INTERVAL); // Force rollTotals to run + + expect(collector.isReady()).toBe(true); + expect(await collector.fetch(callCluster)).toStrictEqual({}); + expect(savedObjectClient.bulkCreate).not.toHaveBeenCalled(); + }); + + test('it only gets 10k even when there are more documents (ES limitation)', async () => { + const savedObjectClient = savedObjectsRepositoryMock.create(); + const total = 10000; + savedObjectClient.find.mockImplementation(async (opts) => { + switch (opts.type) { + case SAVED_OBJECTS_TOTAL_TYPE: + return { + saved_objects: [ + { + id: 'appId', + attributes: { + appId: 'appId', + minutesOnScreen: 10, + numberOfClicks: 10, + }, + }, + ], + total: 1, + } as any; + case SAVED_OBJECTS_TRANSACTIONAL_TYPE: + const doc = { + id: 'test-id', + attributes: { + appId: 'appId', + timestamp: new Date().toISOString(), + minutesOnScreen: 0.5, + numberOfClicks: 1, + }, + }; + const savedObjects = new Array(total).fill(doc); + return { saved_objects: savedObjects, total: total + 1 }; + case SAVED_OBJECTS_DAILY_TYPE: + return { + saved_objects: [ + { + id: 'appId:YYYY-MM-DD', + attributes: { + appId: 'appId', + timestamp: new Date().toISOString(), + minutesOnScreen: 0.5, + numberOfClicks: 1, + }, + }, + ], + total: 1, + }; + } + }); + + getUsageCollector.mockImplementation(() => savedObjectClient); + + jest.runTimersToTime(ROLL_TOTAL_INDICES_INTERVAL); // Force rollTotals to run + + expect(await collector.fetch(callCluster)).toStrictEqual({ + appId: { + clicks_total: total + 1 + 10, + clicks_7_days: total + 1, + clicks_30_days: total + 1, + clicks_90_days: total + 1, + minutes_on_screen_total: (total + 1) * 0.5 + 10, + minutes_on_screen_7_days: (total + 1) * 0.5, + minutes_on_screen_30_days: (total + 1) * 0.5, + minutes_on_screen_90_days: (total + 1) * 0.5, + }, + }); + expect(savedObjectClient.bulkCreate).toHaveBeenCalledWith( + [ + { + id: 'appId', + type: SAVED_OBJECTS_TOTAL_TYPE, + attributes: { + appId: 'appId', + minutesOnScreen: 10.5, + numberOfClicks: 11, + }, + }, + ], + { overwrite: true } + ); + expect(savedObjectClient.delete).toHaveBeenCalledTimes(1); + expect(savedObjectClient.delete).toHaveBeenCalledWith( + SAVED_OBJECTS_DAILY_TYPE, + 'appId:YYYY-MM-DD' + ); + }); + + test('old transactional data not migrated yet', async () => { + const savedObjectClient = savedObjectsRepositoryMock.create(); + savedObjectClient.find.mockImplementation(async (opts) => { + switch (opts.type) { + case SAVED_OBJECTS_TOTAL_TYPE: + case SAVED_OBJECTS_DAILY_TYPE: + return { saved_objects: [], total: 0 } as any; + case SAVED_OBJECTS_TRANSACTIONAL_TYPE: + return { + saved_objects: [ + { + id: 'test-id', + attributes: { + appId: 'appId', + timestamp: new Date(0).toISOString(), + minutesOnScreen: 0.5, + numberOfClicks: 1, + }, + }, + ], + total: 1, + }; + } + }); + + getUsageCollector.mockImplementation(() => savedObjectClient); + + expect(await collector.fetch(callCluster)).toStrictEqual({ + appId: { + clicks_total: 1, + clicks_7_days: 0, + clicks_30_days: 0, + clicks_90_days: 0, + minutes_on_screen_total: 0.5, + minutes_on_screen_7_days: 0, + minutes_on_screen_30_days: 0, + minutes_on_screen_90_days: 0, + }, + }); + }); +}); diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.ts index 69137681e0597..36c89d0a0b4a8 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.ts @@ -18,29 +18,42 @@ */ import moment from 'moment'; -import { ISavedObjectsRepository, SavedObjectsServiceSetup } from 'kibana/server'; +import { timer } from 'rxjs'; +import { ISavedObjectsRepository, Logger, SavedObjectsServiceSetup } from 'kibana/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { findAll } from '../find_all'; import { + ApplicationUsageDaily, ApplicationUsageTotal, ApplicationUsageTransactional, registerMappings, + SAVED_OBJECTS_DAILY_TYPE, + SAVED_OBJECTS_TOTAL_TYPE, + SAVED_OBJECTS_TRANSACTIONAL_TYPE, } from './saved_objects_types'; import { applicationUsageSchema } from './schema'; +import { rollDailyData, rollTotals } from './rollups'; /** - * Roll indices every 24h + * Roll total indices every 24h */ -export const ROLL_INDICES_INTERVAL = 24 * 60 * 60 * 1000; +export const ROLL_TOTAL_INDICES_INTERVAL = 24 * 60 * 60 * 1000; + +/** + * Roll daily indices every 30 minutes. + * This means that, assuming a user can visit all the 44 apps we can possibly report + * in the 3 minutes interval the browser reports to the server, up to 22 users can have the same + * behaviour and we wouldn't need to paginate in the transactional documents (less than 10k docs). + * + * Based on a more normal expected use case, the users could visit up to 5 apps in those 3 minutes, + * allowing up to 200 users before reaching the limit. + */ +export const ROLL_DAILY_INDICES_INTERVAL = 30 * 60 * 1000; /** * Start rolling indices after 5 minutes up */ export const ROLL_INDICES_START = 5 * 60 * 1000; -export const SAVED_OBJECTS_TOTAL_TYPE = 'application_usage_totals'; -export const SAVED_OBJECTS_TRANSACTIONAL_TYPE = 'application_usage_transactional'; - export interface ApplicationUsageTelemetryReport { [appId: string]: { clicks_total: number; @@ -55,6 +68,7 @@ export interface ApplicationUsageTelemetryReport { } export function registerApplicationUsageCollector( + logger: Logger, usageCollection: UsageCollectionSetup, registerType: SavedObjectsServiceSetup['registerType'], getSavedObjectsClient: () => ISavedObjectsRepository | undefined @@ -71,10 +85,22 @@ export function registerApplicationUsageCollector( if (typeof savedObjectsClient === 'undefined') { return; } - const [rawApplicationUsageTotals, rawApplicationUsageTransactional] = await Promise.all([ - findAll(savedObjectsClient, { type: SAVED_OBJECTS_TOTAL_TYPE }), - findAll(savedObjectsClient, { + const [ + { saved_objects: rawApplicationUsageTotals }, + { saved_objects: rawApplicationUsageDaily }, + { saved_objects: rawApplicationUsageTransactional }, + ] = await Promise.all([ + savedObjectsClient.find({ + type: SAVED_OBJECTS_TOTAL_TYPE, + perPage: 10000, // We only have 44 apps for now. This limit is OK. + }), + savedObjectsClient.find({ + type: SAVED_OBJECTS_DAILY_TYPE, + perPage: 10000, // We can have up to 44 apps * 91 days = 4004 docs. This limit is OK + }), + savedObjectsClient.find({ type: SAVED_OBJECTS_TRANSACTIONAL_TYPE, + perPage: 10000, // If we have more than those, we won't report the rest (they'll be rolled up to the daily soon enough to become a problem) }), ]); @@ -101,51 +127,51 @@ export function registerApplicationUsageCollector( const nowMinus30 = moment().subtract(30, 'days'); const nowMinus90 = moment().subtract(90, 'days'); - const applicationUsage = rawApplicationUsageTransactional.reduce( - (acc, { attributes: { appId, minutesOnScreen, numberOfClicks, timestamp } }) => { - const existing = acc[appId] || { - clicks_total: 0, - clicks_7_days: 0, - clicks_30_days: 0, - clicks_90_days: 0, - minutes_on_screen_total: 0, - minutes_on_screen_7_days: 0, - minutes_on_screen_30_days: 0, - minutes_on_screen_90_days: 0, - }; - - const timeOfEntry = moment(timestamp as string); - const isInLast7Days = timeOfEntry.isSameOrAfter(nowMinus7); - const isInLast30Days = timeOfEntry.isSameOrAfter(nowMinus30); - const isInLast90Days = timeOfEntry.isSameOrAfter(nowMinus90); - - const last7Days = { - clicks_7_days: existing.clicks_7_days + numberOfClicks, - minutes_on_screen_7_days: existing.minutes_on_screen_7_days + minutesOnScreen, - }; - const last30Days = { - clicks_30_days: existing.clicks_30_days + numberOfClicks, - minutes_on_screen_30_days: existing.minutes_on_screen_30_days + minutesOnScreen, - }; - const last90Days = { - clicks_90_days: existing.clicks_90_days + numberOfClicks, - minutes_on_screen_90_days: existing.minutes_on_screen_90_days + minutesOnScreen, - }; - - return { - ...acc, - [appId]: { - ...existing, - clicks_total: existing.clicks_total + numberOfClicks, - minutes_on_screen_total: existing.minutes_on_screen_total + minutesOnScreen, - ...(isInLast7Days ? last7Days : {}), - ...(isInLast30Days ? last30Days : {}), - ...(isInLast90Days ? last90Days : {}), - }, - }; - }, - applicationUsageFromTotals - ); + const applicationUsage = [ + ...rawApplicationUsageDaily, + ...rawApplicationUsageTransactional, + ].reduce((acc, { attributes: { appId, minutesOnScreen, numberOfClicks, timestamp } }) => { + const existing = acc[appId] || { + clicks_total: 0, + clicks_7_days: 0, + clicks_30_days: 0, + clicks_90_days: 0, + minutes_on_screen_total: 0, + minutes_on_screen_7_days: 0, + minutes_on_screen_30_days: 0, + minutes_on_screen_90_days: 0, + }; + + const timeOfEntry = moment(timestamp); + const isInLast7Days = timeOfEntry.isSameOrAfter(nowMinus7); + const isInLast30Days = timeOfEntry.isSameOrAfter(nowMinus30); + const isInLast90Days = timeOfEntry.isSameOrAfter(nowMinus90); + + const last7Days = { + clicks_7_days: existing.clicks_7_days + numberOfClicks, + minutes_on_screen_7_days: existing.minutes_on_screen_7_days + minutesOnScreen, + }; + const last30Days = { + clicks_30_days: existing.clicks_30_days + numberOfClicks, + minutes_on_screen_30_days: existing.minutes_on_screen_30_days + minutesOnScreen, + }; + const last90Days = { + clicks_90_days: existing.clicks_90_days + numberOfClicks, + minutes_on_screen_90_days: existing.minutes_on_screen_90_days + minutesOnScreen, + }; + + return { + ...acc, + [appId]: { + ...existing, + clicks_total: existing.clicks_total + numberOfClicks, + minutes_on_screen_total: existing.minutes_on_screen_total + minutesOnScreen, + ...(isInLast7Days ? last7Days : {}), + ...(isInLast30Days ? last30Days : {}), + ...(isInLast90Days ? last90Days : {}), + }, + }; + }, applicationUsageFromTotals); return applicationUsage; }, @@ -154,65 +180,10 @@ export function registerApplicationUsageCollector( usageCollection.registerCollector(collector); - setInterval(() => rollTotals(getSavedObjectsClient()), ROLL_INDICES_INTERVAL); - setTimeout(() => rollTotals(getSavedObjectsClient()), ROLL_INDICES_START); -} - -async function rollTotals(savedObjectsClient?: ISavedObjectsRepository) { - if (!savedObjectsClient) { - return; - } - - try { - const [rawApplicationUsageTotals, rawApplicationUsageTransactional] = await Promise.all([ - findAll(savedObjectsClient, { type: SAVED_OBJECTS_TOTAL_TYPE }), - findAll(savedObjectsClient, { - type: SAVED_OBJECTS_TRANSACTIONAL_TYPE, - filter: `${SAVED_OBJECTS_TRANSACTIONAL_TYPE}.attributes.timestamp < now-90d`, - }), - ]); - - const existingTotals = rawApplicationUsageTotals.reduce( - (acc, { attributes: { appId, numberOfClicks, minutesOnScreen } }) => { - return { - ...acc, - // No need to sum because there should be 1 document per appId only - [appId]: { appId, numberOfClicks, minutesOnScreen }, - }; - }, - {} as Record - ); - - const totals = rawApplicationUsageTransactional.reduce((acc, { attributes, id }) => { - const { appId, numberOfClicks, minutesOnScreen } = attributes; - - const existing = acc[appId] || { minutesOnScreen: 0, numberOfClicks: 0 }; - - return { - ...acc, - [appId]: { - appId, - numberOfClicks: numberOfClicks + existing.numberOfClicks, - minutesOnScreen: minutesOnScreen + existing.minutesOnScreen, - }, - }; - }, existingTotals); - - await Promise.all([ - Object.entries(totals).length && - savedObjectsClient.bulkCreate( - Object.entries(totals).map(([id, entry]) => ({ - type: SAVED_OBJECTS_TOTAL_TYPE, - id, - attributes: entry, - })), - { overwrite: true } - ), - ...rawApplicationUsageTransactional.map( - ({ id }) => savedObjectsClient.delete(SAVED_OBJECTS_TRANSACTIONAL_TYPE, id) // There is no bulkDelete :( - ), - ]); - } catch (err) { - // Silent failure - } + timer(ROLL_INDICES_START, ROLL_DAILY_INDICES_INTERVAL).subscribe(() => + rollDailyData(logger, getSavedObjectsClient()) + ); + timer(ROLL_INDICES_START, ROLL_TOTAL_INDICES_INTERVAL).subscribe(() => + rollTotals(logger, getSavedObjectsClient()) + ); } diff --git a/src/plugins/kibana_usage_collection/server/collectors/find_all.test.ts b/src/plugins/kibana_usage_collection/server/collectors/find_all.test.ts deleted file mode 100644 index d917cd2454e81..0000000000000 --- a/src/plugins/kibana_usage_collection/server/collectors/find_all.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { savedObjectsRepositoryMock } from '../../../../core/server/mocks'; - -import { findAll } from './find_all'; - -describe('telemetry_application_usage', () => { - test('when savedObjectClient is initialised, return something', async () => { - const savedObjectClient = savedObjectsRepositoryMock.create(); - savedObjectClient.find.mockImplementation( - async () => - ({ - saved_objects: [], - total: 0, - } as any) - ); - - expect(await findAll(savedObjectClient, { type: 'test-type' })).toStrictEqual([]); - }); - - test('paging in findAll works', async () => { - const savedObjectClient = savedObjectsRepositoryMock.create(); - let total = 201; - const doc = { id: 'test-id', attributes: { test: 1 } }; - savedObjectClient.find.mockImplementation(async (opts) => { - if ((opts.page || 1) > 2) { - return { saved_objects: [], total } as any; - } - const savedObjects = new Array(opts.perPage).fill(doc); - total = savedObjects.length * 2 + 1; - return { saved_objects: savedObjects, total }; - }); - - expect(await findAll(savedObjectClient, { type: 'test-type' })).toStrictEqual( - new Array(total - 1).fill(doc) - ); - }); -}); diff --git a/src/plugins/kibana_usage_collection/server/collectors/find_all.ts b/src/plugins/kibana_usage_collection/server/collectors/find_all.ts deleted file mode 100644 index 5bb4f20b5c5b1..0000000000000 --- a/src/plugins/kibana_usage_collection/server/collectors/find_all.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { - SavedObjectAttributes, - ISavedObjectsRepository, - SavedObjectsFindOptions, - SavedObject, -} from 'kibana/server'; - -export async function findAll( - savedObjectsClient: ISavedObjectsRepository, - opts: SavedObjectsFindOptions -): Promise>> { - const { page = 1, perPage = 10000, ...options } = opts; - const { saved_objects: savedObjects, total } = await savedObjectsClient.find({ - ...options, - page, - perPage, - }); - if (page * perPage >= total) { - return savedObjects; - } - return [...savedObjects, ...(await findAll(savedObjectsClient, { ...opts, page: page + 1 }))]; -} diff --git a/src/plugins/kibana_usage_collection/server/collectors/ui_metric/telemetry_ui_metric_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/ui_metric/telemetry_ui_metric_collector.ts index 46768813b1970..9c02a9cbf3204 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/ui_metric/telemetry_ui_metric_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/ui_metric/telemetry_ui_metric_collector.ts @@ -23,7 +23,6 @@ import { SavedObjectsServiceSetup, } from 'kibana/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { findAll } from '../find_all'; interface UIMetricsSavedObjects extends SavedObjectAttributes { count: number; @@ -55,9 +54,10 @@ export function registerUiMetricUsageCollector( return; } - const rawUiMetrics = await findAll(savedObjectsClient, { + const { saved_objects: rawUiMetrics } = await savedObjectsClient.find({ type: 'ui-metric', fields: ['count'], + perPage: 10000, }); const uiMetricsByAppName = rawUiMetrics.reduce((accum, rawUiMetric) => { diff --git a/src/plugins/kibana_usage_collection/server/plugin.ts b/src/plugins/kibana_usage_collection/server/plugin.ts index d4295c770803e..260acd19ab516 100644 --- a/src/plugins/kibana_usage_collection/server/plugin.ts +++ b/src/plugins/kibana_usage_collection/server/plugin.ts @@ -30,6 +30,7 @@ import { CoreStart, SavedObjectsServiceSetup, OpsMetrics, + Logger, } from '../../../core/server'; import { registerApplicationUsageCollector, @@ -47,12 +48,14 @@ interface KibanaUsageCollectionPluginsDepsSetup { type SavedObjectsRegisterType = SavedObjectsServiceSetup['registerType']; export class KibanaUsageCollectionPlugin implements Plugin { + private readonly logger: Logger; private readonly legacyConfig$: Observable; private savedObjectsClient?: ISavedObjectsRepository; private uiSettingsClient?: IUiSettingsClient; private metric$: Subject; constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); this.legacyConfig$ = initializerContext.config.legacy.globalConfig$; this.metric$ = new Subject(); } @@ -88,7 +91,12 @@ export class KibanaUsageCollectionPlugin implements Plugin { registerKibanaUsageCollector(usageCollection, this.legacyConfig$); registerManagementUsageCollector(usageCollection, getUiSettingsClient); registerUiMetricUsageCollector(usageCollection, registerType, getSavedObjectsClient); - registerApplicationUsageCollector(usageCollection, registerType, getSavedObjectsClient); + registerApplicationUsageCollector( + this.logger.get('application-usage'), + usageCollection, + registerType, + getSavedObjectsClient + ); registerCspCollector(usageCollection, coreSetup.http); } } diff --git a/src/plugins/legacy_export/server/plugin.ts b/src/plugins/legacy_export/server/plugin.ts index 22c7c1a05dddb..9e1e631903240 100644 --- a/src/plugins/legacy_export/server/plugin.ts +++ b/src/plugins/legacy_export/server/plugin.ts @@ -18,18 +18,23 @@ */ import { Plugin, CoreSetup, PluginInitializerContext } from 'kibana/server'; +import { first } from 'rxjs/operators'; import { registerRoutes } from './routes'; export class LegacyExportPlugin implements Plugin<{}, {}> { - private readonly kibanaVersion: string; + constructor(private readonly initContext: PluginInitializerContext) {} - constructor(context: PluginInitializerContext) { - this.kibanaVersion = context.env.packageInfo.version; - } + public async setup({ http }: CoreSetup) { + const globalConfig = await this.initContext.config.legacy.globalConfig$ + .pipe(first()) + .toPromise(); - public setup({ http }: CoreSetup) { const router = http.createRouter(); - registerRoutes(router, this.kibanaVersion); + registerRoutes( + router, + this.initContext.env.packageInfo.version, + globalConfig.savedObjects.maxImportPayloadBytes.getValueInBytes() + ); return {}; } diff --git a/src/plugins/legacy_export/server/routes/import.ts b/src/plugins/legacy_export/server/routes/import.ts index cf6f28683be17..8d33983ad7e3c 100644 --- a/src/plugins/legacy_export/server/routes/import.ts +++ b/src/plugins/legacy_export/server/routes/import.ts @@ -21,7 +21,7 @@ import { schema } from '@kbn/config-schema'; import { IRouter, SavedObject } from 'src/core/server'; import { importDashboards } from '../lib'; -export const registerImportRoute = (router: IRouter) => { +export const registerImportRoute = (router: IRouter, maxImportPayloadBytes: number) => { router.post( { path: '/api/kibana/dashboards/import', @@ -39,6 +39,9 @@ export const registerImportRoute = (router: IRouter) => { }, options: { tags: ['api'], + body: { + maxBytes: maxImportPayloadBytes, + }, }, }, async (ctx, req, res) => { diff --git a/src/plugins/legacy_export/server/routes/index.ts b/src/plugins/legacy_export/server/routes/index.ts index 7b9de7f016b6b..cac405ce9bdf9 100644 --- a/src/plugins/legacy_export/server/routes/index.ts +++ b/src/plugins/legacy_export/server/routes/index.ts @@ -21,7 +21,11 @@ import { IRouter } from 'src/core/server'; import { registerImportRoute } from './import'; import { registerExportRoute } from './export'; -export const registerRoutes = (router: IRouter, kibanaVersion: string) => { +export const registerRoutes = ( + router: IRouter, + kibanaVersion: string, + maxImportPayloadBytes: number +) => { registerExportRoute(router, kibanaVersion); - registerImportRoute(router); + registerImportRoute(router, maxImportPayloadBytes); }; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap index 0a330d074fd42..3a03c5c01b3c2 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap @@ -222,9 +222,11 @@ exports[`Flyout conflicts should allow conflict resolution 2`] = ` "indexPatterns": Array [ Object { "id": "1", + "title": undefined, }, Object { "id": "2", + "title": undefined, }, ], "isLegacyFile": false, diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx index cc9d2ed160241..72ed69cdca816 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx @@ -64,7 +64,10 @@ describe('Flyout', () => { done: jest.fn(), newIndexPatternUrl: '', indexPatterns: { - getFields: jest.fn().mockImplementation(() => [{ id: '1' }, { id: '2' }]), + getCache: jest.fn().mockImplementation(() => [ + { id: '1', attributes: {} }, + { id: '2', attributes: {} }, + ]), } as any, overlays, http, diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx index 47d82077294cc..3165ea4ca0794 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx @@ -132,7 +132,10 @@ export class Flyout extends Component { } fetchIndexPatterns = async () => { - const indexPatterns = await this.props.indexPatterns.getFields(['id', 'title']); + const indexPatterns = (await this.props.indexPatterns.getCache())?.map((savedObject) => ({ + id: savedObject.id, + title: savedObject.attributes.title, + })); this.setState({ indexPatterns } as any); }; diff --git a/src/plugins/telemetry/server/routes/telemetry_opt_in.ts b/src/plugins/telemetry/server/routes/telemetry_opt_in.ts index aa1de4b2443a4..dd6953ebcda99 100644 --- a/src/plugins/telemetry/server/routes/telemetry_opt_in.ts +++ b/src/plugins/telemetry/server/routes/telemetry_opt_in.ts @@ -26,6 +26,7 @@ import { StatsGetterConfig, TelemetryCollectionManagerPluginSetup, } from 'src/plugins/telemetry_collection_manager/server'; +import { SavedObjectsErrorHelpers } from '../../../../core/server'; import { getTelemetryAllowChangingOptInStatus } from '../../common/telemetry_config'; import { sendTelemetryOptInStatus } from './telemetry_opt_in_stats'; @@ -109,7 +110,13 @@ export function registerTelemetryOptInRoutes({ }); } - await updateTelemetrySavedObject(context.core.savedObjects.client, attributes); + try { + await updateTelemetrySavedObject(context.core.savedObjects.client, attributes); + } catch (e) { + if (SavedObjectsErrorHelpers.isForbiddenError(e)) { + return res.forbidden(); + } + } return res.ok({ body: optInStatus }); } ); diff --git a/src/plugins/ui_actions/public/triggers/value_click_trigger.ts b/src/plugins/ui_actions/public/triggers/value_click_trigger.ts index e63ff28f42d96..f1aff6322522a 100644 --- a/src/plugins/ui_actions/public/triggers/value_click_trigger.ts +++ b/src/plugins/ui_actions/public/triggers/value_click_trigger.ts @@ -27,6 +27,6 @@ export const valueClickTrigger: Trigger<'VALUE_CLICK_TRIGGER'> = { defaultMessage: 'Single click', }), description: i18n.translate('uiActions.triggers.valueClickDescription', { - defaultMessage: 'A single point on the visualization', + defaultMessage: 'A data point click on the visualization', }), }; diff --git a/src/plugins/usage_collection/server/plugin.ts b/src/plugins/usage_collection/server/plugin.ts index 00584e1fd5d86..74e70d5ea9d35 100644 --- a/src/plugins/usage_collection/server/plugin.ts +++ b/src/plugins/usage_collection/server/plugin.ts @@ -49,8 +49,25 @@ export class UsageCollectionPlugin implements Plugin { maximumWaitTimeForAllCollectorsInS: config.maximumWaitTimeForAllCollectorsInS, }); + const globalConfig = await this.initializerContext.config.legacy.globalConfig$ + .pipe(first()) + .toPromise(); + const router = core.http.createRouter(); - setupRoutes(router, () => this.savedObjects); + setupRoutes({ + router, + getSavedObjects: () => this.savedObjects, + collectorSet, + config: { + allowAnonymous: core.status.isStatusPageAnonymous(), + kibanaIndex: globalConfig.kibana.index, + kibanaVersion: this.initializerContext.env.packageInfo.version, + server: core.http.getServerInfo(), + uuid: this.initializerContext.env.instanceUuid, + }, + metrics: core.metrics, + overallStatus$: core.status.overall$, + }); return collectorSet; } diff --git a/src/plugins/usage_collection/server/routes/index.ts b/src/plugins/usage_collection/server/routes/index.ts index e6beef3fbdc59..b367ddc184be7 100644 --- a/src/plugins/usage_collection/server/routes/index.ts +++ b/src/plugins/usage_collection/server/routes/index.ts @@ -17,12 +17,39 @@ * under the License. */ -import { IRouter, ISavedObjectsRepository } from 'kibana/server'; +import { + IRouter, + ISavedObjectsRepository, + MetricsServiceSetup, + ServiceStatus, +} from 'kibana/server'; +import { Observable } from 'rxjs'; +import { CollectorSet } from '../collector'; import { registerUiMetricRoute } from './report_metrics'; +import { registerStatsRoute } from './stats'; -export function setupRoutes( - router: IRouter, - getSavedObjects: () => ISavedObjectsRepository | undefined -) { +export function setupRoutes({ + router, + getSavedObjects, + ...rest +}: { + router: IRouter; + getSavedObjects: () => ISavedObjectsRepository | undefined; + config: { + allowAnonymous: boolean; + kibanaIndex: string; + kibanaVersion: string; + uuid: string; + server: { + name: string; + hostname: string; + port: number; + }; + }; + collectorSet: CollectorSet; + metrics: MetricsServiceSetup; + overallStatus$: Observable; +}) { registerUiMetricRoute(router, getSavedObjects); + registerStatsRoute({ router, ...rest }); } diff --git a/src/plugins/usage_collection/server/routes/integration_tests/stats.test.ts b/src/plugins/usage_collection/server/routes/integration_tests/stats.test.ts new file mode 100644 index 0000000000000..2b39eb626e419 --- /dev/null +++ b/src/plugins/usage_collection/server/routes/integration_tests/stats.test.ts @@ -0,0 +1,104 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { BehaviorSubject } from 'rxjs'; +import { UnwrapPromise } from '@kbn/utility-types'; + +import { + MetricsServiceSetup, + ServiceStatus, + ServiceStatusLevels, +} from '../../../../../core/server'; +import { + contextServiceMock, + loggingSystemMock, + metricsServiceMock, +} from '../../../../../core/server/mocks'; +import { createHttpServer } from '../../../../../core/server/test_utils'; +import { registerStatsRoute } from '../stats'; +import supertest from 'supertest'; +import { CollectorSet } from '../../collector'; + +type HttpService = ReturnType; +type HttpSetup = UnwrapPromise>; + +describe('/api/stats', () => { + let server: HttpService; + let httpSetup: HttpSetup; + let overallStatus$: BehaviorSubject; + let metrics: MetricsServiceSetup; + + beforeEach(async () => { + server = createHttpServer(); + httpSetup = await server.setup({ + context: contextServiceMock.createSetupContract(), + }); + overallStatus$ = new BehaviorSubject({ + level: ServiceStatusLevels.available, + summary: 'everything is working', + }); + metrics = metricsServiceMock.createSetupContract(); + + const router = httpSetup.createRouter(''); + registerStatsRoute({ + router, + collectorSet: new CollectorSet({ + logger: loggingSystemMock.create().asLoggerFactory().get(), + }), + config: { + allowAnonymous: true, + kibanaIndex: '.kibana-test', + kibanaVersion: '8.8.8-SNAPSHOT', + server: { + name: 'mykibana', + hostname: 'mykibana.com', + port: 1234, + }, + uuid: 'xxx-xxxxx', + }, + metrics, + overallStatus$, + }); + + await server.start(); + }); + + afterEach(async () => { + await server.stop(); + }); + + it('successfully returns data', async () => { + const response = await supertest(httpSetup.server.listener).get('/api/stats').expect(200); + expect(response.body).toMatchObject({ + kibana: { + uuid: 'xxx-xxxxx', + name: 'mykibana', + index: '.kibana-test', + host: 'mykibana.com', + locale: 'en', + transport_address: `mykibana.com:1234`, + version: '8.8.8', + snapshot: true, + status: 'green', + }, + last_updated: expect.any(String), + collection_interval_ms: expect.any(Number), + }); + }); +}); diff --git a/src/plugins/usage_collection/server/routes/stats.ts b/src/plugins/usage_collection/server/routes/stats.ts new file mode 100644 index 0000000000000..7c64c9f180319 --- /dev/null +++ b/src/plugins/usage_collection/server/routes/stats.ts @@ -0,0 +1,190 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; +import defaultsDeep from 'lodash/defaultsDeep'; +import { Observable } from 'rxjs'; +import { first } from 'rxjs/operators'; + +import { + IRouter, + LegacyAPICaller, + MetricsServiceSetup, + ServiceStatus, + ServiceStatusLevels, +} from '../../../../core/server'; +import { CollectorSet } from '../collector'; + +const STATS_NOT_READY_MESSAGE = i18n.translate('usageCollection.stats.notReadyMessage', { + defaultMessage: 'Stats are not ready yet. Please try again later.', +}); + +const SNAPSHOT_REGEX = /-snapshot/i; + +export function registerStatsRoute({ + router, + config, + collectorSet, + metrics, + overallStatus$, +}: { + router: IRouter; + config: { + allowAnonymous: boolean; + kibanaIndex: string; + kibanaVersion: string; + uuid: string; + server: { + name: string; + hostname: string; + port: number; + }; + }; + collectorSet: CollectorSet; + metrics: MetricsServiceSetup; + overallStatus$: Observable; +}) { + const getUsage = async (callCluster: LegacyAPICaller): Promise => { + const usage = await collectorSet.bulkFetchUsage(callCluster); + return collectorSet.toObject(usage); + }; + + const getClusterUuid = async (callCluster: LegacyAPICaller): Promise => { + const { cluster_uuid: uuid } = await callCluster('info', { filterPath: 'cluster_uuid' }); + return uuid; + }; + + router.get( + { + path: '/api/stats', + options: { + authRequired: !config.allowAnonymous, + tags: ['api'], // ensures that unauthenticated calls receive a 401 rather than a 302 redirect to login page + }, + validate: { + query: schema.object({ + extended: schema.oneOf([schema.literal(''), schema.boolean()], { defaultValue: false }), + legacy: schema.oneOf([schema.literal(''), schema.boolean()], { defaultValue: false }), + exclude_usage: schema.oneOf([schema.literal(''), schema.boolean()], { + defaultValue: false, + }), + }), + }, + }, + async (context, req, res) => { + const isExtended = req.query.extended === '' || req.query.extended; + const isLegacy = req.query.legacy === '' || req.query.legacy; + const shouldGetUsage = req.query.exclude_usage === false; + + let extended; + if (isExtended) { + const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; + const collectorsReady = await collectorSet.areAllCollectorsReady(); + + if (shouldGetUsage && !collectorsReady) { + return res.customError({ statusCode: 503, body: { message: STATS_NOT_READY_MESSAGE } }); + } + + const usagePromise = shouldGetUsage ? getUsage(callCluster) : Promise.resolve({}); + const [usage, clusterUuid] = await Promise.all([usagePromise, getClusterUuid(callCluster)]); + + let modifiedUsage = usage; + if (isLegacy) { + // In an effort to make telemetry more easily augmented, we need to ensure + // we can passthrough the data without every part of the process needing + // to know about the change; however, to support legacy use cases where this + // wasn't true, we need to be backwards compatible with how the legacy data + // looked and support those use cases here. + modifiedUsage = Object.keys(usage).reduce((accum, usageKey) => { + if (usageKey === 'kibana') { + accum = { + ...accum, + ...usage[usageKey], + }; + } else if (usageKey === 'reporting') { + accum = { + ...accum, + xpack: { + ...accum.xpack, + reporting: usage[usageKey], + }, + }; + } else { + // I don't think we need to it this for the above conditions, but do it for most as it will + // match the behavior done in monitoring/bulk_uploader + defaultsDeep(accum, { [usageKey]: usage[usageKey] }); + } + + return accum; + }, {} as any); + + extended = { + usage: modifiedUsage, + clusterUuid, + }; + } else { + extended = collectorSet.toApiFieldNames({ + usage: modifiedUsage, + clusterUuid, + }); + } + } + + // Guranteed to resolve immediately due to replay effect on getOpsMetrics$ + // eslint-disable-next-line @typescript-eslint/naming-convention + const { collected_at, ...lastMetrics } = await metrics + .getOpsMetrics$() + .pipe(first()) + .toPromise(); + + const overallStatus = await overallStatus$.pipe(first()).toPromise(); + const kibanaStats = collectorSet.toApiFieldNames({ + ...lastMetrics, + kibana: { + uuid: config.uuid, + name: config.server.name, + index: config.kibanaIndex, + host: config.server.hostname, + locale: i18n.getLocale(), + transport_address: `${config.server.hostname}:${config.server.port}`, + version: config.kibanaVersion.replace(SNAPSHOT_REGEX, ''), + snapshot: SNAPSHOT_REGEX.test(config.kibanaVersion), + status: ServiceStatusToLegacyState[overallStatus.level.toString()], + }, + last_updated: collected_at.toISOString(), + collection_interval_in_millis: metrics.collectionInterval, + }); + + return res.ok({ + body: { + ...kibanaStats, + ...extended, + }, + }); + } + ); +} + +const ServiceStatusToLegacyState: Record = { + [ServiceStatusLevels.critical.toString()]: 'red', + [ServiceStatusLevels.unavailable.toString()]: 'red', + [ServiceStatusLevels.degraded.toString()]: 'yellow', + [ServiceStatusLevels.available.toString()]: 'green', +}; diff --git a/src/plugins/vis_type_timeseries/server/index.ts b/src/plugins/vis_type_timeseries/server/index.ts index f460257caf5e3..333ed0ff64fdb 100644 --- a/src/plugins/vis_type_timeseries/server/index.ts +++ b/src/plugins/vis_type_timeseries/server/index.ts @@ -21,7 +21,7 @@ import { PluginInitializerContext, PluginConfigDescriptor } from 'src/core/serve import { VisTypeTimeseriesConfig, config as configSchema } from './config'; import { VisTypeTimeseriesPlugin } from './plugin'; -export { VisTypeTimeseriesSetup, Framework } from './plugin'; +export { VisTypeTimeseriesSetup } from './plugin'; export const config: PluginConfigDescriptor = { deprecations: ({ unused, renameFromRoot }) => [ @@ -39,10 +39,10 @@ export const config: PluginConfigDescriptor = { export { ValidationTelemetryServiceSetup } from './validation_telemetry'; -// @ts-ignore -export { AbstractSearchStrategy } from './lib/search_strategies/strategies/abstract_search_strategy'; -// @ts-ignore -export { AbstractSearchRequest } from './lib/search_strategies/search_requests/abstract_request'; +export { + AbstractSearchStrategy, + ReqFacade, +} from './lib/search_strategies/strategies/abstract_search_strategy'; // @ts-ignore export { DefaultSearchCapabilities } from './lib/search_strategies/default_search_capabilities'; diff --git a/src/plugins/vis_type_timeseries/server/lib/get_fields.ts b/src/plugins/vis_type_timeseries/server/lib/get_fields.ts index 0f0d99bff6f1c..777de89672bbe 100644 --- a/src/plugins/vis_type_timeseries/server/lib/get_fields.ts +++ b/src/plugins/vis_type_timeseries/server/lib/get_fields.ts @@ -38,6 +38,7 @@ export async function getFields( // level object passed from here. The layers should be refactored fully at some point, but for now // this works and we are still using the New Platform services for these vis data portions. const reqFacade: ReqFacade = { + requestContext, ...request, framework, payload: {}, @@ -48,22 +49,6 @@ export async function getFields( }, getUiSettingsService: () => requestContext.core.uiSettings.client, getSavedObjectsClient: () => requestContext.core.savedObjects.client, - server: { - plugins: { - elasticsearch: { - getCluster: () => { - return { - callWithRequest: async (req: any, endpoint: string, params: any) => { - return await requestContext.core.elasticsearch.legacy.client.callAsCurrentUser( - endpoint, - params - ); - }, - }; - }, - }, - }, - }, getEsShardTimeout: async () => { return await framework.globalConfig$ .pipe( diff --git a/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts b/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts index f697e754a2e00..5eef2b53e2431 100644 --- a/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts +++ b/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts @@ -21,7 +21,7 @@ import { FakeRequest, RequestHandlerContext } from 'kibana/server'; import _ from 'lodash'; import { first, map } from 'rxjs/operators'; import { getPanelData } from './vis_data/get_panel_data'; -import { Framework } from '../index'; +import { Framework } from '../plugin'; import { ReqFacade } from './search_strategies/strategies/abstract_search_strategy'; interface GetVisDataResponse { @@ -65,28 +65,13 @@ export function getVisData( // level object passed from here. The layers should be refactored fully at some point, but for now // this works and we are still using the New Platform services for these vis data portions. const reqFacade: ReqFacade = { + requestContext, ...request, framework, pre: {}, payload: request.body, getUiSettingsService: () => requestContext.core.uiSettings.client, getSavedObjectsClient: () => requestContext.core.savedObjects.client, - server: { - plugins: { - elasticsearch: { - getCluster: () => { - return { - callWithRequest: async (req: any, endpoint: string, params: any) => { - return await requestContext.core.elasticsearch.legacy.client.callAsCurrentUser( - endpoint, - params - ); - }, - }; - }, - }, - }, - }, getEsShardTimeout: async () => { return await framework.globalConfig$ .pipe( diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/abstract_request.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/abstract_request.js deleted file mode 100644 index abd2a4c65d35c..0000000000000 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/abstract_request.js +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -export class AbstractSearchRequest { - constructor(req, callWithRequest) { - this.req = req; - this.callWithRequest = callWithRequest; - } - - search() { - throw new Error('AbstractSearchRequest: search method should be defined'); - } -} diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/abstract_request.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/abstract_request.test.js deleted file mode 100644 index 6f71aa63728d5..0000000000000 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/abstract_request.test.js +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { AbstractSearchRequest } from './abstract_request'; - -describe('AbstractSearchRequest', () => { - let searchRequest; - let req; - let callWithRequest; - - beforeEach(() => { - req = {}; - callWithRequest = jest.fn(); - searchRequest = new AbstractSearchRequest(req, callWithRequest); - }); - - test('should init an AbstractSearchRequest instance', () => { - expect(searchRequest.req).toBe(req); - expect(searchRequest.callWithRequest).toBe(callWithRequest); - expect(searchRequest.search).toBeDefined(); - }); - - test('should throw an error trying to search', () => { - try { - searchRequest.search(); - } catch (error) { - expect(error instanceof Error).toBe(true); - expect(error.message).toEqual('AbstractSearchRequest: search method should be defined'); - } - }); -}); diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/multi_search_request.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/multi_search_request.js deleted file mode 100644 index 9ada39e359589..0000000000000 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/multi_search_request.js +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { AbstractSearchRequest } from './abstract_request'; -import { UI_SETTINGS } from '../../../../../data/server'; - -const SEARCH_METHOD = 'msearch'; - -export class MultiSearchRequest extends AbstractSearchRequest { - async search(searches) { - const includeFrozen = await this.req - .getUiSettingsService() - .get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN); - const multiSearchBody = searches.reduce( - (acc, { body, index }) => [ - ...acc, - { - index, - ignoreUnavailable: true, - }, - body, - ], - [] - ); - - const { responses } = await this.callWithRequest(this.req, SEARCH_METHOD, { - body: multiSearchBody, - rest_total_hits_as_int: true, - ignore_throttled: !includeFrozen, - }); - - return responses; - } -} diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/multi_search_request.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/multi_search_request.test.js deleted file mode 100644 index c113db76332b7..0000000000000 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/multi_search_request.test.js +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { MultiSearchRequest } from './multi_search_request'; -import { UI_SETTINGS } from '../../../../../data/server'; - -describe('MultiSearchRequest', () => { - let searchRequest; - let req; - let callWithRequest; - let getServiceMock; - let includeFrozen; - - beforeEach(() => { - includeFrozen = false; - getServiceMock = jest.fn().mockResolvedValue(includeFrozen); - req = { - getUiSettingsService: jest.fn().mockReturnValue({ get: getServiceMock }), - }; - callWithRequest = jest.fn().mockReturnValue({ responses: [] }); - searchRequest = new MultiSearchRequest(req, callWithRequest); - }); - - test('should init an MultiSearchRequest instance', () => { - expect(searchRequest.req).toBe(req); - expect(searchRequest.callWithRequest).toBe(callWithRequest); - expect(searchRequest.search).toBeDefined(); - }); - - test('should get the response from elastic msearch', async () => { - const searches = [ - { body: 'body1', index: 'index' }, - { body: 'body2', index: 'index' }, - ]; - - const responses = await searchRequest.search(searches); - - expect(responses).toEqual([]); - expect(req.getUiSettingsService).toHaveBeenCalled(); - expect(getServiceMock).toHaveBeenCalledWith(UI_SETTINGS.SEARCH_INCLUDE_FROZEN); - expect(callWithRequest).toHaveBeenCalledWith(req, 'msearch', { - body: [ - { ignoreUnavailable: true, index: 'index' }, - 'body1', - { ignoreUnavailable: true, index: 'index' }, - 'body2', - ], - rest_total_hits_as_int: true, - ignore_throttled: !includeFrozen, - }); - }); -}); diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/search_request.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/search_request.js deleted file mode 100644 index e6e3bcb527286..0000000000000 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/search_request.js +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { AbstractSearchRequest } from './abstract_request'; - -import { MultiSearchRequest } from './multi_search_request'; -import { SingleSearchRequest } from './single_search_request'; - -export class SearchRequest extends AbstractSearchRequest { - getSearchRequestType(searches) { - const isMultiSearch = Array.isArray(searches) && searches.length > 1; - const SearchRequest = isMultiSearch ? MultiSearchRequest : SingleSearchRequest; - - return new SearchRequest(this.req, this.callWithRequest); - } - - async search(options) { - const concreteSearchRequest = this.getSearchRequestType(options); - - return concreteSearchRequest.search(options); - } -} diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/search_request.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/search_request.test.js deleted file mode 100644 index 3d35a4aa37c5a..0000000000000 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/search_request.test.js +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { SearchRequest } from './search_request'; -import { MultiSearchRequest } from './multi_search_request'; -import { SingleSearchRequest } from './single_search_request'; - -describe('SearchRequest', () => { - let searchRequest; - let req; - let callWithRequest; - let getServiceMock; - let includeFrozen; - - beforeEach(() => { - includeFrozen = false; - getServiceMock = jest.fn().mockResolvedValue(includeFrozen); - req = { - getUiSettingsService: jest.fn().mockReturnValue({ get: getServiceMock }), - }; - callWithRequest = jest.fn().mockReturnValue({ responses: [] }); - searchRequest = new SearchRequest(req, callWithRequest); - }); - - test('should init an AbstractSearchRequest instance', () => { - expect(searchRequest.req).toBe(req); - expect(searchRequest.callWithRequest).toBe(callWithRequest); - expect(searchRequest.search).toBeDefined(); - }); - - test('should return search value', async () => { - const concreteSearchRequest = { - search: jest.fn().mockReturnValue('concreteSearchRequest'), - }; - const options = {}; - searchRequest.getSearchRequestType = jest.fn().mockReturnValue(concreteSearchRequest); - - const result = await searchRequest.search(options); - - expect(result).toBe('concreteSearchRequest'); - }); - - test('should return a MultiSearchRequest for multi searches', () => { - const searches = [ - { index: 'index', body: 'body' }, - { index: 'index', body: 'body' }, - ]; - - const result = searchRequest.getSearchRequestType(searches); - - expect(result instanceof MultiSearchRequest).toBe(true); - }); - - test('should return a SingleSearchRequest for single search', () => { - const searches = [{ index: 'index', body: 'body' }]; - - const result = searchRequest.getSearchRequestType(searches); - - expect(result instanceof SingleSearchRequest).toBe(true); - }); -}); diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/single_search_request.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/single_search_request.js deleted file mode 100644 index 7d8b60a7e4595..0000000000000 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/single_search_request.js +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { AbstractSearchRequest } from './abstract_request'; -import { UI_SETTINGS } from '../../../../../data/server'; - -const SEARCH_METHOD = 'search'; - -export class SingleSearchRequest extends AbstractSearchRequest { - async search([{ body, index }]) { - const includeFrozen = await this.req - .getUiSettingsService() - .get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN); - const resp = await this.callWithRequest(this.req, SEARCH_METHOD, { - ignore_throttled: !includeFrozen, - body, - index, - }); - - return [resp]; - } -} diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/single_search_request.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/single_search_request.test.js deleted file mode 100644 index b899814f2fe13..0000000000000 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/single_search_request.test.js +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { SingleSearchRequest } from './single_search_request'; -import { UI_SETTINGS } from '../../../../../data/server'; - -describe('SingleSearchRequest', () => { - let searchRequest; - let req; - let callWithRequest; - let getServiceMock; - let includeFrozen; - - beforeEach(() => { - includeFrozen = false; - getServiceMock = jest.fn().mockResolvedValue(includeFrozen); - req = { - getUiSettingsService: jest.fn().mockReturnValue({ get: getServiceMock }), - }; - callWithRequest = jest.fn().mockReturnValue({}); - searchRequest = new SingleSearchRequest(req, callWithRequest); - }); - - test('should init an SingleSearchRequest instance', () => { - expect(searchRequest.req).toBe(req); - expect(searchRequest.callWithRequest).toBe(callWithRequest); - expect(searchRequest.search).toBeDefined(); - }); - - test('should get the response from elastic search', async () => { - const searches = [{ body: 'body', index: 'index' }]; - - const responses = await searchRequest.search(searches); - - expect(responses).toEqual([{}]); - expect(req.getUiSettingsService).toHaveBeenCalled(); - expect(getServiceMock).toHaveBeenCalledWith(UI_SETTINGS.SEARCH_INCLUDE_FROZEN); - expect(callWithRequest).toHaveBeenCalledWith(req, 'search', { - body: 'body', - index: 'index', - ignore_throttled: !includeFrozen, - }); - }); -}); diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts index ecd09653b3b48..66ea4f017dd90 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts @@ -65,7 +65,7 @@ describe('SearchStrategyRegister', () => { }); test('should add a strategy if it is an instance of AbstractSearchStrategy', () => { - const anotherSearchStrategy = new MockSearchStrategy({}, {} as any, {}); + const anotherSearchStrategy = new MockSearchStrategy('es'); const addedStrategies = registry.addStrategy(anotherSearchStrategy); expect(addedStrategies.length).toEqual(2); @@ -75,7 +75,7 @@ describe('SearchStrategyRegister', () => { test('should return a MockSearchStrategy instance', async () => { const req = {}; const indexPattern = '*'; - const anotherSearchStrategy = new MockSearchStrategy({}, {} as any, {}); + const anotherSearchStrategy = new MockSearchStrategy('es'); registry.addStrategy(anotherSearchStrategy); const { searchStrategy, capabilities } = (await registry.getViableStrategy(req, indexPattern))!; diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js index 1fbaffd794c89..6773ee482b098 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js @@ -18,24 +18,13 @@ */ import { AbstractSearchStrategy } from './abstract_search_strategy'; -class SearchRequest { - constructor(req, callWithRequest) { - this.req = req; - this.callWithRequest = callWithRequest; - } -} - describe('AbstractSearchStrategy', () => { let abstractSearchStrategy; - let server; - let callWithRequestFactory; let req; let mockedFields; let indexPattern; beforeEach(() => { - server = {}; - callWithRequestFactory = jest.fn().mockReturnValue('callWithRequest'); mockedFields = {}; req = { pre: { @@ -45,16 +34,11 @@ describe('AbstractSearchStrategy', () => { }, }; - abstractSearchStrategy = new AbstractSearchStrategy( - server, - callWithRequestFactory, - SearchRequest - ); + abstractSearchStrategy = new AbstractSearchStrategy('es'); }); test('should init an AbstractSearchStrategy instance', () => { - expect(abstractSearchStrategy.getCallWithRequestInstance).toBeDefined(); - expect(abstractSearchStrategy.getSearchRequest).toBeDefined(); + expect(abstractSearchStrategy.search).toBeDefined(); expect(abstractSearchStrategy.getFieldsForWildcard).toBeDefined(); expect(abstractSearchStrategy.checkForViability).toBeDefined(); }); @@ -68,17 +52,46 @@ describe('AbstractSearchStrategy', () => { }); }); - test('should invoke callWithRequestFactory with req param passed', () => { - abstractSearchStrategy.getCallWithRequestInstance(req); + test('should return response', async () => { + const searches = [{ body: 'body', index: 'index' }]; + const searchFn = jest.fn().mockReturnValue(Promise.resolve({})); - expect(callWithRequestFactory).toHaveBeenCalledWith(server, req); - }); - - test('should return a search request', () => { - const searchRequest = abstractSearchStrategy.getSearchRequest(req); + const responses = await abstractSearchStrategy.search( + { + requestContext: {}, + framework: { + core: { + getStartServices: jest.fn().mockReturnValue( + Promise.resolve([ + {}, + { + data: { + search: { + search: searchFn, + }, + }, + }, + ]) + ), + }, + }, + }, + searches + ); - expect(searchRequest instanceof SearchRequest).toBe(true); - expect(searchRequest.callWithRequest).toBe('callWithRequest'); - expect(searchRequest.req).toBe(req); + expect(responses).toEqual([{}]); + expect(searchFn).toHaveBeenCalledWith( + {}, + { + params: { + body: 'body', + index: 'index', + }, + indexType: undefined, + }, + { + strategy: 'es', + } + ); }); }); diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts index 0b1c6e6e20414..92b7e6976962e 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts @@ -18,7 +18,7 @@ */ import { - LegacyAPICaller, + RequestHandlerContext, FakeRequest, IUiSettingsClient, SavedObjectsClientContract, @@ -33,6 +33,7 @@ import { IndexPatternsFetcher } from '../../../../../data/server'; * This will be replaced by standard KibanaRequest and RequestContext objects in a later version. */ export type ReqFacade = FakeRequest & { + requestContext: RequestHandlerContext; framework: Framework; payload: unknown; pre: { @@ -40,34 +41,42 @@ export type ReqFacade = FakeRequest & { }; getUiSettingsService: () => IUiSettingsClient; getSavedObjectsClient: () => SavedObjectsClientContract; - server: { - plugins: { - elasticsearch: { - getCluster: () => { - callWithRequest: (req: ReqFacade, endpoint: string, params: any) => Promise; - }; - }; - }; - }; getEsShardTimeout: () => Promise; }; export class AbstractSearchStrategy { - public getCallWithRequestInstance: (req: ReqFacade) => LegacyAPICaller; - public getSearchRequest: (req: ReqFacade) => any; - - constructor( - server: any, - callWithRequestFactory: (server: any, req: ReqFacade) => LegacyAPICaller, - SearchRequest: any - ) { - this.getCallWithRequestInstance = (req) => callWithRequestFactory(server, req); + public searchStrategyName!: string; + public indexType?: string; + public additionalParams: any; - this.getSearchRequest = (req) => { - const callWithRequest = this.getCallWithRequestInstance(req); + constructor(name: string, type?: string, additionalParams: any = {}) { + this.searchStrategyName = name; + this.indexType = type; + this.additionalParams = additionalParams; + } - return new SearchRequest(req, callWithRequest); - }; + async search(req: ReqFacade, bodies: any[], options = {}) { + const [, deps] = await req.framework.core.getStartServices(); + const requests: any[] = []; + bodies.forEach((body) => { + requests.push( + deps.data.search.search( + req.requestContext, + { + params: { + ...body, + ...this.additionalParams, + }, + indexType: this.indexType, + }, + { + ...options, + strategy: this.searchStrategyName, + } + ) + ); + }); + return Promise.all(requests); } async getFieldsForWildcard(req: ReqFacade, indexPattern: string, capabilities: any) { diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.js index 63f2911ce1118..7c3609ae3c405 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.js +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.js @@ -16,21 +16,16 @@ * specific language governing permissions and limitations * under the License. */ + +import { ES_SEARCH_STRATEGY } from '../../../../../data/server'; import { AbstractSearchStrategy } from './abstract_search_strategy'; -import { SearchRequest } from '../search_requests/search_request'; import { DefaultSearchCapabilities } from '../default_search_capabilities'; -const callWithRequestFactory = (server, request) => { - const { callWithRequest } = request.server.plugins.elasticsearch.getCluster('data'); - - return callWithRequest; -}; - export class DefaultSearchStrategy extends AbstractSearchStrategy { name = 'default'; - constructor(server) { - super(server, callWithRequestFactory, SearchRequest); + constructor() { + super(ES_SEARCH_STRATEGY); } checkForViability(req) { diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.js index 2e3a459bf06fd..a9994ba3e1f75 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.js @@ -20,42 +20,20 @@ import { DefaultSearchStrategy } from './default_search_strategy'; describe('DefaultSearchStrategy', () => { let defaultSearchStrategy; - let server; - let callWithRequest; let req; beforeEach(() => { - server = {}; - callWithRequest = jest.fn(); - req = { - server: { - plugins: { - elasticsearch: { - getCluster: jest.fn().mockReturnValue({ - callWithRequest, - }), - }, - }, - }, - }; - defaultSearchStrategy = new DefaultSearchStrategy(server); + req = {}; + defaultSearchStrategy = new DefaultSearchStrategy(); }); test('should init an DefaultSearchStrategy instance', () => { expect(defaultSearchStrategy.name).toBe('default'); expect(defaultSearchStrategy.checkForViability).toBeDefined(); - expect(defaultSearchStrategy.getCallWithRequestInstance).toBeDefined(); - expect(defaultSearchStrategy.getSearchRequest).toBeDefined(); + expect(defaultSearchStrategy.search).toBeDefined(); expect(defaultSearchStrategy.getFieldsForWildcard).toBeDefined(); }); - test('should invoke callWithRequestFactory with passed params', () => { - const value = defaultSearchStrategy.getCallWithRequestInstance(req); - - expect(value).toBe(callWithRequest); - expect(req.server.plugins.elasticsearch.getCluster).toHaveBeenCalledWith('data'); - }); - test('should check a strategy for viability', () => { const value = defaultSearchStrategy.checkForViability(req); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_annotations.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_annotations.js index b015aaf0ef8db..d8a230dfeef4e 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_annotations.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_annotations.js @@ -39,7 +39,6 @@ export async function getAnnotations({ capabilities, series, }) { - const searchRequest = searchStrategy.getSearchRequest(req); const annotations = panel.annotations.filter(validAnnotation); const lastSeriesTimestamp = getLastSeriesTimestamp(series); const handleAnnotationResponseBy = handleAnnotationResponse(lastSeriesTimestamp); @@ -47,6 +46,7 @@ export async function getAnnotations({ const bodiesPromises = annotations.map((annotation) => getAnnotationRequestParams(req, panel, annotation, esQueryConfig, capabilities) ); + const searches = (await Promise.all(bodiesPromises)).reduce( (acc, items) => acc.concat(items), [] @@ -55,10 +55,10 @@ export async function getAnnotations({ if (!searches.length) return { responses: [] }; try { - const data = await searchRequest.search(searches); + const data = await searchStrategy.search(req.framework.core, req.requestContext, searches); return annotations.reduce((acc, annotation, index) => { - acc[annotation.id] = handleAnnotationResponseBy(data[index], annotation); + acc[annotation.id] = handleAnnotationResponseBy(data[index].rawResponse, annotation); return acc; }, {}); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.js index ee48816c6a8af..1eace13c2e336 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.js @@ -28,7 +28,6 @@ export async function getSeriesData(req, panel) { searchStrategy, capabilities, } = await req.framework.searchStrategyRegistry.getViableStrategyForPanel(req, panel); - const searchRequest = searchStrategy.getSearchRequest(req); const esQueryConfig = await getEsQueryConfig(req); const meta = { type: panel.type, @@ -45,8 +44,13 @@ export async function getSeriesData(req, panel) { [] ); - const data = await searchRequest.search(searches); - const series = data.map(handleResponseBody(panel)); + const data = await searchStrategy.search(req, searches); + + const handleResponseBodyFn = handleResponseBody(panel); + + const series = data.map((resp) => + handleResponseBodyFn(resp.rawResponse ? resp.rawResponse : resp) + ); let annotations = null; if (panel.annotations && panel.annotations.length) { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.js index 1d1c245907959..3791eb229db5b 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.js @@ -30,7 +30,6 @@ export async function getTableData(req, panel) { searchStrategy, capabilities, } = await req.framework.searchStrategyRegistry.getViableStrategy(req, panelIndexPattern); - const searchRequest = searchStrategy.getSearchRequest(req); const esQueryConfig = await getEsQueryConfig(req); const { indexPatternObject } = await getIndexPatternObject(req, panelIndexPattern); @@ -41,13 +40,18 @@ export async function getTableData(req, panel) { try { const body = buildRequestBody(req, panel, esQueryConfig, indexPatternObject, capabilities); - const [resp] = await searchRequest.search([ + const [resp] = await searchStrategy.search(req, [ { body, index: panelIndexPattern, }, ]); - const buckets = get(resp, 'aggregations.pivot.buckets', []); + + const buckets = get( + resp.rawResponse ? resp.rawResponse : resp, + 'aggregations.pivot.buckets', + [] + ); return { ...meta, diff --git a/src/plugins/vis_type_timeseries/server/plugin.ts b/src/plugins/vis_type_timeseries/server/plugin.ts index d863937a4e3dc..678ba2b371978 100644 --- a/src/plugins/vis_type_timeseries/server/plugin.ts +++ b/src/plugins/vis_type_timeseries/server/plugin.ts @@ -33,6 +33,7 @@ import { VisTypeTimeseriesConfig } from './config'; import { getVisData, GetVisData, GetVisDataOptions } from './lib/get_vis_data'; import { ValidationTelemetryService } from './validation_telemetry'; import { UsageCollectionSetup } from '../../usage_collection/server'; +import { PluginStart } from '../../data/server'; import { visDataRoutes } from './routes/vis'; // @ts-ignore import { fieldsRoutes } from './routes/fields'; @@ -47,6 +48,10 @@ interface VisTypeTimeseriesPluginSetupDependencies { usageCollection?: UsageCollectionSetup; } +interface VisTypeTimeseriesPluginStartDependencies { + data: PluginStart; +} + export interface VisTypeTimeseriesSetup { getVisData: ( requestContext: RequestHandlerContext, @@ -57,7 +62,7 @@ export interface VisTypeTimeseriesSetup { } export interface Framework { - core: CoreSetup; + core: CoreSetup; plugins: any; config$: Observable; globalConfig$: PluginInitializerContext['config']['legacy']['globalConfig$']; @@ -74,7 +79,10 @@ export class VisTypeTimeseriesPlugin implements Plugin { this.validationTelementryService = new ValidationTelemetryService(); } - public setup(core: CoreSetup, plugins: VisTypeTimeseriesPluginSetupDependencies) { + public setup( + core: CoreSetup, + plugins: VisTypeTimeseriesPluginSetupDependencies + ) { const logger = this.initializerContext.logger.get('visTypeTimeseries'); core.uiSettings.register(uiSettings); const config$ = this.initializerContext.config.create(); diff --git a/src/plugins/vis_type_timeseries/server/routes/vis.ts b/src/plugins/vis_type_timeseries/server/routes/vis.ts index 48efd4398e4d4..1ca8b57ab230f 100644 --- a/src/plugins/vis_type_timeseries/server/routes/vis.ts +++ b/src/plugins/vis_type_timeseries/server/routes/vis.ts @@ -21,7 +21,8 @@ import { IRouter, KibanaRequest } from 'kibana/server'; import { schema } from '@kbn/config-schema'; import { getVisData, GetVisDataOptions } from '../lib/get_vis_data'; import { visPayloadSchema } from '../../common/vis_schema'; -import { Framework, ValidationTelemetryServiceSetup } from '../index'; +import { ValidationTelemetryServiceSetup } from '../index'; +import { Framework } from '../plugin'; const escapeHatch = schema.object({}, { unknowns: 'allow' }); diff --git a/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap b/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap index 3c4c983efa9fa..0925d1c7cc0c9 100644 --- a/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap +++ b/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap @@ -196,7 +196,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` > { - before(() => esArchiver.load('elasticsearch')); - after(() => esArchiver.unload('elasticsearch')); - - it('allows search to specific index', async () => - await supertest.post('/elasticsearch/elasticsearch/_search').expect(200)); - - it('allows msearch', async () => - await supertest - .post('/elasticsearch/_msearch') - .set('content-type', 'application/x-ndjson') - .send( - '{"index":"logstash-2015.04.21","ignore_unavailable":true}\n{"size":500,"sort":{"@timestamp":"desc"},"query":{"bool":{"must":[{"query_string":{"analyze_wildcard":true,"query":"*"}},{"bool":{"must":[{"range":{"@timestamp":{"gte":1429577068175,"lte":1429577968175}}}],"must_not":[]}}],"must_not":[]}},"highlight":{"pre_tags":["@kibana-highlighted-field@"],"post_tags":["@/kibana-highlighted-field@"],"fields":{"*":{}}},"aggs":{"2":{"date_histogram":{"field":"@timestamp","interval":"30s","min_doc_count":0,"extended_bounds":{"min":1429577068175,"max":1429577968175}}}},"stored_fields":["*"],"_source": true,"script_fields":{},"docvalue_fields":["timestamp_offset","@timestamp","utc_time"]}\n' - ) - .expect(200)); - }); -} diff --git a/test/api_integration/apis/index.js b/test/api_integration/apis/index.js index bfbf873cf0616..d07c099634005 100644 --- a/test/api_integration/apis/index.js +++ b/test/api_integration/apis/index.js @@ -20,7 +20,6 @@ export default function ({ loadTestFile }) { describe('apis', () => { loadTestFile(require.resolve('./core')); - loadTestFile(require.resolve('./elasticsearch')); loadTestFile(require.resolve('./general')); loadTestFile(require.resolve('./home')); loadTestFile(require.resolve('./index_patterns')); diff --git a/test/api_integration/apis/telemetry/telemetry_local.js b/test/api_integration/apis/telemetry/telemetry_local.js index d2d61705b763d..9a5467e622ff3 100644 --- a/test/api_integration/apis/telemetry/telemetry_local.js +++ b/test/api_integration/apis/telemetry/telemetry_local.js @@ -154,5 +154,113 @@ export default function ({ getService }) { expect(expected.every((m) => actual.includes(m))).to.be.ok(); }); + + describe('application usage limits', () => { + const timeRange = { + min: '2018-07-23T22:07:00Z', + max: '2018-07-23T22:13:00Z', + }; + + function createSavedObject() { + return supertest + .post('/api/saved_objects/application_usage_transactional') + .send({ + attributes: { + appId: 'test-app', + minutesOnScreen: 10.99, + numberOfClicks: 10, + timestamp: new Date().toISOString(), + }, + }) + .expect(200) + .then((resp) => resp.body.id); + } + + describe('basic behaviour', () => { + let savedObjectId; + before('create 1 entry', async () => { + return createSavedObject().then((id) => (savedObjectId = id)); + }); + after('cleanup', () => { + return supertest + .delete(`/api/saved_objects/application_usage_transactional/${savedObjectId}`) + .expect(200); + }); + + it('should return application_usage data', async () => { + const { body } = await supertest + .post('/api/telemetry/v2/clusters/_stats') + .set('kbn-xsrf', 'xxx') + .send({ timeRange, unencrypted: true }) + .expect(200); + + expect(body.length).to.be(1); + const stats = body[0]; + expect(stats.stack_stats.kibana.plugins.application_usage).to.eql({ + 'test-app': { + clicks_total: 10, + clicks_7_days: 10, + clicks_30_days: 10, + clicks_90_days: 10, + minutes_on_screen_total: 10.99, + minutes_on_screen_7_days: 10.99, + minutes_on_screen_30_days: 10.99, + minutes_on_screen_90_days: 10.99, + }, + }); + }); + }); + + describe('10k + 1', () => { + const savedObjectIds = []; + before('create 10k + 1 entries for application usage', async () => { + await supertest + .post('/api/saved_objects/_bulk_create') + .send( + new Array(10001).fill(0).map(() => ({ + type: 'application_usage_transactional', + attributes: { + appId: 'test-app', + minutesOnScreen: 1, + numberOfClicks: 1, + timestamp: new Date().toISOString(), + }, + })) + ) + .expect(200) + .then((resp) => resp.body.saved_objects.forEach(({ id }) => savedObjectIds.push(id))); + }); + after('clean them all', async () => { + // The SavedObjects API does not allow bulk deleting, and deleting one by one takes ages and the tests timeout + await es.deleteByQuery({ + index: '.kibana', + body: { query: { term: { type: 'application_usage_transactional' } } }, + }); + }); + + it("should only use the first 10k docs for the application_usage data (they'll be rolled up in a later process)", async () => { + const { body } = await supertest + .post('/api/telemetry/v2/clusters/_stats') + .set('kbn-xsrf', 'xxx') + .send({ timeRange, unencrypted: true }) + .expect(200); + + expect(body.length).to.be(1); + const stats = body[0]; + expect(stats.stack_stats.kibana.plugins.application_usage).to.eql({ + 'test-app': { + clicks_total: 10000, + clicks_7_days: 10000, + clicks_30_days: 10000, + clicks_90_days: 10000, + minutes_on_screen_total: 10000, + minutes_on_screen_7_days: 10000, + minutes_on_screen_30_days: 10000, + minutes_on_screen_90_days: 10000, + }, + }); + }); + }); + }); }); } diff --git a/test/common/services/security/security.ts b/test/common/services/security/security.ts index fae4c9198cab6..d94822611f80b 100644 --- a/test/common/services/security/security.ts +++ b/test/common/services/security/security.ts @@ -21,7 +21,7 @@ import { Role } from './role'; import { User } from './user'; import { RoleMappings } from './role_mappings'; import { FtrProviderContext } from '../../ftr_provider_context'; -import { createTestUserService } from './test_user'; +import { createTestUserService, TestUserSupertestProvider } from './test_user'; export async function SecurityServiceProvider(context: FtrProviderContext) { const { getService } = context; @@ -31,11 +31,13 @@ export async function SecurityServiceProvider(context: FtrProviderContext) { const role = new Role(log, kibanaServer); const user = new User(log, kibanaServer); const testUser = await createTestUserService(role, user, context); + const testUserSupertest = TestUserSupertestProvider(context); return new (class SecurityService { roleMappings = new RoleMappings(log, kibanaServer); testUser = testUser; role = role; user = user; + testUserSupertest = testUserSupertest; })(); } diff --git a/test/common/services/security/test_user.ts b/test/common/services/security/test_user.ts index 83eac78621a53..7183943591c88 100644 --- a/test/common/services/security/test_user.ts +++ b/test/common/services/security/test_user.ts @@ -16,12 +16,18 @@ * specific language governing permissions and limitations * under the License. */ +import { format as formatUrl } from 'url'; +import supertestAsPromised from 'supertest-as-promised'; + import { Role } from './role'; import { User } from './user'; import { FtrProviderContext } from '../../ftr_provider_context'; import { Browser } from '../../../functional/services/common'; import { TestSubjects } from '../../../functional/services/common'; +const TEST_USER_NAME = 'test_user'; +const TEST_USER_PASSWORD = 'changeme'; + export async function createTestUserService( role: Role, user: User, @@ -50,15 +56,15 @@ export async function createTestUserService( } try { // delete the test_user if present (will it error if the user doesn't exist?) - await user.delete('test_user'); + await user.delete(TEST_USER_NAME); } catch (exception) { log.debug('no test user to delete'); } // create test_user with username and pwd log.debug(`default roles = ${config.get('security.defaultRoles')}`); - await user.create('test_user', { - password: 'changeme', + await user.create(TEST_USER_NAME, { + password: TEST_USER_PASSWORD, roles: config.get('security.defaultRoles'), full_name: 'test user', }); @@ -74,13 +80,16 @@ export async function createTestUserService( async setRoles(roles: string[], shouldRefreshBrowser: boolean = true) { if (isEnabled()) { log.debug(`set roles = ${roles}`); - await user.create('test_user', { - password: 'changeme', + await user.create(TEST_USER_NAME, { + password: TEST_USER_PASSWORD, roles, full_name: 'test user', }); if (browser && testSubjects && shouldRefreshBrowser) { + // accept alert if it pops up + const alert = await browser.getAlert(); + await alert?.accept(); if (await testSubjects.exists('kibanaChrome', { allowHidden: true })) { await browser.refresh(); await testSubjects.find('kibanaChrome', config.get('timeouts.find') * 10); @@ -90,3 +99,15 @@ export async function createTestUserService( } })(); } + +export function TestUserSupertestProvider({ getService }: FtrProviderContext) { + const config = getService('config'); + const kibanaServerConfig = config.get('servers.kibana'); + + return supertestAsPromised( + formatUrl({ + ...kibanaServerConfig, + auth: `${TEST_USER_NAME}:${TEST_USER_PASSWORD}`, + }) + ); +} diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index 31f4e393f019e..459f596b30256 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -496,6 +496,10 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo const input = await find.byCssSelector('.euiFilePicker__input'); await input.type(path); } + + async scrollKibanaBodyTop() { + await browser.setScrollToById('kibana-body', 0, 0); + } } return new CommonPage(); diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index 5a224d930ee42..7a99509257bf7 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -254,7 +254,7 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider } public async getSidebarWidth() { - const sidebar = await find.byCssSelector('.sidebar-list'); + const sidebar = await testSubjects.find('discover-sidebar'); return await sidebar.getAttribute('clientWidth'); } diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index 43e58a0a2e58d..cf75d5ad7c103 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -356,6 +356,7 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider } async clickAddNewIndexPatternButton() { + await PageObjects.common.scrollKibanaBodyTop(); await testSubjects.click('createIndexPatternButton'); } diff --git a/test/functional/services/common/browser.ts b/test/functional/services/common/browser.ts index 2f8e87c1d58d6..daf1659f0cfe1 100644 --- a/test/functional/services/common/browser.ts +++ b/test/functional/services/common/browser.ts @@ -480,6 +480,12 @@ export async function BrowserProvider({ getService }: FtrProviderContext) { return this.getScrollTop(); } + public async setScrollToById(elementId: string, xCoord: number, yCoord: number) { + await driver.executeScript( + `document.getElementById("${elementId}").scrollTo(${xCoord},${yCoord})` + ); + } + public async setScrollLeft(scrollSize: number | string) { await driver.executeScript('document.body.scrollLeft = ' + scrollSize); return this.getScrollLeft(); diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json index 7f3744c16397a..ace6a48ed8ff5 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json @@ -12,7 +12,7 @@ "build": "rm -rf './target' && tsc" }, "devDependencies": { - "@elastic/eui": "28.2.0", + "@elastic/eui": "28.4.0", "@kbn/plugin-helpers": "1.0.0", "react": "^16.12.0", "react-dom": "^16.12.0", diff --git a/test/plugin_functional/plugins/kbn_sample_panel_action/package.json b/test/plugin_functional/plugins/kbn_sample_panel_action/package.json index acb0cf67ac5c7..d98fa468bd6d1 100644 --- a/test/plugin_functional/plugins/kbn_sample_panel_action/package.json +++ b/test/plugin_functional/plugins/kbn_sample_panel_action/package.json @@ -12,7 +12,7 @@ "build": "rm -rf './target' && tsc" }, "devDependencies": { - "@elastic/eui": "28.2.0", + "@elastic/eui": "28.4.0", "react": "^16.12.0", "typescript": "4.0.2" } diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json index ff84c25400af0..3ac03b444deaf 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json @@ -12,7 +12,7 @@ "build": "rm -rf './target' && tsc" }, "devDependencies": { - "@elastic/eui": "28.2.0", + "@elastic/eui": "28.4.0", "@kbn/plugin-helpers": "1.0.0", "react": "^16.12.0", "typescript": "4.0.2" diff --git a/test/scripts/jenkins_visual_regression.sh b/test/scripts/jenkins_baseline.sh similarity index 63% rename from test/scripts/jenkins_visual_regression.sh rename to test/scripts/jenkins_baseline.sh index 17345d4301882..e679ac7f31bd1 100755 --- a/test/scripts/jenkins_visual_regression.sh +++ b/test/scripts/jenkins_baseline.sh @@ -9,10 +9,3 @@ linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" installDir="$PARENT_DIR/install/kibana" mkdir -p "$installDir" tar -xzf "$linuxBuild" -C "$installDir" --strip=1 - -echo " -> running visual regression tests from kibana directory" -yarn percy exec -t 10000 -- -- \ - node scripts/functional_tests \ - --debug --bail \ - --kibana-install-dir "$installDir" \ - --config test/visual_regression/config.ts; diff --git a/test/scripts/jenkins_xpack_visual_regression.sh b/test/scripts/jenkins_xpack_baseline.sh similarity index 64% rename from test/scripts/jenkins_xpack_visual_regression.sh rename to test/scripts/jenkins_xpack_baseline.sh index 55d4a524820c5..7577b6927d166 100755 --- a/test/scripts/jenkins_xpack_visual_regression.sh +++ b/test/scripts/jenkins_xpack_baseline.sh @@ -14,16 +14,5 @@ tar -xzf "$linuxBuild" -C "$installDir" --strip=1 mkdir -p "$WORKSPACE/kibana-build-xpack" cp -pR install/kibana/. $WORKSPACE/kibana-build-xpack/ -# cd "$KIBANA_DIR" -# source "test/scripts/jenkins_xpack_page_load_metrics.sh" - cd "$KIBANA_DIR" source "test/scripts/jenkins_xpack_saved_objects_field_metrics.sh" - -echo " -> running visual regression tests from x-pack directory" -cd "$XPACK_DIR" -yarn percy exec -t 10000 -- -- \ - node scripts/functional_tests \ - --debug --bail \ - --kibana-install-dir "$installDir" \ - --config test/visual_regression/config.ts; diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index e5b39584a519b..28eb94405abbb 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -1,4 +1,4 @@ -def withPostBuildReporting(Closure closure) { +def withPostBuildReporting(Map params, Closure closure) { try { closure() } finally { @@ -9,8 +9,10 @@ def withPostBuildReporting(Closure closure) { print ex } - catchErrors { - runErrorReporter([pwd()] + parallelWorkspaces) + if (params.runErrorReporter) { + catchErrors { + runErrorReporter([pwd()] + parallelWorkspaces) + } } catchErrors { diff --git a/vars/workers.groovy b/vars/workers.groovy index e582e996a78b5..b6ff5b27667dd 100644 --- a/vars/workers.groovy +++ b/vars/workers.groovy @@ -118,11 +118,11 @@ def base(Map params, Closure closure) { // Worker for ci processes. Extends the base worker and adds GCS artifact upload, error reporting, junit processing def ci(Map params, Closure closure) { - def config = [ramDisk: true, bootstrapped: true] + params + def config = [ramDisk: true, bootstrapped: true, runErrorReporter: true] + params return base(config) { kibanaPipeline.withGcsArtifactUpload(config.name) { - kibanaPipeline.withPostBuildReporting { + kibanaPipeline.withPostBuildReporting(config) { closure() } } diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index a693e008db6ea..e6f160ce8c654 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -18,7 +18,6 @@ export function createJestConfig({ kibanaDirectory, rootDir, xPackKibanaDirector '^src/core/(.*)': `${kibanaDirectory}/src/core/$1`, '^src/legacy/(.*)': `${kibanaDirectory}/src/legacy/$1`, '^src/plugins/(.*)': `${kibanaDirectory}/src/plugins/$1`, - '^plugins/([^/.]*)(.*)': `${kibanaDirectory}/src/legacy/core_plugins/$1/public$2`, '^legacy/plugins/xpack_main/(.*);': `${xPackKibanaDirectory}/legacy/plugins/xpack_main/public/$1`, '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': fileMockPath, '\\.module.(css|scss)$': `${kibanaDirectory}/src/dev/jest/mocks/css_module_mock.js`, diff --git a/x-pack/index.js b/x-pack/index.js index 074b8e6859dc2..745b4bd72dde8 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -5,9 +5,8 @@ */ import { xpackMain } from './legacy/plugins/xpack_main'; -import { security } from './legacy/plugins/security'; import { spaces } from './legacy/plugins/spaces'; module.exports = function (kibana) { - return [xpackMain(kibana), spaces(kibana), security(kibana)]; + return [xpackMain(kibana), spaces(kibana)]; }; diff --git a/x-pack/legacy/plugins/security/index.ts b/x-pack/legacy/plugins/security/index.ts deleted file mode 100644 index c3596d3745e57..0000000000000 --- a/x-pack/legacy/plugins/security/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Root } from 'joi'; -import { resolve } from 'path'; - -export const security = (kibana: Record) => - new kibana.Plugin({ - id: 'security', - publicDir: resolve(__dirname, 'public'), - require: ['elasticsearch'], - configPrefix: 'xpack.security', - config: (Joi: Root) => - Joi.object({ enabled: Joi.boolean().default(true) }) - .unknown() - .default(), - init() {}, - }); diff --git a/x-pack/legacy/plugins/spaces/index.ts b/x-pack/legacy/plugins/spaces/index.ts index 725d022879e0d..aec06a4596203 100644 --- a/x-pack/legacy/plugins/spaces/index.ts +++ b/x-pack/legacy/plugins/spaces/index.ts @@ -16,7 +16,7 @@ export const spaces = (kibana: Record) => id: 'spaces', configPrefix: 'xpack.spaces', publicDir: resolve(__dirname, 'public'), - require: ['elasticsearch', 'xpack_main'], + require: ['xpack_main'], config(Joi: any) { return Joi.object({ enabled: Joi.boolean().default(true), diff --git a/x-pack/legacy/plugins/xpack_main/index.js b/x-pack/legacy/plugins/xpack_main/index.js index 854fba6624719..a3bd66e744fda 100644 --- a/x-pack/legacy/plugins/xpack_main/index.js +++ b/x-pack/legacy/plugins/xpack_main/index.js @@ -5,16 +5,15 @@ */ import { resolve } from 'path'; -import { mirrorPluginStatus } from '../../server/lib/mirror_plugin_status'; import { setupXPackMain } from './server/lib/setup_xpack_main'; -import { xpackInfoRoute, settingsRoute } from './server/routes/api/v1'; +import { xpackInfoRoute } from './server/routes/api/v1'; export const xpackMain = (kibana) => { return new kibana.Plugin({ id: 'xpack_main', configPrefix: 'xpack.xpack_main', publicDir: resolve(__dirname, 'public'), - require: ['elasticsearch'], + require: [], config(Joi) { return Joi.object({ @@ -23,13 +22,10 @@ export const xpackMain = (kibana) => { }, init(server) { - mirrorPluginStatus(server.plugins.elasticsearch, this, 'yellow', 'red'); - setupXPackMain(server); // register routes xpackInfoRoute(server); - settingsRoute(server, this.kbnServer); }, }); }; diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/setup_xpack_main.js b/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/setup_xpack_main.js index c34e27642d2ce..f49f44bed97a7 100644 --- a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/setup_xpack_main.js +++ b/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/setup_xpack_main.js @@ -13,33 +13,35 @@ describe('setupXPackMain()', () => { const sandbox = sinon.createSandbox(); let mockServer; + let mockStatusObservable; let mockElasticsearchPlugin; - let mockXPackMainPlugin; beforeEach(() => { sandbox.useFakeTimers(); mockElasticsearchPlugin = { getCluster: sinon.stub(), - status: sinon.stub({ - on() {}, - }), }; - mockXPackMainPlugin = { - status: sinon.stub({ - green() {}, - red() {}, - }), - }; + mockStatusObservable = sinon.stub({ subscribe() {} }); mockServer = sinon.stub({ plugins: { elasticsearch: mockElasticsearchPlugin, - xpack_main: mockXPackMainPlugin, }, newPlatform: { - setup: { plugins: { features: {}, licensing: { license$: new BehaviorSubject() } } }, + setup: { + core: { + status: { + core$: { + pipe() { + return mockStatusObservable; + }, + }, + }, + }, + plugins: { features: {}, licensing: { license$: new BehaviorSubject() } }, + }, }, events: { on() {} }, log() {}, @@ -61,55 +63,6 @@ describe('setupXPackMain()', () => { setupXPackMain(mockServer); sinon.assert.calledWithExactly(mockServer.expose, 'info', sinon.match.instanceOf(XPackInfo)); - sinon.assert.calledWithExactly(mockElasticsearchPlugin.status.on, 'change', sinon.match.func); - }); - - describe('Elasticsearch plugin state changes cause XPackMain plugin state change.', () => { - let xPackInfo; - let onElasticsearchPluginStatusChange; - beforeEach(() => { - setupXPackMain(mockServer); - - onElasticsearchPluginStatusChange = mockElasticsearchPlugin.status.on.withArgs('change') - .firstCall.args[1]; - xPackInfo = mockServer.expose.firstCall.args[1]; - }); - - it('if `XPackInfo` is available status will become `green`.', async () => { - sinon.stub(xPackInfo, 'isAvailable').returns(false); - // We need this to make sure the code waits for `refreshNow` to complete before it tries - // to access its properties. - sinon.stub(xPackInfo, 'refreshNow').callsFake(() => { - return new Promise((resolve) => { - xPackInfo.isAvailable.returns(true); - resolve(); - }); - }); - - await onElasticsearchPluginStatusChange(); - - sinon.assert.calledWithExactly(mockXPackMainPlugin.status.green, 'Ready'); - sinon.assert.notCalled(mockXPackMainPlugin.status.red); - }); - - it('if `XPackInfo` is not available status will become `red`.', async () => { - sinon.stub(xPackInfo, 'isAvailable').returns(true); - sinon.stub(xPackInfo, 'unavailableReason').returns(''); - - // We need this to make sure the code waits for `refreshNow` to complete before it tries - // to access its properties. - sinon.stub(xPackInfo, 'refreshNow').callsFake(() => { - return new Promise((resolve) => { - xPackInfo.isAvailable.returns(false); - xPackInfo.unavailableReason.returns('Some weird error.'); - resolve(); - }); - }); - - await onElasticsearchPluginStatusChange(); - - sinon.assert.calledWithExactly(mockXPackMainPlugin.status.red, 'Some weird error.'); - sinon.assert.notCalled(mockXPackMainPlugin.status.green); - }); + sinon.assert.calledWithExactly(mockStatusObservable.subscribe, sinon.match.func); }); }); diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/setup_xpack_main.js b/x-pack/legacy/plugins/xpack_main/server/lib/setup_xpack_main.js index 33b551bbe864f..fd4e3c86d0ca7 100644 --- a/x-pack/legacy/plugins/xpack_main/server/lib/setup_xpack_main.js +++ b/x-pack/legacy/plugins/xpack_main/server/lib/setup_xpack_main.js @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { pairwise } from 'rxjs/operators'; import { XPackInfo } from './xpack_info'; /** @@ -19,23 +20,14 @@ export function setupXPackMain(server) { server.expose('info', info); - const setPluginStatus = () => { - if (info.isAvailable()) { - server.plugins.xpack_main.status.green('Ready'); - } else { - server.plugins.xpack_main.status.red(info.unavailableReason()); - } - }; - // trigger an xpack info refresh whenever the elasticsearch plugin status changes - server.plugins.elasticsearch.status.on('change', async () => { - await info.refreshNow(); - setPluginStatus(); - }); - - // whenever the license info is updated, regardless of the elasticsearch plugin status - // changes, reflect the change in our plugin status. See https://github.com/elastic/kibana/issues/20017 - info.onLicenseInfoChange(setPluginStatus); + server.newPlatform.setup.core.status.core$ + .pipe(pairwise()) + .subscribe(async ([coreLast, coreCurrent]) => { + if (coreLast.elasticsearch.level !== coreCurrent.elasticsearch.level) { + await info.refreshNow(); + } + }); return info; } diff --git a/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/index.js b/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/index.js index c0e59b4ea4ab2..80baf7bf1a64d 100644 --- a/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/index.js +++ b/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/index.js @@ -5,4 +5,3 @@ */ export { xpackInfoRoute } from './xpack_info'; -export { settingsRoute } from './settings'; diff --git a/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/settings.js b/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/settings.js deleted file mode 100644 index 34fc4d97c1328..0000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/settings.js +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { boomify } from 'boom'; -import { get } from 'lodash'; -import { KIBANA_SETTINGS_TYPE } from '../../../../../../../plugins/monitoring/common/constants'; - -const getClusterUuid = async (callCluster) => { - const { cluster_uuid: uuid } = await callCluster('info', { filterPath: 'cluster_uuid' }); - return uuid; -}; - -export function settingsRoute(server, kbnServer) { - server.route({ - path: '/api/settings', - method: 'GET', - async handler(req) { - const { server } = req; - const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); - const callCluster = (...args) => callWithRequest(req, ...args); // All queries from HTTP API must use authentication headers from the request - - try { - const { usageCollection } = server.newPlatform.setup.plugins; - const settingsCollector = usageCollection.getCollectorByType(KIBANA_SETTINGS_TYPE); - - let settings = await settingsCollector.fetch(callCluster); - if (!settings) { - settings = settingsCollector.getEmailValueStructure(null); - } - const uuid = await getClusterUuid(callCluster); - - const snapshotRegex = /-snapshot/i; - const config = server.config(); - const status = kbnServer.status.toJSON(); - const kibana = { - uuid: config.get('server.uuid'), - name: config.get('server.name'), - index: config.get('kibana.index'), - host: config.get('server.host'), - port: config.get('server.port'), - locale: config.get('i18n.locale'), - transport_address: `${config.get('server.host')}:${config.get('server.port')}`, - version: kbnServer.version.replace(snapshotRegex, ''), - snapshot: snapshotRegex.test(kbnServer.version), - status: get(status, 'overall.state'), - }; - - return { - cluster_uuid: uuid, - settings: { - ...settings, - kibana, - }, - }; - } catch (err) { - req.log(['error'], err); // FIXME doesn't seem to log anything useful if ES times out - if (err.isBoom) { - return err; - } else { - return boomify(err, { statusCode: err.statusCode, message: err.message }); - } - } - }, - }); -} diff --git a/x-pack/legacy/server/lib/__tests__/mirror_plugin_status.js b/x-pack/legacy/server/lib/__tests__/mirror_plugin_status.js deleted file mode 100644 index c7cae0785c9eb..0000000000000 --- a/x-pack/legacy/server/lib/__tests__/mirror_plugin_status.js +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import EventEmitter from 'events'; -import expect from '@kbn/expect'; -import { mirrorPluginStatus } from '../mirror_plugin_status'; - -describe('mirror_plugin_status', () => { - class MockPluginStatus extends EventEmitter { - constructor() { - super(); - this.state = 'uninitialized'; - } - - _changeState(newState, newMessage) { - if (this.state === newState) { - return; - } - const prevState = this.state; - const prevMessage = this.message; - - this.state = newState; - this.message = newMessage; - - this.emit(newState, prevState, prevMessage, this.state, this.message); - this.emit('change', prevState, prevMessage, this.state, this.message); - } - - red(message) { - this._changeState('red', message); - } - yellow(message) { - this._changeState('yellow', message); - } - green(message) { - this._changeState('green', message); - } - uninitialized(message) { - this._changeState('uninitialized', message); - } - } - - class MockPlugin { - constructor() { - this.status = new MockPluginStatus(); - } - } - - let upstreamPlugin; - let downstreamPlugin; - let eventNotEmittedTimeout; - - beforeEach(() => { - upstreamPlugin = new MockPlugin(); - downstreamPlugin = new MockPlugin(); - eventNotEmittedTimeout = setTimeout(() => { - throw new Error('Event should have been emitted'); - }, 100); - }); - - it('should mirror all downstream plugin statuses to upstream plugin statuses', (done) => { - mirrorPluginStatus(upstreamPlugin, downstreamPlugin); - downstreamPlugin.status.on('change', () => { - clearTimeout(eventNotEmittedTimeout); - expect(downstreamPlugin.status.state).to.be('red'); - expect(downstreamPlugin.status.message).to.be('test message'); - done(); - }); - upstreamPlugin.status.red('test message'); - }); - - describe('should only mirror specific downstream plugin statuses to corresponding upstream plugin statuses: ', () => { - beforeEach(() => { - mirrorPluginStatus(upstreamPlugin, downstreamPlugin, 'yellow', 'red'); - }); - - it('yellow', (done) => { - downstreamPlugin.status.on('change', () => { - clearTimeout(eventNotEmittedTimeout); - expect(downstreamPlugin.status.state).to.be('yellow'); - expect(downstreamPlugin.status.message).to.be('test yellow message'); - done(); - }); - upstreamPlugin.status.yellow('test yellow message'); - }); - - it('red', (done) => { - downstreamPlugin.status.on('change', () => { - clearTimeout(eventNotEmittedTimeout); - expect(downstreamPlugin.status.state).to.be('red'); - expect(downstreamPlugin.status.message).to.be('test red message'); - done(); - }); - upstreamPlugin.status.red('test red message'); - }); - - it('not green', () => { - clearTimeout(eventNotEmittedTimeout); // because event should not be emitted in this test - downstreamPlugin.status.on('change', () => { - throw new Error('Event should NOT have been emitted'); - }); - upstreamPlugin.status.green('test green message'); - }); - }); -}); diff --git a/x-pack/legacy/server/lib/create_router/call_with_request_factory/index.d.ts b/x-pack/legacy/server/lib/create_router/call_with_request_factory/index.d.ts index 4ec5bc13eea81..3537d1bf42079 100644 --- a/x-pack/legacy/server/lib/create_router/call_with_request_factory/index.d.ts +++ b/x-pack/legacy/server/lib/create_router/call_with_request_factory/index.d.ts @@ -5,9 +5,9 @@ */ import { Legacy } from 'kibana'; -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; +import { LegacyAPICaller } from '../../../../../../src/core/server'; -export type CallWithRequest = (...args: any[]) => CallCluster; +export type CallWithRequest = (...args: any[]) => LegacyAPICaller; export declare function callWithRequestFactory( server: Legacy.Server, diff --git a/x-pack/legacy/server/lib/mirror_plugin_status.js b/x-pack/legacy/server/lib/mirror_plugin_status.js deleted file mode 100644 index 7b1ac215f5e4c..0000000000000 --- a/x-pack/legacy/server/lib/mirror_plugin_status.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export function mirrorPluginStatus(upstreamPlugin, downstreamPlugin, ...statesToMirror) { - upstreamPlugin.status.setMaxListeners(21); // We need more than the default, which is 10 - - function mirror(previousState, previousMsg, newState, newMsg) { - if (newState) { - downstreamPlugin.status[newState](newMsg); - } - } - - if (statesToMirror.length === 0) { - statesToMirror.push('change'); - } - - statesToMirror.map((state) => upstreamPlugin.status.on(state, mirror)); - mirror(null, null, upstreamPlugin.status.state, upstreamPlugin.status.message); // initial mirroring -} diff --git a/x-pack/legacy/server/lib/register_license_checker/register_license_checker.js b/x-pack/legacy/server/lib/register_license_checker/register_license_checker.js index eabe375eefd02..57cbe30c25cb2 100644 --- a/x-pack/legacy/server/lib/register_license_checker/register_license_checker.js +++ b/x-pack/legacy/server/lib/register_license_checker/register_license_checker.js @@ -4,21 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mirrorPluginStatus } from '../mirror_plugin_status'; +import { pairwise } from 'rxjs/operators'; + +import { ServiceStatusLevels } from '../../../../../src/core/server'; import { checkLicense } from '../check_license'; export function registerLicenseChecker(server, pluginId, pluginName, minimumLicenseRequired) { const xpackMainPlugin = server.plugins.xpack_main; - const thisPlugin = server.plugins[pluginId]; + const subscription = server.newPlatform.setup.core.status.core$ + .pipe(pairwise()) + .subscribe(([coreLast, coreCurrent]) => { + if ( + !subscription.closed && + coreLast.elasticsearch.level !== ServiceStatusLevels.available && + coreCurrent.elasticsearch.level === ServiceStatusLevels.available + ) { + // Unsubscribe as soon as ES becomes available so this function only runs once + subscription.unsubscribe(); - mirrorPluginStatus(xpackMainPlugin, thisPlugin); - xpackMainPlugin.status.once('green', () => { - // Register a function that is called whenever the xpack info changes, - // to re-compute the license check results for this plugin - xpackMainPlugin.info - .feature(pluginId) - .registerLicenseCheckResultsGenerator((xpackLicenseInfo) => { - return checkLicense(pluginName, minimumLicenseRequired, xpackLicenseInfo); - }); - }); + // Register a function that is called whenever the xpack info changes, + // to re-compute the license check results for this plugin + xpackMainPlugin.info + .feature(pluginId) + .registerLicenseCheckResultsGenerator((xpackLicenseInfo) => { + return checkLicense(pluginName, minimumLicenseRequired, xpackLicenseInfo); + }); + } + }); } diff --git a/x-pack/package.json b/x-pack/package.json index 3e1e1173be2d7..de0bf0d922b42 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -39,7 +39,7 @@ "@kbn/storybook": "1.0.0", "@kbn/test": "1.0.0", "@kbn/utility-types": "1.0.0", - "@mapbox/geojson-rewind": "^0.4.1", + "@mapbox/geojson-rewind": "^0.5.0", "@mapbox/mapbox-gl-draw": "^1.2.0", "@mapbox/mapbox-gl-rtl-text": "^0.2.3", "@scant/router": "^0.1.0", @@ -101,7 +101,7 @@ "@types/mocha": "^7.0.2", "@types/nock": "^10.0.3", "@types/node": ">=10.17.17 <10.20.0", - "@types/node-fetch": "^2.5.0", + "@types/node-fetch": "^2.5.7", "@types/nodemailer": "^6.2.1", "@types/object-hash": "^1.3.0", "@types/papaparse": "^5.0.3", @@ -209,7 +209,6 @@ "mochawesome-merge": "^4.1.0", "mustache": "^2.3.0", "mutation-observer": "^1.0.3", - "node-fetch": "^2.6.0", "null-loader": "^3.0.0", "oboe": "^2.1.4", "pixelmatch": "^5.1.0", @@ -275,7 +274,7 @@ "@babel/runtime": "^7.11.2", "@elastic/datemath": "5.0.3", "@elastic/ems-client": "7.9.3", - "@elastic/eui": "28.2.0", + "@elastic/eui": "28.4.0", "@elastic/filesaver": "1.1.2", "@elastic/node-crypto": "1.2.1", "@elastic/numeral": "^2.5.0", @@ -345,7 +344,7 @@ "moment-timezone": "^0.5.27", "ngreact": "^0.5.1", "nock": "12.0.3", - "node-fetch": "^2.6.0", + "node-fetch": "^2.6.1", "nodemailer": "^4.7.0", "object-hash": "^1.3.1", "object-path-immutable": "^3.1.1", diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index 573fb0e1be580..adef12454f2d5 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -893,7 +893,7 @@ describe('update()', () => { }, references: [], }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: 'my-action', type: 'action', attributes: { @@ -946,7 +946,7 @@ describe('update()', () => { }, references: [], }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: 'my-action', type: 'action', attributes: { @@ -972,17 +972,21 @@ describe('update()', () => { name: 'my name', config: {}, }); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toMatchInlineSnapshot(` Array [ "action", - "my-action", Object { "actionTypeId": "my-action-type", "config": Object {}, "name": "my name", "secrets": Object {}, }, + Object { + "id": "my-action", + "overwrite": true, + "references": Array [], + }, ] `); expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); @@ -1043,7 +1047,7 @@ describe('update()', () => { }, references: [], }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: 'my-action', type: 'action', attributes: { @@ -1081,11 +1085,10 @@ describe('update()', () => { c: true, }, }); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toMatchInlineSnapshot(` Array [ "action", - "my-action", Object { "actionTypeId": "my-action-type", "config": Object { @@ -1096,6 +1099,11 @@ describe('update()', () => { "name": "my name", "secrets": Object {}, }, + Object { + "id": "my-action", + "overwrite": true, + "references": Array [], + }, ] `); }); @@ -1118,7 +1126,7 @@ describe('update()', () => { }, references: [], }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: 'my-action', type: 'action', attributes: { diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index 06c9555f3a18d..4079a6ddeeb8a 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -13,6 +13,7 @@ import { } from 'src/core/server'; import { i18n } from '@kbn/i18n'; +import { omitBy, isUndefined } from 'lodash'; import { ActionTypeRegistry } from './action_type_registry'; import { validateConfig, validateSecrets, ActionExecutorContract } from './lib'; import { @@ -30,7 +31,10 @@ import { } from './create_execute_function'; import { ActionsAuthorization } from './authorization/actions_authorization'; import { ActionType } from '../common'; -import { shouldLegacyRbacApplyBySource } from './authorization/should_legacy_rbac_apply_by_source'; +import { + getAuthorizationModeBySource, + AuthorizationMode, +} from './authorization/get_authorization_mode_by_source'; // We are assuming there won't be many actions. This is why we will load // all the actions in advance and assume the total count to not go over 10000. @@ -151,8 +155,10 @@ export class ActionsClient { 'update' ); } - const existingObject = await this.unsecuredSavedObjectsClient.get('action', id); - const { actionTypeId } = existingObject.attributes; + const { attributes, references, version } = await this.unsecuredSavedObjectsClient.get< + RawAction + >('action', id); + const { actionTypeId } = attributes; const { name, config, secrets } = action; const actionType = this.actionTypeRegistry.get(actionTypeId); const validatedActionTypeConfig = validateConfig(actionType, config); @@ -160,12 +166,25 @@ export class ActionsClient { this.actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); - const result = await this.unsecuredSavedObjectsClient.update('action', id, { - actionTypeId, - name, - config: validatedActionTypeConfig as SavedObjectAttributes, - secrets: validatedActionTypeSecrets as SavedObjectAttributes, - }); + const result = await this.unsecuredSavedObjectsClient.create( + 'action', + { + ...attributes, + actionTypeId, + name, + config: validatedActionTypeConfig as SavedObjectAttributes, + secrets: validatedActionTypeSecrets as SavedObjectAttributes, + }, + omitBy( + { + id, + overwrite: true, + references, + version, + }, + isUndefined + ) + ); return { id, @@ -301,7 +320,10 @@ export class ActionsClient { params, source, }: Omit): Promise> { - if (!(await shouldLegacyRbacApplyBySource(this.unsecuredSavedObjectsClient, source))) { + if ( + (await getAuthorizationModeBySource(this.unsecuredSavedObjectsClient, source)) === + AuthorizationMode.RBAC + ) { await this.authorization.ensureAuthorized('execute'); } return this.actionExecutor.execute({ actionId, params, source, request: this.request }); @@ -309,7 +331,10 @@ export class ActionsClient { public async enqueueExecution(options: EnqueueExecutionOptions): Promise { const { source } = options; - if (!(await shouldLegacyRbacApplyBySource(this.unsecuredSavedObjectsClient, source))) { + if ( + (await getAuthorizationModeBySource(this.unsecuredSavedObjectsClient, source)) === + AuthorizationMode.RBAC + ) { await this.authorization.ensureAuthorized('execute'); } return this.executionEnqueuer(this.unsecuredSavedObjectsClient, options); diff --git a/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts b/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts index 08c4472f8007b..a19a662f8323c 100644 --- a/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts +++ b/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts @@ -10,6 +10,7 @@ import { actionsAuthorizationAuditLoggerMock } from './audit_logger.mock'; import { ActionsAuthorizationAuditLogger, AuthorizationResult } from './audit_logger'; import { ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from '../saved_objects'; import { AuthenticatedUser } from '../../../security/server'; +import { AuthorizationMode } from './get_authorization_mode_by_source'; const request = {} as KibanaRequest; @@ -195,7 +196,7 @@ describe('ensureAuthorized', () => { `); }); - test('exempts users from requiring privileges to execute actions when shouldUseLegacyRbac is true', async () => { + test('exempts users from requiring privileges to execute actions when authorizationMode is Legacy', async () => { const { authorization, authentication } = mockSecurity(); const checkPrivileges: jest.MockedFunction { authorization, authentication, auditLogger, - shouldUseLegacyRbac: true, + authorizationMode: AuthorizationMode.Legacy, }); authentication.getCurrentUser.mockReturnValueOnce(({ diff --git a/x-pack/plugins/actions/server/authorization/actions_authorization.ts b/x-pack/plugins/actions/server/authorization/actions_authorization.ts index bd6e355c2cf9d..cad58bed50981 100644 --- a/x-pack/plugins/actions/server/authorization/actions_authorization.ts +++ b/x-pack/plugins/actions/server/authorization/actions_authorization.ts @@ -9,6 +9,7 @@ import { KibanaRequest } from 'src/core/server'; import { SecurityPluginSetup } from '../../../security/server'; import { ActionsAuthorizationAuditLogger } from './audit_logger'; import { ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE } from '../saved_objects'; +import { AuthorizationMode } from './get_authorization_mode_by_source'; export interface ConstructorOptions { request: KibanaRequest; @@ -22,7 +23,7 @@ export interface ConstructorOptions { // actions to continue to execute - which requires that we exempt auth on // `get` for Connectors and `execute` for Action execution when used by // these legacy alerts - shouldUseLegacyRbac?: boolean; + authorizationMode?: AuthorizationMode; } const operationAlias: Record< @@ -43,20 +44,19 @@ export class ActionsAuthorization { private readonly authorization?: SecurityPluginSetup['authz']; private readonly authentication?: SecurityPluginSetup['authc']; private readonly auditLogger: ActionsAuthorizationAuditLogger; - private readonly shouldUseLegacyRbac: boolean; - + private readonly authorizationMode: AuthorizationMode; constructor({ request, authorization, authentication, auditLogger, - shouldUseLegacyRbac = false, + authorizationMode = AuthorizationMode.RBAC, }: ConstructorOptions) { this.request = request; this.authorization = authorization; this.authentication = authentication; this.auditLogger = auditLogger; - this.shouldUseLegacyRbac = shouldUseLegacyRbac; + this.authorizationMode = authorizationMode; } public async ensureAuthorized(operation: string, actionTypeId?: string) { @@ -87,6 +87,9 @@ export class ActionsAuthorization { } private isOperationExemptDueToLegacyRbac(operation: string) { - return this.shouldUseLegacyRbac && LEGACY_RBAC_EXEMPT_OPERATIONS.has(operation); + return ( + this.authorizationMode === AuthorizationMode.Legacy && + LEGACY_RBAC_EXEMPT_OPERATIONS.has(operation) + ); } } diff --git a/x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.test.ts b/x-pack/plugins/actions/server/authorization/get_authorization_mode_by_source.test.ts similarity index 67% rename from x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.test.ts rename to x-pack/plugins/actions/server/authorization/get_authorization_mode_by_source.test.ts index 03062994adeb6..4980c476e60ea 100644 --- a/x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.test.ts +++ b/x-pack/plugins/actions/server/authorization/get_authorization_mode_by_source.test.ts @@ -3,88 +3,93 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { shouldLegacyRbacApplyBySource } from './should_legacy_rbac_apply_by_source'; +import { + getAuthorizationModeBySource, + AuthorizationMode, +} from './get_authorization_mode_by_source'; import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; import uuid from 'uuid'; import { asSavedObjectExecutionSource } from '../lib'; const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); -describe(`#shouldLegacyRbacApplyBySource`, () => { - test('should return false if no source is provided', async () => { - expect(await shouldLegacyRbacApplyBySource(unsecuredSavedObjectsClient)).toEqual(false); +describe(`#getAuthorizationModeBySource`, () => { + test('should return RBAC if no source is provided', async () => { + expect(await getAuthorizationModeBySource(unsecuredSavedObjectsClient)).toEqual( + AuthorizationMode.RBAC + ); }); - test('should return false if source is not an alert', async () => { + test('should return RBAC if source is not an alert', async () => { expect( - await shouldLegacyRbacApplyBySource( + await getAuthorizationModeBySource( unsecuredSavedObjectsClient, asSavedObjectExecutionSource({ type: 'action', id: uuid.v4(), }) ) - ).toEqual(false); + ).toEqual(AuthorizationMode.RBAC); }); - test('should return false if source alert is not marked as legacy', async () => { + test('should return RBAC if source alert is not marked as legacy', async () => { const id = uuid.v4(); unsecuredSavedObjectsClient.get.mockResolvedValue(mockAlert({ id })); expect( - await shouldLegacyRbacApplyBySource( + await getAuthorizationModeBySource( unsecuredSavedObjectsClient, asSavedObjectExecutionSource({ type: 'alert', id, }) ) - ).toEqual(false); + ).toEqual(AuthorizationMode.RBAC); }); - test('should return true if source alert is marked as legacy', async () => { + test('should return Legacy if source alert is marked as legacy', async () => { const id = uuid.v4(); unsecuredSavedObjectsClient.get.mockResolvedValue( mockAlert({ id, attributes: { meta: { versionApiKeyLastmodified: 'pre-7.10.0' } } }) ); expect( - await shouldLegacyRbacApplyBySource( + await getAuthorizationModeBySource( unsecuredSavedObjectsClient, asSavedObjectExecutionSource({ type: 'alert', id, }) ) - ).toEqual(true); + ).toEqual(AuthorizationMode.Legacy); }); - test('should return false if source alert is marked as modern', async () => { + test('should return RBAC if source alert is marked as modern', async () => { const id = uuid.v4(); unsecuredSavedObjectsClient.get.mockResolvedValue( mockAlert({ id, attributes: { meta: { versionApiKeyLastmodified: '7.10.0' } } }) ); expect( - await shouldLegacyRbacApplyBySource( + await getAuthorizationModeBySource( unsecuredSavedObjectsClient, asSavedObjectExecutionSource({ type: 'alert', id, }) ) - ).toEqual(false); + ).toEqual(AuthorizationMode.RBAC); }); - test('should return false if source alert is marked with a last modified version', async () => { + test('should return RBAC if source alert doesnt have a last modified version', async () => { const id = uuid.v4(); unsecuredSavedObjectsClient.get.mockResolvedValue(mockAlert({ id, attributes: { meta: {} } })); expect( - await shouldLegacyRbacApplyBySource( + await getAuthorizationModeBySource( unsecuredSavedObjectsClient, asSavedObjectExecutionSource({ type: 'alert', id, }) ) - ).toEqual(false); + ).toEqual(AuthorizationMode.RBAC); }); }); diff --git a/x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.ts b/x-pack/plugins/actions/server/authorization/get_authorization_mode_by_source.ts similarity index 55% rename from x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.ts rename to x-pack/plugins/actions/server/authorization/get_authorization_mode_by_source.ts index 06d5776003ede..85d646c75defa 100644 --- a/x-pack/plugins/actions/server/authorization/should_legacy_rbac_apply_by_source.ts +++ b/x-pack/plugins/actions/server/authorization/get_authorization_mode_by_source.ts @@ -10,18 +10,24 @@ import { ALERT_SAVED_OBJECT_TYPE } from '../saved_objects'; const LEGACY_VERSION = 'pre-7.10.0'; -export async function shouldLegacyRbacApplyBySource( +export enum AuthorizationMode { + Legacy, + RBAC, +} + +export async function getAuthorizationModeBySource( unsecuredSavedObjectsClient: SavedObjectsClientContract, executionSource?: ActionExecutionSource -): Promise { +): Promise { return isSavedObjectExecutionSource(executionSource) && - executionSource?.source?.type === ALERT_SAVED_OBJECT_TYPE - ? ( - await unsecuredSavedObjectsClient.get<{ - meta?: { - versionApiKeyLastmodified?: string; - }; - }>(ALERT_SAVED_OBJECT_TYPE, executionSource.source.id) - ).attributes.meta?.versionApiKeyLastmodified === LEGACY_VERSION - : false; + executionSource?.source?.type === ALERT_SAVED_OBJECT_TYPE && + ( + await unsecuredSavedObjectsClient.get<{ + meta?: { + versionApiKeyLastmodified?: string; + }; + }>(ALERT_SAVED_OBJECT_TYPE, executionSource.source.id) + ).attributes.meta?.versionApiKeyLastmodified === LEGACY_VERSION + ? AuthorizationMode.Legacy + : AuthorizationMode.RBAC; } diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index a607dc0de0bda..73434d5c1eaa2 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -27,7 +27,7 @@ export interface ActionExecutorContext { getServices: GetServicesFunction; getActionsClientWithRequest: ( request: KibanaRequest, - executionSource?: ActionExecutionSource + authorizationContext?: ActionExecutionSource ) => Promise>; encryptedSavedObjectsClient: EncryptedSavedObjectsClient; actionTypeRegistry: ActionTypeRegistryContract; diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 97cefafad4385..dca1114f0ae44 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -71,7 +71,10 @@ import { ACTIONS_FEATURE } from './feature'; import { ActionsAuthorization } from './authorization/actions_authorization'; import { ActionsAuthorizationAuditLogger } from './authorization/audit_logger'; import { ActionExecutionSource } from './lib/action_execution_source'; -import { shouldLegacyRbacApplyBySource } from './authorization/should_legacy_rbac_apply_by_source'; +import { + getAuthorizationModeBySource, + AuthorizationMode, +} from './authorization/get_authorization_mode_by_source'; const EVENT_LOG_PROVIDER = 'actions'; export const EVENT_LOG_ACTIONS = { @@ -281,7 +284,7 @@ export class ActionsPlugin implements Plugin, Plugi const getActionsClientWithRequest = async ( request: KibanaRequest, - source?: ActionExecutionSource + authorizationContext?: ActionExecutionSource ) => { if (isESOUsingEphemeralEncryptionKey === true) { throw new Error( @@ -303,7 +306,7 @@ export class ActionsPlugin implements Plugin, Plugi request, authorization: instantiateAuthorization( request, - await shouldLegacyRbacApplyBySource(unsecuredSavedObjectsClient, source) + await getAuthorizationModeBySource(unsecuredSavedObjectsClient, authorizationContext) ), actionExecutor: actionExecutor!, executionEnqueuer: createExecutionEnqueuerFunction({ @@ -316,7 +319,8 @@ export class ActionsPlugin implements Plugin, Plugi }; // Ensure the public API cannot be used to circumvent authorization - // using our legacy exemption mechanism + // using our legacy exemption mechanism by passing in a legacy SO + // as authorizationContext which would then set a Legacy AuthorizationMode const secureGetActionsClientWithRequest = (request: KibanaRequest) => getActionsClientWithRequest(request); @@ -389,11 +393,11 @@ export class ActionsPlugin implements Plugin, Plugi private instantiateAuthorization = ( request: KibanaRequest, - shouldUseLegacyRbac: boolean = false + authorizationMode?: AuthorizationMode ) => { return new ActionsAuthorization({ request, - shouldUseLegacyRbac, + authorizationMode, authorization: this.security?.authz, authentication: this.security?.authc, auditLogger: new ActionsAuthorizationAuditLogger( diff --git a/x-pack/plugins/alerts/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts index 4b5af942024c0..a6cffb0284815 100644 --- a/x-pack/plugins/alerts/server/alerts_client.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client.test.ts @@ -220,7 +220,7 @@ describe('create()', () => { params: {}, ownerId: null, }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -270,27 +270,33 @@ describe('create()', () => { test('creates an alert', async () => { const data = getMockData(); + const createdAttributes = { + ...data, + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + createdAt: '2019-02-12T21:01:22.479Z', + createdBy: 'elastic', + updatedBy: 'elastic', + muteAll: false, + mutedInstanceIds: [], + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + }; unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', - attributes: { - alertTypeId: '123', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - createdAt: '2019-02-12T21:01:22.479Z', - actions: [ - { - group: 'default', - actionRef: 'action_0', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - ], - }, + attributes: createdAttributes, references: [ { name: 'action_0', @@ -312,11 +318,11 @@ describe('create()', () => { params: {}, ownerId: null, }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { - actions: [], + ...createdAttributes, scheduledTaskId: 'task-123', }, references: [ @@ -342,8 +348,14 @@ describe('create()', () => { }, ], "alertTypeId": "123", + "consumer": "bar", "createdAt": 2019-02-12T21:01:22.479Z, + "createdBy": "elastic", + "enabled": true, "id": "1", + "muteAll": false, + "mutedInstanceIds": Array [], + "name": "abc", "params": Object { "bar": true, }, @@ -351,7 +363,12 @@ describe('create()', () => { "interval": "10s", }, "scheduledTaskId": "task-123", + "tags": Array [ + "foo", + ], + "throttle": null, "updatedAt": 2019-02-12T21:01:22.479Z, + "updatedBy": "elastic", } `); expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); @@ -531,7 +548,7 @@ describe('create()', () => { params: {}, ownerId: null, }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -965,7 +982,7 @@ describe('create()', () => { params: {}, ownerId: null, }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -1081,7 +1098,7 @@ describe('create()', () => { params: {}, ownerId: null, }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -1175,6 +1192,16 @@ describe('enable()', () => { alertsClientParams.createAPIKey.mockResolvedValue({ apiKeysEnabled: false, }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + ...existingAlert, + attributes: { + ...existingAlert.attributes, + enabled: true, + apiKey: null, + apiKeyOwner: null, + updatedBy: 'elastic', + }, + }); taskManager.schedule.mockResolvedValue({ id: 'task-123', scheduledAt: new Date(), @@ -1233,6 +1260,17 @@ describe('enable()', () => { }); test('enables an alert', async () => { + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + ...existingAlert, + attributes: { + ...existingAlert.attributes, + enabled: true, + apiKey: null, + apiKeyOwner: null, + updatedBy: 'elastic', + }, + }); + await alertsClient.enable({ id: '1' }); expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { @@ -1317,7 +1355,7 @@ describe('enable()', () => { await alertsClient.enable({ id: '1' }); expect(alertsClientParams.getUserName).not.toHaveBeenCalled(); expect(alertsClientParams.createAPIKey).not.toHaveBeenCalled(); - expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); expect(taskManager.schedule).not.toHaveBeenCalled(); }); @@ -1384,6 +1422,7 @@ describe('enable()', () => { }); test('throws error when failing to update the first time', async () => { + unsecuredSavedObjectsClient.update.mockReset(); unsecuredSavedObjectsClient.update.mockRejectedValueOnce(new Error('Fail to update')); await expect(alertsClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( @@ -1396,6 +1435,7 @@ describe('enable()', () => { }); test('throws error when failing to update the second time', async () => { + unsecuredSavedObjectsClient.update.mockReset(); unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ ...existingAlert, attributes: { @@ -1460,6 +1500,8 @@ describe('disable()', () => { ...existingAlert.attributes, apiKey: Buffer.from('123:abc').toString('base64'), }, + version: '123', + references: [], }; beforeEach(() => { @@ -1501,13 +1543,13 @@ describe('disable()', () => { consumer: 'myApp', schedule: { interval: '10s' }, alertTypeId: 'myType', - apiKey: null, - apiKeyOwner: null, enabled: false, meta: { versionApiKeyLastmodified: kibanaVersion, }, scheduledTaskId: null, + apiKey: null, + apiKeyOwner: null, updatedBy: 'elastic', actions: [ { @@ -1544,13 +1586,13 @@ describe('disable()', () => { consumer: 'myApp', schedule: { interval: '10s' }, alertTypeId: 'myType', - apiKey: null, - apiKeyOwner: null, enabled: false, meta: { versionApiKeyLastmodified: kibanaVersion, }, scheduledTaskId: null, + apiKey: null, + apiKeyOwner: null, updatedBy: 'elastic', actions: [ { @@ -1739,6 +1781,7 @@ describe('unmuteAll()', () => { muteAll: true, }, references: [], + version: '123', }); await alertsClient.unmuteAll({ id: '1' }); @@ -1829,7 +1872,9 @@ describe('muteInstance()', () => { mutedInstanceIds: ['2'], updatedBy: 'elastic', }, - { version: '123' } + { + version: '123', + } ); }); @@ -1850,7 +1895,7 @@ describe('muteInstance()', () => { }); await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }); - expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); }); test('skips muting when alert is muted', async () => { @@ -1871,7 +1916,7 @@ describe('muteInstance()', () => { }); await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }); - expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); }); describe('authorization', () => { @@ -1983,7 +2028,7 @@ describe('unmuteInstance()', () => { }); await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); - expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); }); test('skips unmuting when alert is muted', async () => { @@ -2004,7 +2049,7 @@ describe('unmuteInstance()', () => { }); await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); - expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); }); describe('authorization', () => { @@ -3052,7 +3097,7 @@ describe('update()', () => { }); test('updates given parameters', async () => { - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -3189,11 +3234,10 @@ describe('update()', () => { namespace: 'default', }); expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toHaveLength(4); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][0]).toEqual('alert'); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][1]).toEqual('1'); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` Object { "actions": Array [ Object { @@ -3244,8 +3288,10 @@ describe('update()', () => { "updatedBy": "elastic", } `); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` Object { + "id": "1", + "overwrite": true, "references": Array [ Object { "id": "1", @@ -3286,7 +3332,7 @@ describe('update()', () => { apiKeysEnabled: true, result: { id: '123', name: '123', api_key: 'abc' }, }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -3365,11 +3411,10 @@ describe('update()', () => { "updatedAt": 2019-02-12T21:01:22.479Z, } `); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toHaveLength(4); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][0]).toEqual('alert'); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][1]).toEqual('1'); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` Object { "actions": Array [ Object { @@ -3404,18 +3449,20 @@ describe('update()', () => { "updatedBy": "elastic", } `); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` - Object { - "references": Array [ - Object { - "id": "1", - "name": "action_0", - "type": "action", - }, - ], - "version": "123", - } - `); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` + Object { + "id": "1", + "overwrite": true, + "references": Array [ + Object { + "id": "1", + "name": "action_0", + "type": "action", + }, + ], + "version": "123", + } + `); }); it(`doesn't call the createAPIKey function when alert is disabled`, async () => { @@ -3439,7 +3486,7 @@ describe('update()', () => { }, ], }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -3519,11 +3566,10 @@ describe('update()', () => { "updatedAt": 2019-02-12T21:01:22.479Z, } `); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toHaveLength(4); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][0]).toEqual('alert'); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][1]).toEqual('1'); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` Object { "actions": Array [ Object { @@ -3558,18 +3604,20 @@ describe('update()', () => { "updatedBy": "elastic", } `); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` - Object { - "references": Array [ - Object { - "id": "1", - "name": "action_0", - "type": "action", - }, - ], - "version": "123", - } - `); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` + Object { + "id": "1", + "overwrite": true, + "references": Array [ + Object { + "id": "1", + "name": "action_0", + "type": "action", + }, + ], + "version": "123", + } + `); }); it('should validate params', async () => { @@ -3627,7 +3675,7 @@ describe('update()', () => { }, ], }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -3686,7 +3734,7 @@ describe('update()', () => { }, ], }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -3765,7 +3813,7 @@ describe('update()', () => { }, ], }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { @@ -3919,7 +3967,7 @@ describe('update()', () => { params: {}, ownerId: null, }); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: alertId, type: 'alert', attributes: { @@ -4091,7 +4139,7 @@ describe('update()', () => { describe('authorization', () => { beforeEach(() => { - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', attributes: { diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client.ts index 0a08ca848c73d..671b1d6411d7f 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client.ts @@ -251,7 +251,7 @@ export class AlertsClient { } throw e; } - await this.unsecuredSavedObjectsClient.update('alert', createdAlert.id, { + await this.unsecuredSavedObjectsClient.update('alert', createdAlert.id, { scheduledTaskId: scheduledTask.id, }); createdAlert.attributes.scheduledTaskId = scheduledTask.id; @@ -488,9 +488,8 @@ export class AlertsClient { : null; const apiKeyAttributes = this.apiKeyAsAlertAttributes(createdAPIKey, username); - const updatedObject = await this.unsecuredSavedObjectsClient.update( + const updatedObject = await this.unsecuredSavedObjectsClient.create( 'alert', - id, this.updateMeta({ ...attributes, ...data, @@ -500,6 +499,8 @@ export class AlertsClient { updatedBy: username, }), { + id, + overwrite: true, version, references, } @@ -798,6 +799,7 @@ export class AlertsClient { 'alert', alertId ); + await this.authorization.ensureAuthorized( attributes.alertTypeId, attributes.consumer, @@ -809,7 +811,7 @@ export class AlertsClient { const mutedInstanceIds = attributes.mutedInstanceIds || []; if (!attributes.muteAll && mutedInstanceIds.includes(alertInstanceId)) { - await this.unsecuredSavedObjectsClient.update( + await this.unsecuredSavedObjectsClient.update( 'alert', alertId, this.updateMeta({ diff --git a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts index f873b0178ece9..aca447b6adedd 100644 --- a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts @@ -19,6 +19,7 @@ import { AlertInstanceContext, AlertType, AlertTypeParams, + RawAlert, } from '../types'; interface CreateExecutionHandlerOptions { @@ -28,7 +29,7 @@ interface CreateExecutionHandlerOptions { actionsPlugin: ActionsPluginStartContract; actions: AlertAction[]; spaceId: string; - apiKey: string | null; + apiKey: RawAlert['apiKey']; alertType: AlertType; logger: Logger; eventLogger: IEventLogger; @@ -99,7 +100,7 @@ export function createExecutionHandler({ id: action.id, params: action.params, spaceId, - apiKey, + apiKey: apiKey ?? null, source: asSavedObjectExecutionSource({ id: alertId, type: 'alert', diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.ts index 4c16d23b485b5..5be684eca4651 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.ts @@ -73,7 +73,7 @@ export class TaskRunner { return apiKey; } - private getFakeKibanaRequest(spaceId: string, apiKey: string | null) { + private getFakeKibanaRequest(spaceId: string, apiKey: RawAlert['apiKey']) { const requestHeaders: Record = {}; if (apiKey) { @@ -98,7 +98,7 @@ export class TaskRunner { private getServicesWithSpaceLevelPermissions( spaceId: string, - apiKey: string | null + apiKey: RawAlert['apiKey'] ): [Services, PublicMethodsOf] { const request = this.getFakeKibanaRequest(spaceId, apiKey); return [this.context.getServices(request), this.context.getAlertsClientWithRequest(request)]; @@ -109,7 +109,7 @@ export class TaskRunner { alertName: string, tags: string[] | undefined, spaceId: string, - apiKey: string | null, + apiKey: RawAlert['apiKey'], actions: Alert['actions'], alertParams: RawAlert['params'] ) { @@ -250,7 +250,11 @@ export class TaskRunner { }; } - async validateAndExecuteAlert(services: Services, apiKey: string | null, alert: SanitizedAlert) { + async validateAndExecuteAlert( + services: Services, + apiKey: RawAlert['apiKey'], + alert: SanitizedAlert + ) { const { params: { alertId, spaceId }, } = this.taskInstance; diff --git a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap index 8218eefe738f0..8c233d3691c7f 100644 --- a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap +++ b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap @@ -36,12 +36,12 @@ exports[`Error ERROR_LOG_MESSAGE 1`] = `undefined`; exports[`Error ERROR_PAGE_URL 1`] = `undefined`; +exports[`Error EVENT_OUTCOME 1`] = `undefined`; + exports[`Error FCP_FIELD 1`] = `undefined`; exports[`Error FID_FIELD 1`] = `undefined`; -exports[`Error EVENT_OUTCOME 1`] = `undefined`; - exports[`Error HOST_NAME 1`] = `"my hostname"`; exports[`Error HTTP_REQUEST_METHOD 1`] = `undefined`; @@ -52,6 +52,10 @@ exports[`Error LABEL_NAME 1`] = `undefined`; exports[`Error LCP_FIELD 1`] = `undefined`; +exports[`Error METRIC_CGROUP_MEMORY_LIMIT_BYTES 1`] = `undefined`; + +exports[`Error METRIC_CGROUP_MEMORY_USAGE_BYTES 1`] = `undefined`; + exports[`Error METRIC_JAVA_GC_COUNT 1`] = `undefined`; exports[`Error METRIC_JAVA_GC_TIME 1`] = `undefined`; @@ -136,6 +140,8 @@ exports[`Error TRANSACTION_DOM_INTERACTIVE 1`] = `undefined`; exports[`Error TRANSACTION_DURATION 1`] = `undefined`; +exports[`Error TRANSACTION_DURATION_HISTOGRAM 1`] = `undefined`; + exports[`Error TRANSACTION_ID 1`] = `"transaction id"`; exports[`Error TRANSACTION_NAME 1`] = `undefined`; @@ -144,6 +150,8 @@ exports[`Error TRANSACTION_PAGE_URL 1`] = `undefined`; exports[`Error TRANSACTION_RESULT 1`] = `undefined`; +exports[`Error TRANSACTION_ROOT 1`] = `undefined`; + exports[`Error TRANSACTION_SAMPLED 1`] = `undefined`; exports[`Error TRANSACTION_TIME_TO_FIRST_BYTE 1`] = `undefined`; @@ -200,12 +208,12 @@ exports[`Span ERROR_LOG_MESSAGE 1`] = `undefined`; exports[`Span ERROR_PAGE_URL 1`] = `undefined`; +exports[`Span EVENT_OUTCOME 1`] = `undefined`; + exports[`Span FCP_FIELD 1`] = `undefined`; exports[`Span FID_FIELD 1`] = `undefined`; -exports[`Span EVENT_OUTCOME 1`] = `undefined`; - exports[`Span HOST_NAME 1`] = `undefined`; exports[`Span HTTP_REQUEST_METHOD 1`] = `undefined`; @@ -216,6 +224,10 @@ exports[`Span LABEL_NAME 1`] = `undefined`; exports[`Span LCP_FIELD 1`] = `undefined`; +exports[`Span METRIC_CGROUP_MEMORY_LIMIT_BYTES 1`] = `undefined`; + +exports[`Span METRIC_CGROUP_MEMORY_USAGE_BYTES 1`] = `undefined`; + exports[`Span METRIC_JAVA_GC_COUNT 1`] = `undefined`; exports[`Span METRIC_JAVA_GC_TIME 1`] = `undefined`; @@ -300,6 +312,8 @@ exports[`Span TRANSACTION_DOM_INTERACTIVE 1`] = `undefined`; exports[`Span TRANSACTION_DURATION 1`] = `undefined`; +exports[`Span TRANSACTION_DURATION_HISTOGRAM 1`] = `undefined`; + exports[`Span TRANSACTION_ID 1`] = `"transaction id"`; exports[`Span TRANSACTION_NAME 1`] = `undefined`; @@ -308,6 +322,8 @@ exports[`Span TRANSACTION_PAGE_URL 1`] = `undefined`; exports[`Span TRANSACTION_RESULT 1`] = `undefined`; +exports[`Span TRANSACTION_ROOT 1`] = `undefined`; + exports[`Span TRANSACTION_SAMPLED 1`] = `undefined`; exports[`Span TRANSACTION_TIME_TO_FIRST_BYTE 1`] = `undefined`; @@ -364,12 +380,12 @@ exports[`Transaction ERROR_LOG_MESSAGE 1`] = `undefined`; exports[`Transaction ERROR_PAGE_URL 1`] = `undefined`; +exports[`Transaction EVENT_OUTCOME 1`] = `undefined`; + exports[`Transaction FCP_FIELD 1`] = `undefined`; exports[`Transaction FID_FIELD 1`] = `undefined`; -exports[`Transaction EVENT_OUTCOME 1`] = `undefined`; - exports[`Transaction HOST_NAME 1`] = `"my hostname"`; exports[`Transaction HTTP_REQUEST_METHOD 1`] = `"GET"`; @@ -380,6 +396,10 @@ exports[`Transaction LABEL_NAME 1`] = `undefined`; exports[`Transaction LCP_FIELD 1`] = `undefined`; +exports[`Transaction METRIC_CGROUP_MEMORY_LIMIT_BYTES 1`] = `undefined`; + +exports[`Transaction METRIC_CGROUP_MEMORY_USAGE_BYTES 1`] = `undefined`; + exports[`Transaction METRIC_JAVA_GC_COUNT 1`] = `undefined`; exports[`Transaction METRIC_JAVA_GC_TIME 1`] = `undefined`; @@ -464,6 +484,8 @@ exports[`Transaction TRANSACTION_DOM_INTERACTIVE 1`] = `undefined`; exports[`Transaction TRANSACTION_DURATION 1`] = `1337`; +exports[`Transaction TRANSACTION_DURATION_HISTOGRAM 1`] = `undefined`; + exports[`Transaction TRANSACTION_ID 1`] = `"transaction id"`; exports[`Transaction TRANSACTION_NAME 1`] = `"transaction name"`; @@ -472,6 +494,8 @@ exports[`Transaction TRANSACTION_PAGE_URL 1`] = `undefined`; exports[`Transaction TRANSACTION_RESULT 1`] = `"transaction result"`; +exports[`Transaction TRANSACTION_ROOT 1`] = `undefined`; + exports[`Transaction TRANSACTION_SAMPLED 1`] = `true`; exports[`Transaction TRANSACTION_TIME_TO_FIRST_BYTE 1`] = `undefined`; diff --git a/x-pack/plugins/apm/common/aggregated_transactions.ts b/x-pack/plugins/apm/common/aggregated_transactions.ts new file mode 100644 index 0000000000000..51149df45e9de --- /dev/null +++ b/x-pack/plugins/apm/common/aggregated_transactions.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; + * you may not use this file except in compliance with the Elastic License. + */ + +export enum SearchAggregatedTransactionSetting { + always = 'always', + never = 'never', + auto = 'auto', +} diff --git a/x-pack/plugins/apm/common/alert_types.ts b/x-pack/plugins/apm/common/alert_types.ts index a1161354e04f4..15a3c642faf32 100644 --- a/x-pack/plugins/apm/common/alert_types.ts +++ b/x-pack/plugins/apm/common/alert_types.ts @@ -7,42 +7,33 @@ import { i18n } from '@kbn/i18n'; export enum AlertType { - ErrorRate = 'apm.error_rate', + ErrorCount = 'apm.error_rate', // ErrorRate was renamed to ErrorCount but the key is kept as `error_rate` for backwards-compat. + TransactionErrorRate = 'apm.transaction_error_rate', TransactionDuration = 'apm.transaction_duration', TransactionDurationAnomaly = 'apm.transaction_duration_anomaly', } +const THRESHOLD_MET_GROUP = { + id: 'threshold_met', + name: i18n.translate('xpack.apm.a.thresholdMet', { + defaultMessage: 'Threshold met', + }), +}; + export const ALERT_TYPES_CONFIG = { - [AlertType.ErrorRate]: { - name: i18n.translate('xpack.apm.errorRateAlert.name', { - defaultMessage: 'Error rate', + [AlertType.ErrorCount]: { + name: i18n.translate('xpack.apm.errorCountAlert.name', { + defaultMessage: 'Error count threshold', }), - actionGroups: [ - { - id: 'threshold_met', - name: i18n.translate('xpack.apm.errorRateAlert.thresholdMet', { - defaultMessage: 'Threshold met', - }), - }, - ], + actionGroups: [THRESHOLD_MET_GROUP], defaultActionGroupId: 'threshold_met', producer: 'apm', }, [AlertType.TransactionDuration]: { name: i18n.translate('xpack.apm.transactionDurationAlert.name', { - defaultMessage: 'Transaction duration', + defaultMessage: 'Transaction duration threshold', }), - actionGroups: [ - { - id: 'threshold_met', - name: i18n.translate( - 'xpack.apm.transactionDurationAlert.thresholdMet', - { - defaultMessage: 'Threshold met', - } - ), - }, - ], + actionGroups: [THRESHOLD_MET_GROUP], defaultActionGroupId: 'threshold_met', producer: 'apm', }, @@ -50,39 +41,24 @@ export const ALERT_TYPES_CONFIG = { name: i18n.translate('xpack.apm.transactionDurationAnomalyAlert.name', { defaultMessage: 'Transaction duration anomaly', }), - actionGroups: [ - { - id: 'threshold_met', - name: i18n.translate( - 'xpack.apm.transactionDurationAlert.thresholdMet', - { - defaultMessage: 'Threshold met', - } - ), - }, - ], + actionGroups: [THRESHOLD_MET_GROUP], + defaultActionGroupId: 'threshold_met', + producer: 'apm', + }, + [AlertType.TransactionErrorRate]: { + name: i18n.translate('xpack.apm.transactionErrorRateAlert.name', { + defaultMessage: 'Transaction error rate threshold', + }), + actionGroups: [THRESHOLD_MET_GROUP], defaultActionGroupId: 'threshold_met', producer: 'apm', }, }; -export const TRANSACTION_ALERT_AGGREGATION_TYPES = { - avg: i18n.translate( - 'xpack.apm.transactionDurationAlert.aggregationType.avg', - { - defaultMessage: 'Average', - } - ), - '95th': i18n.translate( - 'xpack.apm.transactionDurationAlert.aggregationType.95th', - { - defaultMessage: '95th percentile', - } - ), - '99th': i18n.translate( - 'xpack.apm.transactionDurationAlert.aggregationType.99th', - { - defaultMessage: '99th percentile', - } - ), -}; +// Server side registrations +// x-pack/plugins/apm/server/lib/alerts/.ts +// x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts + +// Client side registrations: +// x-pack/plugins/apm/public/components/alerting//index.tsx +// x-pack/plugins/apm/public/components/alerting/register_apm_alerts diff --git a/x-pack/plugins/apm/common/custom_link/index.ts b/x-pack/plugins/apm/common/custom_link/index.ts new file mode 100644 index 0000000000000..bc0ffefd79c4d --- /dev/null +++ b/x-pack/plugins/apm/common/custom_link/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; + +export const INVALID_LICENSE = i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.license.text', + { + defaultMessage: + "To create custom links, you must be subscribed to an Elastic Gold license or above. With it, you'll have the ability to create custom links to improve your workflow when analyzing your services.", + } +); diff --git a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts index e1a279714d308..cc6a1fffb2288 100644 --- a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts +++ b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts @@ -37,6 +37,7 @@ export const OBSERVER_LISTENING = 'observer.listening'; export const PROCESSOR_EVENT = 'processor.event'; export const TRANSACTION_DURATION = 'transaction.duration.us'; +export const TRANSACTION_DURATION_HISTOGRAM = 'transaction.duration.histogram'; export const TRANSACTION_TYPE = 'transaction.type'; export const TRANSACTION_RESULT = 'transaction.result'; export const TRANSACTION_NAME = 'transaction.name'; @@ -44,6 +45,8 @@ export const TRANSACTION_ID = 'transaction.id'; export const TRANSACTION_SAMPLED = 'transaction.sampled'; export const TRANSACTION_BREAKDOWN_COUNT = 'transaction.breakdown.count'; export const TRANSACTION_PAGE_URL = 'transaction.page.url'; +// for transaction metrics +export const TRANSACTION_ROOT = 'transaction.root'; export const EVENT_OUTCOME = 'event.outcome'; @@ -76,6 +79,10 @@ export const METRIC_SYSTEM_FREE_MEMORY = 'system.memory.actual.free'; export const METRIC_SYSTEM_TOTAL_MEMORY = 'system.memory.total'; export const METRIC_SYSTEM_CPU_PERCENT = 'system.cpu.total.norm.pct'; export const METRIC_PROCESS_CPU_PERCENT = 'system.process.cpu.total.norm.pct'; +export const METRIC_CGROUP_MEMORY_LIMIT_BYTES = + 'system.process.cgroup.memory.mem.limit.bytes'; +export const METRIC_CGROUP_MEMORY_USAGE_BYTES = + 'system.process.cgroup.memory.mem.usage.bytes'; export const METRIC_JAVA_HEAP_MEMORY_MAX = 'jvm.memory.heap.max'; export const METRIC_JAVA_HEAP_MEMORY_COMMITTED = 'jvm.memory.heap.committed'; diff --git a/x-pack/plugins/apm/common/utils/maybe.ts b/x-pack/plugins/apm/common/utils/maybe.ts new file mode 100644 index 0000000000000..b586827a71c7e --- /dev/null +++ b/x-pack/plugins/apm/common/utils/maybe.ts @@ -0,0 +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; + * you may not use this file except in compliance with the Elastic License. + */ + +export function maybe(value: T): T | null | undefined { + return value; +} diff --git a/x-pack/plugins/apm/e2e/cypress/integration/helpers.ts b/x-pack/plugins/apm/e2e/cypress/integration/helpers.ts index 1956f1c2d9f0d..462304a959102 100644 --- a/x-pack/plugins/apm/e2e/cypress/integration/helpers.ts +++ b/x-pack/plugins/apm/e2e/cypress/integration/helpers.ts @@ -11,14 +11,19 @@ export const DEFAULT_TIMEOUT = 60 * 1000; export function loginAndWaitForPage( url: string, - dateRange: { to: string; from: string } + dateRange: { to: string; from: string }, + selectedService?: string ) { const username = Cypress.env('elasticsearch_username'); const password = Cypress.env('elasticsearch_password'); cy.log(`Authenticating via ${username} / ${password}`); - const fullUrl = `${BASE_URL}${url}?rangeFrom=${dateRange.from}&rangeTo=${dateRange.to}`; + let fullUrl = `${BASE_URL}${url}?rangeFrom=${dateRange.from}&rangeTo=${dateRange.to}`; + + if (selectedService) { + fullUrl += `&serviceName=${selectedService}`; + } cy.visit(fullUrl, { auth: { username, password } }); cy.viewport('macbook-15'); diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_dashboard.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_dashboard.ts index a57241a197ca4..461e2960c5e02 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_dashboard.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_dashboard.ts @@ -15,10 +15,14 @@ Given(`a user browses the APM UI application for RUM Data`, () => { // open service overview page const RANGE_FROM = 'now-24h'; const RANGE_TO = 'now'; - loginAndWaitForPage(`/app/csm`, { - from: RANGE_FROM, - to: RANGE_TO, - }); + loginAndWaitForPage( + `/app/csm`, + { + from: RANGE_FROM, + to: RANGE_TO, + }, + 'client' + ); }); Then(`should have correct client metrics`, () => { diff --git a/x-pack/plugins/apm/e2e/yarn.lock b/x-pack/plugins/apm/e2e/yarn.lock index 936294052aa7b..fc63189e97ea3 100644 --- a/x-pack/plugins/apm/e2e/yarn.lock +++ b/x-pack/plugins/apm/e2e/yarn.lock @@ -5494,10 +5494,10 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@3.9.6: - version "3.9.6" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.6.tgz#8f3e0198a34c3ae17091b35571d3afd31999365a" - integrity sha512-Pspx3oKAPJtjNwE92YS05HQoY7z2SFyOpHo9MqJor3BXAGNaPUs83CuVp9VISFkSjyRfiTpmKuAYGJB7S7hOxw== +typescript@4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.2.tgz#7ea7c88777c723c681e33bf7988be5d008d05ac2" + integrity sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ== umd@^3.0.0: version "3.0.3" diff --git a/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.stories.tsx b/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.stories.tsx similarity index 83% rename from x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.stories.tsx rename to x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.stories.tsx index 632d53a9c63b6..c30cef7210a43 100644 --- a/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.stories.tsx +++ b/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.stories.tsx @@ -6,14 +6,14 @@ import { storiesOf } from '@storybook/react'; import React from 'react'; -import { ErrorRateAlertTrigger } from '.'; +import { ErrorCountAlertTrigger } from '.'; import { ApmPluginContextValue } from '../../../context/ApmPluginContext'; import { mockApmPluginContextValue, MockApmPluginContextWrapper, } from '../../../context/ApmPluginContext/MockApmPluginContext'; -storiesOf('app/ErrorRateAlertTrigger', module).add( +storiesOf('app/ErrorCountAlertTrigger', module).add( 'example', () => { const params = { @@ -26,7 +26,7 @@ storiesOf('app/ErrorRateAlertTrigger', module).add( value={(mockApmPluginContextValue as unknown) as ApmPluginContextValue} >
- undefined} setAlertProperty={() => undefined} @@ -37,7 +37,7 @@ storiesOf('app/ErrorRateAlertTrigger', module).add( }, { info: { - propTablesExclude: [ErrorRateAlertTrigger, MockApmPluginContextWrapper], + propTablesExclude: [ErrorCountAlertTrigger, MockApmPluginContextWrapper], source: false, }, } diff --git a/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.tsx similarity index 54% rename from x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx rename to x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.tsx index 7b284696477f3..a465b90e7bf05 100644 --- a/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.tsx @@ -3,36 +3,33 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { EuiFieldNumber, EuiSelect } from '@elastic/eui'; + import { i18n } from '@kbn/i18n'; -import { isFinite } from 'lodash'; import React from 'react'; import { useParams } from 'react-router-dom'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; -import { ALERT_TYPES_CONFIG } from '../../../../common/alert_types'; -import { - ENVIRONMENT_ALL, - getEnvironmentLabel, -} from '../../../../common/environment_filter_values'; +import { ALERT_TYPES_CONFIG, AlertType } from '../../../../common/alert_types'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { useEnvironments } from '../../../hooks/useEnvironments'; import { useUrlParams } from '../../../hooks/useUrlParams'; +import { EnvironmentField, ServiceField, IsAboveField } from '../fields'; import { ServiceAlertTrigger } from '../ServiceAlertTrigger'; -import { PopoverExpression } from '../ServiceAlertTrigger/PopoverExpression'; -export interface ErrorRateAlertTriggerParams { +export interface AlertParams { windowSize: number; windowUnit: string; threshold: number; + serviceName: string; environment: string; } interface Props { - alertParams: ErrorRateAlertTriggerParams; + alertParams: AlertParams; setAlertParams: (key: string, value: any) => void; setAlertProperty: (key: string, value: any) => void; } -export function ErrorRateAlertTrigger(props: Props) { +export function ErrorCountAlertTrigger(props: Props) { const { setAlertParams, setAlertProperty, alertParams } = props; const { serviceName } = useParams<{ serviceName?: string }>(); const { urlParams } = useUrlParams(); @@ -51,45 +48,20 @@ export function ErrorRateAlertTrigger(props: Props) { ...alertParams, }; - const threshold = isFinite(params.threshold) ? params.threshold : ''; - const fields = [ - - - setAlertParams( - 'environment', - e.target.value as ErrorRateAlertTriggerParams['environment'] - ) - } - compressed - /> - , - , + setAlertParams('environment', e.target.value)} + />, + - - setAlertParams('threshold', parseInt(e.target.value, 10)) - } - compressed - append={i18n.translate('xpack.apm.errorRateAlertTrigger.errors', { - defaultMessage: 'errors', - })} - /> - , + onChange={(value) => setAlertParams('threshold', value)} + />, setAlertParams('windowSize', windowSize || '') @@ -108,7 +80,7 @@ export function ErrorRateAlertTrigger(props: Props) { return ( void; setAlertProperty: (key: string, value: any) => void; } export function TransactionDurationAlertTrigger(props: Props) { const { setAlertParams, alertParams, setAlertProperty } = props; - const { serviceName } = alertParams; const { urlParams } = useUrlParams(); - const transactionTypes = useServiceTransactionTypes(urlParams); - - const { start, end } = urlParams; + const { serviceName } = useParams<{ serviceName?: string }>(); + const { start, end, transactionType } = urlParams; const { environmentOptions } = useEnvironments({ serviceName, start, end }); - if (!transactionTypes.length) { + if (!transactionTypes.length || !serviceName) { return null; } @@ -57,7 +77,9 @@ export function TransactionDurationAlertTrigger(props: Props) { aggregationType: 'avg', windowSize: 5, windowUnit: 'm', - transactionType: transactionTypes[0], + + // use the current transaction type or default to the first in the list + transactionType: transactionType || transactionTypes[0], environment: urlParams.environment || ENVIRONMENT_ALL.value, }; @@ -67,47 +89,17 @@ export function TransactionDurationAlertTrigger(props: Props) { }; const fields = [ - - - setAlertParams('environment', e.target.value as Params['environment']) - } - compressed - /> - , - - { - return { - text: key, - value: key, - }; - })} - onChange={(e) => - setAlertParams( - 'transactionType', - e.target.value as Params['transactionType'] - ) - } - compressed - /> - , + , + setAlertParams('environment', e.target.value)} + />, + ({ text: key, value: key }))} + onChange={(e) => setAlertParams('transactionType', e.target.value)} + />, - setAlertParams( - 'aggregationType', - e.target.value as Params['aggregationType'] - ) - } - compressed - /> - , - - setAlertParams('threshold', e.target.value)} - append={i18n.translate('xpack.apm.transactionDurationAlertTrigger.ms', { - defaultMessage: 'ms', - })} + onChange={(e) => setAlertParams('aggregationType', e.target.value)} compressed /> , + setAlertParams('threshold', value)} + />, setAlertParams('windowSize', timeWindowSize || '') diff --git a/x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx rename to x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx diff --git a/x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx similarity index 75% rename from x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/index.tsx rename to x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx index 20e0a3f27c4a4..fb4cda56fce04 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx @@ -3,7 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { EuiExpression, EuiSelect } from '@elastic/eui'; + +import { useParams } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { ALERT_TYPES_CONFIG } from '../../../../common/alert_types'; @@ -16,14 +17,16 @@ import { AnomalySeverity, SelectAnomalySeverity, } from './SelectAnomalySeverity'; -import { - ENVIRONMENT_ALL, - getEnvironmentLabel, -} from '../../../../common/environment_filter_values'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { TRANSACTION_PAGE_LOAD, TRANSACTION_REQUEST, } from '../../../../common/transaction_types'; +import { + EnvironmentField, + ServiceField, + TransactionTypeField, +} from '../fields'; interface Params { windowSize: number; @@ -42,9 +45,9 @@ interface Props { export function TransactionDurationAnomalyAlertTrigger(props: Props) { const { setAlertParams, alertParams, setAlertProperty } = props; - const { serviceName } = alertParams; const { urlParams } = useUrlParams(); const transactionTypes = useServiceTransactionTypes(urlParams); + const { serviceName } = useParams<{ serviceName?: string }>(); const { start, end } = urlParams; const { environmentOptions } = useEnvironments({ serviceName, start, end }); const supportedTransactionTypes = transactionTypes.filter((transactionType) => @@ -55,10 +58,13 @@ export function TransactionDurationAnomalyAlertTrigger(props: Props) { return null; } + // 'page-load' for RUM, 'request' otherwise + const transactionType = supportedTransactionTypes[0]; + const defaults: Params = { windowSize: 15, windowUnit: 'm', - transactionType: supportedTransactionTypes[0], // 'page-load' for RUM, 'request' otherwise + transactionType, serviceName, environment: urlParams.environment || ENVIRONMENT_ALL.value, anomalyScore: 75, @@ -70,31 +76,13 @@ export function TransactionDurationAnomalyAlertTrigger(props: Props) { }; const fields = [ - , + , + setAlertParams('environment', e.target.value)} />, - - setAlertParams('environment', e.target.value)} - compressed - /> - , } title={i18n.translate( diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx new file mode 100644 index 0000000000000..4dbf4dc10a907 --- /dev/null +++ b/x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { useParams } from 'react-router-dom'; +import React from 'react'; +import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; +import { ALERT_TYPES_CONFIG, AlertType } from '../../../../common/alert_types'; +import { useEnvironments } from '../../../hooks/useEnvironments'; +import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes'; +import { useUrlParams } from '../../../hooks/useUrlParams'; +import { ServiceAlertTrigger } from '../ServiceAlertTrigger'; + +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; +import { + ServiceField, + TransactionTypeField, + EnvironmentField, + IsAboveField, +} from '../fields'; + +interface AlertParams { + windowSize: number; + windowUnit: string; + threshold: number; + serviceName: string; + transactionType: string; + environment: string; +} + +interface Props { + alertParams: AlertParams; + setAlertParams: (key: string, value: any) => void; + setAlertProperty: (key: string, value: any) => void; +} + +export function TransactionErrorRateAlertTrigger(props: Props) { + const { setAlertParams, alertParams, setAlertProperty } = props; + const { urlParams } = useUrlParams(); + const transactionTypes = useServiceTransactionTypes(urlParams); + const { serviceName } = useParams<{ serviceName?: string }>(); + const { start, end, transactionType } = urlParams; + const { environmentOptions } = useEnvironments({ serviceName, start, end }); + + if (!transactionTypes.length || !serviceName) { + return null; + } + + const defaultParams = { + threshold: 30, + windowSize: 5, + windowUnit: 'm', + transactionType: transactionType || transactionTypes[0], + environment: urlParams.environment || ENVIRONMENT_ALL.value, + }; + + const params = { + ...defaultParams, + ...alertParams, + }; + + const fields = [ + , + setAlertParams('environment', e.target.value)} + />, + ({ text: key, value: key }))} + onChange={(e) => setAlertParams('transactionType', e.target.value)} + />, + setAlertParams('threshold', value)} + />, + + setAlertParams('windowSize', timeWindowSize || '') + } + onChangeWindowUnit={(timeWindowUnit) => + setAlertParams('windowUnit', timeWindowUnit) + } + timeWindowSize={params.windowSize} + timeWindowUnit={params.windowUnit} + errors={{ + timeWindowSize: [], + timeWindowUnit: [], + }} + />, + ]; + + return ( + + ); +} + +// Default export is required for React.lazy loading +// +// eslint-disable-next-line import/no-default-export +export default TransactionErrorRateAlertTrigger; diff --git a/x-pack/plugins/apm/public/components/alerting/fields.tsx b/x-pack/plugins/apm/public/components/alerting/fields.tsx new file mode 100644 index 0000000000000..e145d03671a18 --- /dev/null +++ b/x-pack/plugins/apm/public/components/alerting/fields.tsx @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiSelect, EuiExpression, EuiFieldNumber } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiSelectOption } from '@elastic/eui'; +import { getEnvironmentLabel } from '../../../common/environment_filter_values'; +import { PopoverExpression } from './ServiceAlertTrigger/PopoverExpression'; + +export function ServiceField({ value }: { value?: string }) { + return ( + + ); +} + +export function EnvironmentField({ + currentValue, + options, + onChange, +}: { + currentValue: string; + options: EuiSelectOption[]; + onChange: (event: React.ChangeEvent) => void; +}) { + return ( + + + + ); +} + +export function TransactionTypeField({ + currentValue, + options, + onChange, +}: { + currentValue: string; + options?: EuiSelectOption[]; + onChange?: (event: React.ChangeEvent) => void; +}) { + const label = i18n.translate('xpack.apm.alerting.fields.type', { + defaultMessage: 'Type', + }); + + if (!options || options.length === 1) { + return ; + } + + return ( + + + + ); +} + +export function IsAboveField({ + value, + unit, + onChange, + step, +}: { + value: number; + unit: string; + onChange: (value: number) => void; + step?: number; +}) { + return ( + + onChange(parseInt(e.target.value, 10))} + append={unit} + compressed + step={step} + /> + + ); +} diff --git a/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts new file mode 100644 index 0000000000000..c0a1955e2cc8a --- /dev/null +++ b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { lazy } from 'react'; +import { AlertType } from '../../../common/alert_types'; +import { ApmPluginStartDeps } from '../../plugin'; + +export function registerApmAlerts( + alertTypeRegistry: ApmPluginStartDeps['triggers_actions_ui']['alertTypeRegistry'] +) { + alertTypeRegistry.register({ + id: AlertType.ErrorCount, + name: i18n.translate('xpack.apm.alertTypes.errorCount', { + defaultMessage: 'Error count threshold', + }), + iconClass: 'bell', + alertParamsExpression: lazy(() => import('./ErrorCountAlertTrigger')), + validate: () => ({ + errors: [], + }), + requiresAppContext: true, + }); + + alertTypeRegistry.register({ + id: AlertType.TransactionDuration, + name: i18n.translate('xpack.apm.alertTypes.transactionDuration', { + defaultMessage: 'Transaction duration threshold', + }), + iconClass: 'bell', + alertParamsExpression: lazy( + () => import('./TransactionDurationAlertTrigger') + ), + validate: () => ({ + errors: [], + }), + requiresAppContext: true, + }); + + alertTypeRegistry.register({ + id: AlertType.TransactionErrorRate, + name: i18n.translate('xpack.apm.alertTypes.transactionErrorRate', { + defaultMessage: 'Transaction error rate threshold', + }), + iconClass: 'bell', + alertParamsExpression: lazy( + () => import('./TransactionErrorRateAlertTrigger') + ), + validate: () => ({ + errors: [], + }), + requiresAppContext: true, + }); + + alertTypeRegistry.register({ + id: AlertType.TransactionDurationAnomaly, + name: i18n.translate('xpack.apm.alertTypes.transactionDurationAnomaly', { + defaultMessage: 'Transaction duration anomaly', + }), + iconClass: 'bell', + alertParamsExpression: lazy( + () => import('./TransactionDurationAnomalyAlertTrigger') + ), + validate: () => ({ + errors: [], + }), + requiresAppContext: true, + }); +} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Breakdowns/BreakdownFilter.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Breakdowns/BreakdownFilter.tsx index 91ce57c78b993..626c2124d421e 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Breakdowns/BreakdownFilter.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Breakdowns/BreakdownFilter.tsx @@ -75,8 +75,9 @@ export function BreakdownFilter({ const onOptionChange = (value: string) => { if (value === NO_BREAKDOWN) { onBreakdownChange(null); + } else { + onBreakdownChange(items.find(({ fieldName }) => fieldName === value)!); } - onBreakdownChange(items.find(({ fieldName }) => fieldName === value)!); }; return ( diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageLoadDistChart.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageLoadDistChart.tsx index c832ec9fcc0d0..79cd1c5753ae5 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageLoadDistChart.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageLoadDistChart.tsx @@ -18,6 +18,7 @@ import { TooltipValueFormatter, DARK_THEME, LIGHT_THEME, + Fit, } from '@elastic/charts'; import { EUI_CHARTS_THEME_DARK, @@ -115,12 +116,14 @@ export function PageLoadDistChart({ tickFormat={(d) => numeral(d).format('0.0') + '%'} /> {breakdown && ( { - if (start && end) { + const { serviceName } = uiFilters; + if (start && end && serviceName) { return callApmApi({ pathname: '/api/apm/rum/client-metrics', params: { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/BreakdownSeries.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/BreakdownSeries.tsx index 475a235ef5eed..3463327441b7b 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/BreakdownSeries.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/BreakdownSeries.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CurveType, LineSeries, ScaleType } from '@elastic/charts'; +import { CurveType, Fit, LineSeries, ScaleType } from '@elastic/charts'; import React, { useEffect } from 'react'; import { PercentileRange } from './index'; import { useBreakdowns } from './use_breakdowns'; @@ -41,8 +41,10 @@ export function BreakdownSeries({ name={name} xScaleType={ScaleType.Linear} yScaleType={ScaleType.Linear} + curve={CurveType.CURVE_CATMULL_ROM} data={seriesData ?? []} - curve={CurveType.CURVE_NATURAL} + lineSeriesStyle={{ point: { visible: false } }} + fit={Fit.Linear} /> ))} diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx index 27c4a37e09c00..c11bfdeae945b 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx @@ -24,9 +24,13 @@ const transactionDurationLabel = i18n.translate( 'xpack.apm.serviceDetails.alertsMenu.transactionDuration', { defaultMessage: 'Transaction duration' } ); -const errorRateLabel = i18n.translate( - 'xpack.apm.serviceDetails.alertsMenu.errorRate', - { defaultMessage: 'Error rate' } +const transactionErrorRateLabel = i18n.translate( + 'xpack.apm.serviceDetails.alertsMenu.transactionErrorRate', + { defaultMessage: 'Transaction error rate' } +); +const errorCountLabel = i18n.translate( + 'xpack.apm.serviceDetails.alertsMenu.errorCount', + { defaultMessage: 'Error count' } ); const createThresholdAlertLabel = i18n.translate( 'xpack.apm.serviceDetails.alertsMenu.createThresholdAlert', @@ -38,8 +42,10 @@ const createAnomalyAlertAlertLabel = i18n.translate( ); const CREATE_TRANSACTION_DURATION_ALERT_PANEL_ID = - 'create_transaction_duration'; -const CREATE_ERROR_RATE_ALERT_PANEL_ID = 'create_error_rate'; + 'create_transaction_duration_panel'; +const CREATE_TRANSACTION_ERROR_RATE_ALERT_PANEL_ID = + 'create_transaction_error_rate_panel'; +const CREATE_ERROR_COUNT_ALERT_PANEL_ID = 'create_error_count_panel'; interface Props { canReadAlerts: boolean; @@ -77,7 +83,14 @@ export function AlertIntegrations(props: Props) { name: transactionDurationLabel, panel: CREATE_TRANSACTION_DURATION_ALERT_PANEL_ID, }, - { name: errorRateLabel, panel: CREATE_ERROR_RATE_ALERT_PANEL_ID }, + { + name: transactionErrorRateLabel, + panel: CREATE_TRANSACTION_ERROR_RATE_ALERT_PANEL_ID, + }, + { + name: errorCountLabel, + panel: CREATE_ERROR_COUNT_ALERT_PANEL_ID, + }, ] : []), ...(canReadAlerts @@ -96,10 +109,13 @@ export function AlertIntegrations(props: Props) { : []), ], }, + + // transaction duration panel { id: CREATE_TRANSACTION_DURATION_ALERT_PANEL_ID, title: transactionDurationLabel, items: [ + // threshold alerts { name: createThresholdAlertLabel, onClick: () => { @@ -107,6 +123,8 @@ export function AlertIntegrations(props: Props) { setPopoverOpen(false); }, }, + + // anomaly alerts ...(canReadAnomalies ? [ { @@ -120,14 +138,32 @@ export function AlertIntegrations(props: Props) { : []), ], }, + + // transaction error rate panel + { + id: CREATE_TRANSACTION_ERROR_RATE_ALERT_PANEL_ID, + title: transactionErrorRateLabel, + items: [ + // threshold alerts + { + name: createThresholdAlertLabel, + onClick: () => { + setAlertType(AlertType.TransactionErrorRate); + setPopoverOpen(false); + }, + }, + ], + }, + + // error alerts panel { - id: CREATE_ERROR_RATE_ALERT_PANEL_ID, - title: errorRateLabel, + id: CREATE_ERROR_COUNT_ALERT_PANEL_ID, + title: errorCountLabel, items: [ { name: createThresholdAlertLabel, onClick: () => { - setAlertType(AlertType.ErrorRate); + setAlertType(AlertType.ErrorCount); setPopoverOpen(false); }, }, diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx index aa34515ea460a..45a7fa2a118f2 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx @@ -7,7 +7,7 @@ import { EuiPanel, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { isEmpty } from 'lodash'; import React, { useEffect, useState } from 'react'; -import { i18n } from '@kbn/i18n'; +import { INVALID_LICENSE } from '../../../../../../common/custom_link'; import { CustomLink } from '../../../../../../common/custom_link/custom_link_types'; import { useLicense } from '../../../../../hooks/useLicense'; import { useFetcher, FETCH_STATUS } from '../../../../../hooks/useFetcher'; @@ -94,15 +94,7 @@ export function CustomLinkOverview() { /> ) ) : ( - + )} diff --git a/x-pack/plugins/apm/public/components/app/TraceLink/get_redirect_to_trace_page_url.ts b/x-pack/plugins/apm/public/components/app/TraceLink/get_redirect_to_trace_page_url.ts new file mode 100644 index 0000000000000..4b1ced980010a --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TraceLink/get_redirect_to_trace_page_url.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { format } from 'url'; +import { TRACE_ID } from '../../../../common/elasticsearch_fieldnames'; + +export const getRedirectToTracePageUrl = ({ + traceId, + rangeFrom, + rangeTo, +}: { + traceId: string; + rangeFrom?: string; + rangeTo?: string; +}) => + format({ + pathname: `/traces`, + query: { + kuery: encodeURIComponent(`${TRACE_ID} : "${traceId}"`), + rangeFrom, + rangeTo, + }, + }); diff --git a/x-pack/plugins/apm/public/components/app/TraceLink/get_redirect_to_transaction_detail_page_url.ts b/x-pack/plugins/apm/public/components/app/TraceLink/get_redirect_to_transaction_detail_page_url.ts new file mode 100644 index 0000000000000..4738622aa9c31 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TraceLink/get_redirect_to_transaction_detail_page_url.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { format } from 'url'; +import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; + +export const getRedirectToTransactionDetailPageUrl = ({ + transaction, + rangeFrom, + rangeTo, +}: { + transaction: Transaction; + rangeFrom?: string; + rangeTo?: string; +}) => + format({ + pathname: `/services/${transaction.service.name}/transactions/view`, + query: { + traceId: transaction.trace.id, + transactionId: transaction.transaction.id, + transactionName: transaction.transaction.name, + transactionType: transaction.transaction.type, + rangeFrom, + rangeTo, + }, + }); diff --git a/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx b/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx index 584af956c2022..ee3b0a33ebbc2 100644 --- a/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx @@ -8,56 +8,16 @@ import { EuiEmptyPrompt } from '@elastic/eui'; import React from 'react'; import { Redirect, RouteComponentProps } from 'react-router-dom'; import styled from 'styled-components'; -import url from 'url'; -import { TRACE_ID } from '../../../../common/elasticsearch_fieldnames'; -import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher'; import { useUrlParams } from '../../../hooks/useUrlParams'; +import { getRedirectToTransactionDetailPageUrl } from './get_redirect_to_transaction_detail_page_url'; +import { getRedirectToTracePageUrl } from './get_redirect_to_trace_page_url'; const CentralizedContainer = styled.div` height: 100%; display: flex; `; -const redirectToTransactionDetailPage = ({ - transaction, - rangeFrom, - rangeTo, -}: { - transaction: Transaction; - rangeFrom?: string; - rangeTo?: string; -}) => - url.format({ - pathname: `/services/${transaction.service.name}/transactions/view`, - query: { - traceId: transaction.trace.id, - transactionId: transaction.transaction.id, - transactionName: transaction.transaction.name, - transactionType: transaction.transaction.type, - rangeFrom, - rangeTo, - }, - }); - -const redirectToTracePage = ({ - traceId, - rangeFrom, - rangeTo, -}: { - traceId: string; - rangeFrom?: string; - rangeTo?: string; -}) => - url.format({ - pathname: `/traces`, - query: { - kuery: encodeURIComponent(`${TRACE_ID} : "${traceId}"`), - rangeFrom, - rangeTo, - }, - }); - export function TraceLink({ match }: RouteComponentProps<{ traceId: string }>) { const { traceId } = match.params; const { urlParams } = useUrlParams(); @@ -80,12 +40,12 @@ export function TraceLink({ match }: RouteComponentProps<{ traceId: string }>) { ); if (traceId && status === FETCH_STATUS.SUCCESS) { const to = data.transaction - ? redirectToTransactionDetailPage({ + ? getRedirectToTransactionDetailPageUrl({ transaction: data.transaction, rangeFrom, rangeTo, }) - : redirectToTracePage({ traceId, rangeFrom, rangeTo }); + : getRedirectToTracePageUrl({ traceId, rangeFrom, rangeTo }); return ; } diff --git a/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx b/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx index f54255ec0cd18..cf2fe006f67d2 100644 --- a/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx @@ -14,9 +14,9 @@ import { fontSizes, truncate } from '../../../style/variables'; import { asMillisecondDuration } from '../../../utils/formatters'; import { EmptyMessage } from '../../shared/EmptyMessage'; import { ImpactBar } from '../../shared/ImpactBar'; -import { TransactionDetailLink } from '../../shared/Links/apm/TransactionDetailLink'; import { ITableColumn, ManagedTable } from '../../shared/ManagedTable'; import { LoadingStatePrompt } from '../../shared/LoadingStatePrompt'; +import { TransactionDetailLink } from '../../shared/Links/apm/TransactionDetailLink'; const StyledTransactionLink = styled(TransactionDetailLink)` font-size: ${fontSizes.large}; @@ -36,22 +36,23 @@ const traceListColumns: Array> = [ }), width: '40%', sortable: true, - render: (_: string, { sample }: TransactionGroup) => ( - + render: ( + _: string, + { serviceName, transactionName, transactionType }: TransactionGroup + ) => ( + - {sample.transaction.name} + {transactionName} ), }, { - field: 'sample.service.name', + field: 'serviceName', name: i18n.translate( 'xpack.apm.tracesTable.originatingServiceColumnLabel', { diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/__test__/distribution.test.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/__test__/distribution.test.ts index 488dba00f96f5..d944e3fa6db7c 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/__test__/distribution.test.ts +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/__test__/distribution.test.ts @@ -34,30 +34,20 @@ describe('Distribution', () => { }, ] as IBucket[]; expect(getFormattedBuckets(buckets, 20)).toEqual([ - { x: 20, x0: 0, y: 0, style: { cursor: 'default' }, samples: [] }, - { x: 40, x0: 20, y: 0, style: { cursor: 'default' }, samples: [] }, - { x: 60, x0: 40, y: 0, style: { cursor: 'default' }, samples: [] }, + { x: 20, x0: 0, y: 0, style: { cursor: 'default' } }, + { x: 40, x0: 20, y: 0, style: { cursor: 'default' } }, + { x: 60, x0: 40, y: 0, style: { cursor: 'default' } }, { x: 80, x0: 60, y: 5, style: { cursor: 'pointer' }, - samples: [ - { - transactionId: 'someTransactionId', - }, - ], }, { x: 100, x0: 80, y: 100, style: { cursor: 'pointer' }, - samples: [ - { - transactionId: 'anotherTransactionId', - }, - ], }, ]); }); diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx index 069c4468d206b..fa5a9956c8287 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import d3 from 'd3'; import { isEmpty } from 'lodash'; import React, { useCallback } from 'react'; -import { useHistory } from 'react-router-dom'; +import { ValuesType } from 'utility-types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { TransactionDistributionAPIResponse } from '../../../../../server/lib/transactions/distribution'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths @@ -19,11 +19,9 @@ import { getDurationFormatter } from '../../../../utils/formatters'; // @ts-expect-error import Histogram from '../../../shared/charts/Histogram'; import { EmptyMessage } from '../../../shared/EmptyMessage'; -import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; interface IChartPoint { - samples: IBucket['samples']; x0: number; x: number; y: number; @@ -40,7 +38,6 @@ export function getFormattedBuckets(buckets: IBucket[], bucketSize: number) { return buckets.map( ({ samples, count, key }): IChartPoint => { return { - samples, x0: key, x: key + bucketSize, y: count, @@ -97,6 +94,9 @@ interface Props { urlParams: IUrlParams; isLoading: boolean; bucketIndex: number; + onBucketClick: ( + bucket: ValuesType + ) => void; } export function TransactionDistribution(props: Props) { @@ -105,8 +105,8 @@ export function TransactionDistribution(props: Props) { urlParams: { transactionType }, isLoading, bucketIndex, + onBucketClick, } = props; - const history = useHistory(); /* eslint-disable-next-line react-hooks/exhaustive-deps */ const formatYShort = useCallback(getFormatYShort(transactionType), [ @@ -134,6 +134,14 @@ export function TransactionDistribution(props: Props) { ); } + function getBucketFromChartPoint(chartPoint: IChartPoint) { + const clickedBucket = distribution?.buckets.find((bucket) => { + return bucket.key === chartPoint.x0; + }); + + return clickedBucket; + } + const buckets = getFormattedBuckets( distribution.buckets, distribution.bucketSize @@ -175,31 +183,29 @@ export function TransactionDistribution(props: Props) { buckets={buckets} bucketSize={distribution.bucketSize} bucketIndex={bucketIndex} - onClick={(bucket: IChartPoint) => { - if (!isEmpty(bucket.samples)) { - const sample = bucket.samples[0]; - history.push({ - ...history.location, - search: fromQuery({ - ...toQuery(history.location.search), - transactionId: sample.transactionId, - traceId: sample.traceId, - }), - }); + onClick={(chartPoint: IChartPoint) => { + const clickedBucket = getBucketFromChartPoint(chartPoint); + + if (clickedBucket) { + onBucketClick(clickedBucket); } }} formatX={(time: number) => timeFormatter(time).formatted} formatYShort={formatYShort} formatYLong={formatYLong} - verticalLineHover={(bucket: IChartPoint) => isEmpty(bucket.samples)} - backgroundHover={(bucket: IChartPoint) => !isEmpty(bucket.samples)} - tooltipHeader={(bucket: IChartPoint) => { - const xFormatted = timeFormatter(bucket.x); - const x0Formatted = timeFormatter(bucket.x0); + verticalLineHover={(point: IChartPoint) => + isEmpty(getBucketFromChartPoint(point)?.samples) + } + backgroundHover={(point: IChartPoint) => + !isEmpty(getBucketFromChartPoint(point)?.samples) + } + tooltipHeader={(point: IChartPoint) => { + const xFormatted = timeFormatter(point.x); + const x0Formatted = timeFormatter(point.x0); return `${x0Formatted.value} - ${xFormatted.value} ${xFormatted.unit}`; }} - tooltipFooter={(bucket: IChartPoint) => - isEmpty(bucket.samples) && + tooltipFooter={(point: IChartPoint) => + isEmpty(getBucketFromChartPoint(point)?.samples) && i18n.translate( 'xpack.apm.transactionDetails.transactionsDurationDistributionChart.noSampleTooltip', { diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx index d166656db0672..3bf4807877428 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx @@ -132,7 +132,7 @@ export function Waterfall({
{ setIsAccordionOpen((isOpen) => !isOpen); }} diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx index 614089c0457ac..e31a2b24f1d15 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx @@ -13,21 +13,29 @@ import { EuiTitle, } from '@elastic/eui'; import React, { useMemo } from 'react'; +import { isEmpty, flatten } from 'lodash'; +import { useHistory } from 'react-router-dom'; import { RouteComponentProps } from 'react-router-dom'; -import { useTrackPageview } from '../../../../../observability/public'; -import { Projection } from '../../../../common/projections'; -import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; -import { FETCH_STATUS } from '../../../hooks/useFetcher'; import { useTransactionCharts } from '../../../hooks/useTransactionCharts'; import { useTransactionDistribution } from '../../../hooks/useTransactionDistribution'; -import { useUrlParams } from '../../../hooks/useUrlParams'; import { useWaterfall } from '../../../hooks/useWaterfall'; import { ApmHeader } from '../../shared/ApmHeader'; import { TransactionCharts } from '../../shared/charts/TransactionCharts'; -import { HeightRetainer } from '../../shared/HeightRetainer'; -import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { TransactionDistribution } from './Distribution'; import { WaterfallWithSummmary } from './WaterfallWithSummmary'; +import { FETCH_STATUS } from '../../../hooks/useFetcher'; +import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; +import { useTrackPageview } from '../../../../../observability/public'; +import { Projection } from '../../../../common/projections'; +import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; +import { useUrlParams } from '../../../hooks/useUrlParams'; +import { LocalUIFilters } from '../../shared/LocalUIFilters'; +import { HeightRetainer } from '../../shared/HeightRetainer'; + +interface Sample { + traceId: string; + transactionId: string; +} type TransactionDetailsProps = RouteComponentProps<{ serviceName: string }>; @@ -37,6 +45,7 @@ export function TransactionDetails({ }: TransactionDetailsProps) { const { serviceName } = match.params; const { urlParams } = useUrlParams(); + const history = useHistory(); const { data: distributionData, status: distributionStatus, @@ -64,15 +73,35 @@ export function TransactionDetails({ return config; }, [transactionName, transactionType, serviceName]); - const bucketIndex = distributionData.buckets.findIndex((bucket) => - bucket.samples.some( - (sample) => - sample.transactionId === urlParams.transactionId && - sample.traceId === urlParams.traceId - ) + const selectedSample = flatten( + distributionData.buckets.map((bucket) => bucket.samples) + ).find( + (sample) => + sample.transactionId === urlParams.transactionId && + sample.traceId === urlParams.traceId ); - const traceSamples = distributionData.buckets[bucketIndex]?.samples; + const bucketWithSample = + selectedSample && + distributionData.buckets.find((bucket) => + bucket.samples.includes(selectedSample) + ); + + const traceSamples = bucketWithSample?.samples ?? []; + const bucketIndex = bucketWithSample + ? distributionData.buckets.indexOf(bucketWithSample) + : -1; + + const selectSampleFromBucketClick = (sample: Sample) => { + history.push({ + ...history.location, + search: fromQuery({ + ...toQuery(history.location.search), + transactionId: sample.transactionId, + traceId: sample.traceId, + }), + }); + }; return (
@@ -102,6 +131,11 @@ export function TransactionDetails({ isLoading={distributionStatus === FETCH_STATUS.LOADING} urlParams={urlParams} bucketIndex={bucketIndex} + onBucketClick={(bucket) => { + if (!isEmpty(bucket.samples)) { + selectSampleFromBucketClick(bucket.samples[0]); + } + }} /> diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionList/TransactionList.stories.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionList/TransactionList.stories.tsx index bc9df71c534ef..a65589bdd147f 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionList/TransactionList.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionList/TransactionList.stories.tsx @@ -15,102 +15,19 @@ storiesOf('app/TransactionOverview/TransactionList', module).add( () => { const items: TransactionGroup[] = [ { - name: - 'GET /api/v1/regions/azure-eastus2/clusters/elasticsearch/xc18de071deb4262be54baebf5f6a1ce/proxy/_snapshot/found-snapshots/_all', - sample: { - container: { - id: - 'xa802046074071c9c828e8db3b7ef92ea0484d9fe783b9c518f65a7b45dfdd2c', - }, - agent: { - name: 'java', - ephemeral_id: 'x787d6b7-3241-4b55-ba49-0c96bc9857d1', - version: '1.17.0', - }, - process: { - pid: 28, - title: '/usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java', - }, - processor: { - name: 'transaction', - event: 'transaction', - }, - labels: { - path: - '/api/v1/regions/azure-eastus2/clusters/elasticsearch/xc18de071deb4262be54baebf5f6a1ce/proxy/_snapshot/found-snapshots/_all', - status_code: '200', - request_method: 'GET', - request_id: 'x273dc2477e021979125e0ec67e8d778', - }, - observer: { - hostname: 'x840922c967b', - name: 'instance-000000000x', - id: 'xb384baf-c16a-415a-928a-a10635a04b81', - ephemeral_id: 'x9227f0e-848d-423e-a65a-5fdee321f4a9', - type: 'apm-server', - version: '7.8.1', - version_major: 7, - }, - trace: { - id: 'x998d7e5db84aa8341b358a264a78984', - }, - '@timestamp': '2020-08-26T14:40:31.472Z', - ecs: { - version: '1.5.0', - }, - service: { - node: { - name: - 'xa802046074071c9c828e8db3b7ef92ea0484d9fe783b9c518f65a7b45dfdd2c', - }, - environment: 'qa', - framework: { - name: 'API', - }, - name: 'adminconsole', - runtime: { - name: 'Java', - version: '1.8.0_265', - }, - language: { - name: 'Java', - version: '1.8.0_265', - }, - version: 'ms-44.1-BC_1', - }, - host: { - hostname: 'xa8020460740', - os: { - platform: 'Linux', - }, - ip: '3.83.239.24', - name: 'xa8020460740', - architecture: 'amd64', - }, - transaction: { - duration: { - us: 8260617, - }, - result: 'HTTP 2xx', - name: - 'GET /api/v1/regions/azure-eastus2/clusters/elasticsearch/xc18de071deb4262be54baebf5f6a1ce/proxy/_snapshot/found-snapshots/_all', - span_count: { - dropped: 0, - started: 8, - }, - id: 'xaa3cae6fd4f7023', - type: 'request', - sampled: true, - }, - timestamp: { - us: 1598452831472001, - }, + key: { + ['service.name']: 'adminconsole', + ['transaction.name']: + 'GET /api/v1/regions/azure-eastus2/clusters/elasticsearch/xc18de071deb4262be54baebf5f6a1ce/proxy/_snapshot/found-snapshots/_all', }, + transactionName: + 'GET /api/v1/regions/azure-eastus2/clusters/elasticsearch/xc18de071deb4262be54baebf5f6a1ce/proxy/_snapshot/found-snapshots/_all', + serviceName: 'adminconsole', + transactionType: 'request', p95: 11974156, averageResponseTime: 8087434.558974359, transactionsPerMinute: 0.40625, impact: 100, - impactRelative: 100, }, ]; diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionList/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionList/index.tsx index d8c6d7d28fa9f..68f9c240f1562 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionList/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/TransactionList/index.tsx @@ -8,7 +8,6 @@ import { EuiToolTip, EuiIconTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; import styled from 'styled-components'; -import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { TransactionGroup } from '../../../../../server/lib/transaction_groups/fetcher'; import { fontFamilyCode, truncate } from '../../../../style/variables'; @@ -46,20 +45,21 @@ export function TransactionList({ items, isLoading }: Props) { }), width: '50%', sortable: true, - render: (_, { sample }: TransactionGroup) => { + render: ( + _, + { serviceName, transactionName, transactionType }: TransactionGroup + ) => { return ( - <>{sample.transaction.name || NOT_AVAILABLE_LABEL} + <>{transactionName} ); diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionDetailLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionDetailLink.tsx index 2ca3dce5da9ce..c9b26b557512c 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionDetailLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionDetailLink.tsx @@ -11,8 +11,8 @@ import { pickKeys } from '../../../../../common/utils/pick_keys'; interface Props extends APMLinkExtendProps { serviceName: string; - traceId: string; - transactionId: string; + traceId?: string; + transactionId?: string; transactionName: string; transactionType: string; } diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx index 3deba69a25df2..cbf9ba009dce2 100644 --- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx @@ -24,7 +24,7 @@ interface Props { function ServiceNameFilter({ loading, serviceNames }: Props) { const history = useHistory(); const { - urlParams: { serviceName }, + urlParams: { serviceName: selectedServiceName }, } = useUrlParams(); const options = serviceNames.map((type) => ({ @@ -47,10 +47,22 @@ function ServiceNameFilter({ loading, serviceNames }: Props) { ); useEffect(() => { - if (!serviceName && serviceNames.length > 0) { - updateServiceName(serviceNames[0]); + if (serviceNames?.length > 0) { + // select first from the list + if (!selectedServiceName) { + updateServiceName(serviceNames[0]); + } + + // in case serviceName is cached from url and isn't present in current list + if (selectedServiceName && !serviceNames.includes(selectedServiceName)) { + updateServiceName(serviceNames[0]); + } + } + + if (selectedServiceName && serviceNames.length === 0 && !loading) { + updateServiceName(''); } - }, [serviceNames, serviceName, updateServiceName]); + }, [serviceNames, selectedServiceName, updateServiceName, loading]); return ( <> @@ -68,7 +80,7 @@ function ServiceNameFilter({ loading, serviceNames }: Props) { isLoading={loading} data-cy="serviceNameFilter" options={options} - value={serviceName} + value={selectedServiceName} compressed={true} onChange={(event) => { updateServiceName(event.target.value); diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/__test__/Stackframe.test.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/__test__/Stackframe.test.tsx index c502237235578..589b2dd27fbc4 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/__test__/Stackframe.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/__test__/Stackframe.test.tsx @@ -11,6 +11,12 @@ import { mountWithTheme } from '../../../../utils/testHelpers'; import { Stackframe as StackframeComponent } from '../Stackframe'; import stacktracesMock from './stacktraces.json'; +jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { + return { + htmlIdGenerator: () => () => `generated-id`, + }; +}); + describe('Stackframe', () => { describe('when stackframe has source lines', () => { let wrapper: ReactWrapper; diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/__test__/__snapshots__/Stackframe.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/Stacktrace/__test__/__snapshots__/Stackframe.test.tsx.snap index f87cc62d7809d..a5f8c40876540 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/__test__/__snapshots__/Stackframe.test.tsx.snap +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/__test__/__snapshots__/Stackframe.test.tsx.snap @@ -177,6 +177,7 @@ exports[`Stackframe when stackframe has source lines should render correctly 1`] aria-controls="test" aria-expanded={false} className="euiAccordion__button" + id="generated-id" onClick={[Function]} type="button" > diff --git a/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx index 85d975870d9bc..e806f556347f1 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx @@ -70,6 +70,7 @@ export function ErroneousTransactionsRateChart() { { it('should call onClick with bucket', () => { expect(onClick).toHaveBeenCalledWith({ - samples: [ - { - transactionId: '99c50a5b-44b4-4289-a3d1-a2815d128192', - }, - ], style: { cursor: 'pointer' }, xCenter: 869010, x0: 811076, diff --git a/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts b/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts index d93a27df1c861..65482c9d21c16 100644 --- a/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts +++ b/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts @@ -4,12 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useParams } from 'react-router-dom'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { TransactionDistributionAPIResponse } from '../../server/lib/transactions/distribution'; -import { useUiFilters } from '../context/UrlParamsContext'; +import { flatten, omit } from 'lodash'; +import { useHistory, useParams } from 'react-router-dom'; import { IUrlParams } from '../context/UrlParamsContext/types'; import { useFetcher } from './useFetcher'; +import { useUiFilters } from '../context/UrlParamsContext'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { TransactionDistributionAPIResponse } from '../../server/lib/transactions/distribution'; +import { toQuery, fromQuery } from '../components/shared/Links/url_helpers'; +import { maybe } from '../../common/utils/maybe'; const INITIAL_DATA = { buckets: [] as TransactionDistributionAPIResponse['buckets'], @@ -29,10 +32,12 @@ export function useTransactionDistribution(urlParams: IUrlParams) { } = urlParams; const uiFilters = useUiFilters(urlParams); + const history = useHistory(); + const { data = INITIAL_DATA, status, error } = useFetcher( - (callApmApi) => { + async (callApmApi) => { if (serviceName && start && end && transactionType && transactionName) { - return callApmApi({ + const response = await callApmApi({ pathname: '/api/apm/services/{serviceName}/transaction_groups/distribution', params: { @@ -50,6 +55,39 @@ export function useTransactionDistribution(urlParams: IUrlParams) { }, }, }); + + const selectedSample = + transactionId && traceId + ? flatten(response.buckets.map((bucket) => bucket.samples)).find( + (sample) => + sample.transactionId === transactionId && + sample.traceId === traceId + ) + : undefined; + + if (!selectedSample) { + // selected sample was not found. select a new one: + // sorted by total number of requests, but only pick + // from buckets that have samples + const preferredSample = maybe( + response.buckets + .filter((bucket) => bucket.samples.length > 0) + .sort((bucket) => bucket.count)[0]?.samples[0] + ); + + history.push({ + ...history.location, + search: fromQuery({ + ...omit(toQuery(history.location.search), [ + 'traceId', + 'transactionId', + ]), + ...preferredSample, + }), + }); + } + + return response; } }, // the histogram should not be refetched if the transactionId or traceId changes diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index 51ac6673251fb..ab3f1026a92dd 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { i18n } from '@kbn/i18n'; -import { lazy } from 'react'; import { ConfigSchema } from '.'; import { FetchDataParams, @@ -34,10 +32,10 @@ import { TriggersAndActionsUIPublicPluginSetup, TriggersAndActionsUIPublicPluginStart, } from '../../triggers_actions_ui/public'; -import { AlertType } from '../common/alert_types'; import { featureCatalogueEntry } from './featureCatalogueEntry'; import { toggleAppLinkInNav } from './toggleAppLinkInNav'; import { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; +import { registerApmAlerts } from './components/alerting/register_apm_alerts'; export type ApmPluginSetup = void; export type ApmPluginStart = void; @@ -147,51 +145,6 @@ export class ApmPlugin implements Plugin { } public start(core: CoreStart, plugins: ApmPluginStartDeps) { toggleAppLinkInNav(core, this.initializerContext.config.get()); - - plugins.triggers_actions_ui.alertTypeRegistry.register({ - id: AlertType.ErrorRate, - name: i18n.translate('xpack.apm.alertTypes.errorRate', { - defaultMessage: 'Error rate', - }), - iconClass: 'bell', - alertParamsExpression: lazy( - () => import('./components/shared/ErrorRateAlertTrigger') - ), - validate: () => ({ - errors: [], - }), - requiresAppContext: true, - }); - - plugins.triggers_actions_ui.alertTypeRegistry.register({ - id: AlertType.TransactionDuration, - name: i18n.translate('xpack.apm.alertTypes.transactionDuration', { - defaultMessage: 'Transaction duration', - }), - iconClass: 'bell', - alertParamsExpression: lazy( - () => import('./components/shared/TransactionDurationAlertTrigger') - ), - validate: () => ({ - errors: [], - }), - requiresAppContext: true, - }); - - plugins.triggers_actions_ui.alertTypeRegistry.register({ - id: AlertType.TransactionDurationAnomaly, - name: i18n.translate('xpack.apm.alertTypes.transactionDurationAnomaly', { - defaultMessage: 'Transaction duration anomaly', - }), - iconClass: 'bell', - alertParamsExpression: lazy( - () => - import('./components/shared/TransactionDurationAnomalyAlertTrigger') - ), - validate: () => ({ - errors: [], - }), - requiresAppContext: true, - }); + registerApmAlerts(plugins.triggers_actions_ui.alertTypeRegistry); } } diff --git a/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js b/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js index f6f64db164f9a..1de98515e1aac 100644 --- a/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js +++ b/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js @@ -11,6 +11,7 @@ const { promisify } = require('util'); const path = require('path'); const json5 = require('json5'); const execa = require('execa'); +const { omit } = require('lodash'); const readFile = promisify(fs.readFile); const writeFile = promisify(fs.writeFile); @@ -27,6 +28,7 @@ function prepareParentTsConfigs() { return Promise.all( [ path.resolve(xpackRoot, 'tsconfig.json'), + path.resolve(kibanaRoot, 'tsconfig.base.json'), path.resolve(kibanaRoot, 'tsconfig.json'), ].map(async (filename) => { const config = json5.parse(await readFile(filename, 'utf-8')); @@ -35,7 +37,11 @@ function prepareParentTsConfigs() { filename, JSON.stringify( { - ...config, + ...omit(config, 'references'), + compilerOptions: { + ...config.compilerOptions, + incremental: false, + }, include: [], }, null, diff --git a/x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js b/x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js index c75e16f74b932..b32bb9b20796e 100644 --- a/x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js +++ b/x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js @@ -13,6 +13,7 @@ const tsconfigTpl = path.resolve(__dirname, './tsconfig.json'); const filesToIgnore = [ path.resolve(xpackRoot, 'tsconfig.json'), path.resolve(kibanaRoot, 'tsconfig.json'), + path.resolve(kibanaRoot, 'tsconfig.base.json'), ]; module.exports = { diff --git a/x-pack/plugins/apm/server/feature.ts b/x-pack/plugins/apm/server/feature.ts index 0f6061653f352..1cda70a140c67 100644 --- a/x-pack/plugins/apm/server/feature.ts +++ b/x-pack/plugins/apm/server/feature.ts @@ -5,7 +5,12 @@ */ import { i18n } from '@kbn/i18n'; +import { LicenseType } from '../../licensing/common/types'; import { AlertType } from '../common/alert_types'; +import { + LicensingPluginSetup, + LicensingRequestHandlerContext, +} from '../../licensing/server'; export const APM_FEATURE = { id: 'apm', @@ -58,5 +63,43 @@ export const APM_FEATURE = { }, }; -export const APM_SERVICE_MAPS_FEATURE_NAME = 'APM service maps'; -export const APM_SERVICE_MAPS_LICENSE_TYPE = 'platinum'; +interface Feature { + name: string; + license: LicenseType; +} +type FeatureName = 'serviceMaps' | 'ml' | 'customLinks'; +export const features: Record = { + serviceMaps: { + name: 'APM service maps', + license: 'platinum', + }, + ml: { + name: 'APM machine learning', + license: 'platinum', + }, + customLinks: { + name: 'APM custom links', + license: 'gold', + }, +}; + +export function registerFeaturesUsage({ + licensingPlugin, +}: { + licensingPlugin: LicensingPluginSetup; +}) { + Object.values(features).forEach(({ name, license }) => { + licensingPlugin.featureUsage.register(name, license); + }); +} + +export function notifyFeatureUsage({ + licensingPlugin, + featureName, +}: { + licensingPlugin: LicensingRequestHandlerContext; + featureName: FeatureName; +}) { + const feature = features[featureName]; + licensingPlugin.featureUsage.notifyUsage(feature.name); +} diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts index 29b2a77df348e..090110b0454c0 100644 --- a/x-pack/plugins/apm/server/index.ts +++ b/x-pack/plugins/apm/server/index.ts @@ -8,6 +8,7 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { PluginInitializerContext } from 'src/core/server'; import { APMOSSConfig } from 'src/plugins/apm_oss/server'; import { APMPlugin } from './plugin'; +import { SearchAggregatedTransactionSetting } from '../common/aggregated_transactions'; export const config = { exposeToBrowser: { @@ -30,6 +31,16 @@ export const config = { transactionGroupBucketSize: schema.number({ defaultValue: 1000 }), maxTraceItems: schema.number({ defaultValue: 1000 }), }), + searchAggregatedTransactions: schema.oneOf( + [ + schema.literal(SearchAggregatedTransactionSetting.auto), + schema.literal(SearchAggregatedTransactionSetting.always), + schema.literal(SearchAggregatedTransactionSetting.never), + ], + { + defaultValue: SearchAggregatedTransactionSetting.never, + } + ), telemetryCollectionEnabled: schema.boolean({ defaultValue: true }), metricsInterval: schema.number({ defaultValue: 30 }), }), @@ -69,6 +80,8 @@ export function mergeConfigs( 'xpack.apm.autocreateApmIndexPattern': apmConfig.autocreateApmIndexPattern, 'xpack.apm.telemetryCollectionEnabled': apmConfig.telemetryCollectionEnabled, + 'xpack.apm.searchAggregatedTransactions': + apmConfig.searchAggregatedTransactions, 'xpack.apm.metricsInterval': apmConfig.metricsInterval, }; } diff --git a/x-pack/plugins/apm/server/lib/alerts/action_variables.ts b/x-pack/plugins/apm/server/lib/alerts/action_variables.ts new file mode 100644 index 0000000000000..f2558da3a30e4 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/alerts/action_variables.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const apmActionVariables = { + serviceName: { + description: i18n.translate( + 'xpack.apm.alerts.action_variables.serviceName', + { defaultMessage: 'The service the alert is created for' } + ), + name: 'serviceName', + }, + transactionType: { + description: i18n.translate( + 'xpack.apm.alerts.action_variables.transactionType', + { defaultMessage: 'The transaction type the alert is created for' } + ), + name: 'transactionType', + }, + environment: { + description: i18n.translate( + 'xpack.apm.alerts.action_variables.environment', + { defaultMessage: 'The transaction type the alert is created for' } + ), + name: 'environment', + }, + threshold: { + description: i18n.translate('xpack.apm.alerts.action_variables.threshold', { + defaultMessage: + 'Any trigger value above this value will cause the alert to fire', + }), + name: 'threshold', + }, + triggerValue: { + description: i18n.translate( + 'xpack.apm.alerts.action_variables.triggerValue', + { + defaultMessage: + 'The value that breached the threshold and triggered the alert', + } + ), + name: 'triggerValue', + }, +}; diff --git a/x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts b/x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts index 44ca80143bcd9..fcbb4cc5950e0 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts @@ -9,9 +9,10 @@ import { AlertingPlugin } from '../../../../alerts/server'; import { ActionsPlugin } from '../../../../actions/server'; import { registerTransactionDurationAlertType } from './register_transaction_duration_alert_type'; import { registerTransactionDurationAnomalyAlertType } from './register_transaction_duration_anomaly_alert_type'; -import { registerErrorRateAlertType } from './register_error_rate_alert_type'; +import { registerErrorCountAlertType } from './register_error_count_alert_type'; import { APMConfig } from '../..'; import { MlPluginSetup } from '../../../../ml/server'; +import { registerTransactionErrorRateAlertType } from './register_transaction_error_rate_alert_type'; interface Params { alerts: AlertingPlugin['setup']; @@ -30,7 +31,11 @@ export function registerApmAlerts(params: Params) { ml: params.ml, config$: params.config$, }); - registerErrorRateAlertType({ + registerErrorCountAlertType({ + alerts: params.alerts, + config$: params.config$, + }); + registerTransactionErrorRateAlertType({ alerts: params.alerts, config$: params.config$, }); diff --git a/x-pack/plugins/apm/server/lib/alerts/register_error_rate_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts similarity index 66% rename from x-pack/plugins/apm/server/lib/alerts/register_error_rate_alert_type.ts rename to x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts index 61e3dfee420a5..5455cd9f6a495 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_error_rate_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { schema, TypeOf } from '@kbn/config-schema'; +import { schema } from '@kbn/config-schema'; import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; -import { i18n } from '@kbn/i18n'; -import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; import { ESSearchResponse, @@ -17,11 +17,11 @@ import { import { PROCESSOR_EVENT, SERVICE_NAME, - SERVICE_ENVIRONMENT, } from '../../../common/elasticsearch_fieldnames'; import { AlertingPlugin } from '../../../../alerts/server'; import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; import { APMConfig } from '../..'; +import { apmActionVariables } from './action_variables'; interface RegisterAlertParams { alerts: AlertingPlugin['setup']; @@ -29,21 +29,21 @@ interface RegisterAlertParams { } const paramsSchema = schema.object({ - serviceName: schema.string(), windowSize: schema.number(), windowUnit: schema.string(), threshold: schema.number(), + serviceName: schema.string(), environment: schema.string(), }); -const alertTypeConfig = ALERT_TYPES_CONFIG[AlertType.ErrorRate]; +const alertTypeConfig = ALERT_TYPES_CONFIG[AlertType.ErrorCount]; -export function registerErrorRateAlertType({ +export function registerErrorCountAlertType({ alerts, config$, }: RegisterAlertParams) { alerts.registerType({ - id: AlertType.ErrorRate, + id: AlertType.ErrorCount, name: alertTypeConfig.name, actionGroups: alertTypeConfig.actionGroups, defaultActionGroupId: alertTypeConfig.defaultActionGroupId, @@ -52,37 +52,26 @@ export function registerErrorRateAlertType({ }, actionVariables: { context: [ - { - description: i18n.translate( - 'xpack.apm.registerErrorRateAlertType.variables.serviceName', - { - defaultMessage: 'Service name', - } - ), - name: 'serviceName', - }, + apmActionVariables.serviceName, + apmActionVariables.environment, + apmActionVariables.threshold, + apmActionVariables.triggerValue, ], }, producer: 'apm', executor: async ({ services, params }) => { const config = await config$.pipe(take(1)).toPromise(); - - const alertParams = params as TypeOf; - + const alertParams = params; const indices = await getApmIndices({ config, savedObjectsClient: services.savedObjectsClient, }); - const environmentTerm = - alertParams.environment === ENVIRONMENT_ALL.value - ? [] - : [{ term: { [SERVICE_ENVIRONMENT]: alertParams.environment } }]; - const searchParams = { index: indices['apm_oss.errorIndices'], size: 0, body: { + track_total_hits: true, query: { bool: { filter: [ @@ -93,21 +82,12 @@ export function registerErrorRateAlertType({ }, }, }, - { - term: { - [PROCESSOR_EVENT]: 'error', - }, - }, - { - term: { - [SERVICE_NAME]: alertParams.serviceName, - }, - }, - ...environmentTerm, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.error } }, + { term: { [SERVICE_NAME]: alertParams.serviceName } }, + ...getEnvironmentUiFilterES(alertParams.environment), ], }, }, - track_total_hits: true, }, }; @@ -116,18 +96,19 @@ export function registerErrorRateAlertType({ ESSearchRequest > = await services.callCluster('search', searchParams); - const value = response.hits.total.value; + const errorCount = response.hits.total.value; - if (value && value > alertParams.threshold) { + if (errorCount > alertParams.threshold) { const alertInstance = services.alertInstanceFactory( - AlertType.ErrorRate + AlertType.ErrorCount ); alertInstance.scheduleActions(alertTypeConfig.defaultActionGroupId, { serviceName: alertParams.serviceName, + environment: alertParams.environment, + threshold: alertParams.threshold, + triggerValue: errorCount, }); } - - return {}; }, }); } diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts index ead28c325692d..373d4bd4da832 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { schema, TypeOf } from '@kbn/config-schema'; +import { schema } from '@kbn/config-schema'; import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; -import { i18n } from '@kbn/i18n'; -import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; +import { ProcessorEvent } from '../../../common/processor_event'; import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; import { ESSearchResponse } from '../../../typings/elasticsearch'; import { @@ -16,11 +15,12 @@ import { SERVICE_NAME, TRANSACTION_TYPE, TRANSACTION_DURATION, - SERVICE_ENVIRONMENT, } from '../../../common/elasticsearch_fieldnames'; import { AlertingPlugin } from '../../../../alerts/server'; import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; import { APMConfig } from '../..'; +import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { apmActionVariables } from './action_variables'; interface RegisterAlertParams { alerts: AlertingPlugin['setup']; @@ -57,42 +57,22 @@ export function registerTransactionDurationAlertType({ }, actionVariables: { context: [ - { - description: i18n.translate( - 'xpack.apm.registerTransactionDurationAlertType.variables.serviceName', - { - defaultMessage: 'Service name', - } - ), - name: 'serviceName', - }, - { - description: i18n.translate( - 'xpack.apm.registerTransactionDurationAlertType.variables.transactionType', - { - defaultMessage: 'Transaction type', - } - ), - name: 'transactionType', - }, + apmActionVariables.serviceName, + apmActionVariables.transactionType, + apmActionVariables.environment, + apmActionVariables.threshold, + apmActionVariables.triggerValue, ], }, producer: 'apm', executor: async ({ services, params }) => { const config = await config$.pipe(take(1)).toPromise(); - - const alertParams = params as TypeOf; - + const alertParams = params; const indices = await getApmIndices({ config, savedObjectsClient: services.savedObjectsClient, }); - const environmentTerm = - alertParams.environment === ENVIRONMENT_ALL.value - ? [] - : [{ term: { [SERVICE_ENVIRONMENT]: alertParams.environment } }]; - const searchParams = { index: indices['apm_oss.transactionIndices'], size: 0, @@ -107,33 +87,17 @@ export function registerTransactionDurationAlertType({ }, }, }, - { - term: { - [PROCESSOR_EVENT]: 'transaction', - }, - }, - { - term: { - [SERVICE_NAME]: alertParams.serviceName, - }, - }, - { - term: { - [TRANSACTION_TYPE]: alertParams.transactionType, - }, - }, - ...environmentTerm, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, + { term: { [SERVICE_NAME]: alertParams.serviceName } }, + { term: { [TRANSACTION_TYPE]: alertParams.transactionType } }, + ...getEnvironmentUiFilterES(alertParams.environment), ], }, }, aggs: { agg: alertParams.aggregationType === 'avg' - ? { - avg: { - field: TRANSACTION_DURATION, - }, - } + ? { avg: { field: TRANSACTION_DURATION } } : { percentiles: { field: TRANSACTION_DURATION, @@ -157,19 +121,23 @@ export function registerTransactionDurationAlertType({ const { agg } = response.aggregations; - const value = 'values' in agg ? Object.values(agg.values)[0] : agg?.value; + const transactionDuration = + 'values' in agg ? Object.values(agg.values)[0] : agg?.value; - if (value && value > alertParams.threshold * 1000) { + const threshold = alertParams.threshold * 1000; + + if (transactionDuration && transactionDuration > threshold) { const alertInstance = services.alertInstanceFactory( AlertType.TransactionDuration ); alertInstance.scheduleActions(alertTypeConfig.defaultActionGroupId, { transactionType: alertParams.transactionType, serviceName: alertParams.serviceName, + environment: alertParams.environment, + threshold, + triggerValue: transactionDuration, }); } - - return {}; }, }); } diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts index 93af51b572aa5..b3526b6a97ad9 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts @@ -4,15 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { schema, TypeOf } from '@kbn/config-schema'; +import { schema } from '@kbn/config-schema'; import { Observable } from 'rxjs'; -import { i18n } from '@kbn/i18n'; import { KibanaRequest } from '../../../../../../src/core/server'; import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; import { AlertingPlugin } from '../../../../alerts/server'; import { APMConfig } from '../..'; import { MlPluginSetup } from '../../../../ml/server'; import { getMLJobIds } from '../service_map/get_service_anomalies'; +import { apmActionVariables } from './action_variables'; interface RegisterAlertParams { alerts: AlertingPlugin['setup']; @@ -47,24 +47,9 @@ export function registerTransactionDurationAnomalyAlertType({ }, actionVariables: { context: [ - { - description: i18n.translate( - 'xpack.apm.registerTransactionDurationAnomalyAlertType.variables.serviceName', - { - defaultMessage: 'Service name', - } - ), - name: 'serviceName', - }, - { - description: i18n.translate( - 'xpack.apm.registerTransactionDurationAnomalyAlertType.variables.transactionType', - { - defaultMessage: 'Transaction type', - } - ), - name: 'transactionType', - }, + apmActionVariables.serviceName, + apmActionVariables.transactionType, + apmActionVariables.environment, ], }, producer: 'apm', @@ -72,7 +57,7 @@ export function registerTransactionDurationAnomalyAlertType({ if (!ml) { return; } - const alertParams = params as TypeOf; + const alertParams = params; const request = {} as KibanaRequest; const { mlAnomalySearch } = ml.mlSystemProvider(request); const anomalyDetectors = ml.anomalyDetectorsProvider(request); @@ -88,6 +73,7 @@ export function registerTransactionDurationAnomalyAlertType({ const anomalySearchParams = { body: { + terminateAfter: 1, size: 0, query: { bool: { @@ -131,10 +117,10 @@ export function registerTransactionDurationAnomalyAlertType({ ); alertInstance.scheduleActions(alertTypeConfig.defaultActionGroupId, { serviceName: alertParams.serviceName, + transactionType: alertParams.transactionType, + environment: alertParams.environment, }); } - - return {}; }, }); } diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts new file mode 100644 index 0000000000000..a6ed40fc15ec6 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { Observable } from 'rxjs'; +import { take } from 'rxjs/operators'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { EventOutcome } from '../../../common/event_outcome'; +import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; +import { ESSearchResponse } from '../../../typings/elasticsearch'; +import { + PROCESSOR_EVENT, + SERVICE_NAME, + TRANSACTION_TYPE, + EVENT_OUTCOME, +} from '../../../common/elasticsearch_fieldnames'; +import { AlertingPlugin } from '../../../../alerts/server'; +import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; +import { APMConfig } from '../..'; +import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { apmActionVariables } from './action_variables'; + +interface RegisterAlertParams { + alerts: AlertingPlugin['setup']; + config$: Observable; +} + +const paramsSchema = schema.object({ + windowSize: schema.number(), + windowUnit: schema.string(), + threshold: schema.number(), + transactionType: schema.string(), + serviceName: schema.string(), + environment: schema.string(), +}); + +const alertTypeConfig = ALERT_TYPES_CONFIG[AlertType.TransactionErrorRate]; + +export function registerTransactionErrorRateAlertType({ + alerts, + config$, +}: RegisterAlertParams) { + alerts.registerType({ + id: AlertType.TransactionErrorRate, + name: alertTypeConfig.name, + actionGroups: alertTypeConfig.actionGroups, + defaultActionGroupId: alertTypeConfig.defaultActionGroupId, + validate: { + params: paramsSchema, + }, + actionVariables: { + context: [ + apmActionVariables.transactionType, + apmActionVariables.serviceName, + apmActionVariables.environment, + apmActionVariables.threshold, + apmActionVariables.triggerValue, + ], + }, + producer: 'apm', + executor: async ({ services, params: alertParams }) => { + const config = await config$.pipe(take(1)).toPromise(); + const indices = await getApmIndices({ + config, + savedObjectsClient: services.savedObjectsClient, + }); + + const searchParams = { + index: indices['apm_oss.transactionIndices'], + size: 0, + body: { + track_total_hits: true, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + gte: `now-${alertParams.windowSize}${alertParams.windowUnit}`, + }, + }, + }, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, + { term: { [SERVICE_NAME]: alertParams.serviceName } }, + { term: { [TRANSACTION_TYPE]: alertParams.transactionType } }, + ...getEnvironmentUiFilterES(alertParams.environment), + ], + }, + }, + aggs: { + erroneous_transactions: { + filter: { term: { [EVENT_OUTCOME]: EventOutcome.failure } }, + }, + }, + }, + }; + + const response: ESSearchResponse< + unknown, + typeof searchParams + > = await services.callCluster('search', searchParams); + + if (!response.aggregations) { + return; + } + + const errornousTransactionsCount = + response.aggregations.erroneous_transactions.doc_count; + const totalTransactionCount = response.hits.total.value; + const transactionErrorRate = + (errornousTransactionsCount / totalTransactionCount) * 100; + + if (transactionErrorRate > alertParams.threshold) { + const alertInstance = services.alertInstanceFactory( + AlertType.TransactionErrorRate + ); + + alertInstance.scheduleActions(alertTypeConfig.defaultActionGroupId, { + serviceName: alertParams.serviceName, + transactionType: alertParams.transactionType, + environment: alertParams.environment, + threshold: alertParams.threshold, + triggerValue: transactionErrorRate, + }); + } + }, + }); +} diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts index 7bcd945d890ad..d0673335387c6 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts @@ -8,6 +8,7 @@ import { Logger } from 'kibana/server'; import uuid from 'uuid/v4'; import { snakeCase } from 'lodash'; import Boom from 'boom'; +import { ProcessorEvent } from '../../../common/processor_event'; import { ML_ERRORS } from '../../../common/anomaly_detection'; import { PromiseReturnType } from '../../../../observability/typings/common'; import { Setup } from '../helpers/setup_request'; @@ -79,7 +80,7 @@ async function createAnomalyDetectionJob({ query: { bool: { filter: [ - { term: { [PROCESSOR_EVENT]: 'transaction' } }, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, { exists: { field: TRANSACTION_DURATION } }, ...getEnvironmentUiFilterES(environment), ], diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts index a53068d152d03..fcd4f468d4367 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts @@ -85,7 +85,7 @@ export const tasks: TelemetryTask[] = [ query: { bool: { filter: [ - { term: { [PROCESSOR_EVENT]: 'transaction' } }, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, { range: { '@timestamp': { gte: start, lt: end } } }, ], }, @@ -606,7 +606,10 @@ export const tasks: TelemetryTask[] = [ timeout, query: { bool: { - filter: [{ term: { [PROCESSOR_EVENT]: 'error' } }, range1d], + filter: [ + { term: { [PROCESSOR_EVENT]: ProcessorEvent.error } }, + range1d, + ], }, }, aggs: { @@ -640,7 +643,7 @@ export const tasks: TelemetryTask[] = [ query: { bool: { filter: [ - { term: { [PROCESSOR_EVENT]: 'transaction' } }, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, range1d, ], }, @@ -674,7 +677,7 @@ export const tasks: TelemetryTask[] = [ query: { bool: { filter: [ - { term: { [PROCESSOR_EVENT]: 'transaction' } }, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, range1d, ], must_not: { diff --git a/x-pack/plugins/apm/server/lib/environments/get_all_environments.test.ts b/x-pack/plugins/apm/server/lib/environments/get_all_environments.test.ts index dfac607eb7232..141d58cc02ba8 100644 --- a/x-pack/plugins/apm/server/lib/environments/get_all_environments.test.ts +++ b/x-pack/plugins/apm/server/lib/environments/get_all_environments.test.ts @@ -21,6 +21,7 @@ describe('getAllEnvironments', () => { mock = await inspectSearchParams((setup) => getAllEnvironments({ serviceName: 'test', + searchAggregatedTransactions: false, setup, }) ); @@ -33,6 +34,7 @@ describe('getAllEnvironments', () => { getAllEnvironments({ serviceName: 'test', setup, + searchAggregatedTransactions: false, includeMissing: true, }) ); diff --git a/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts b/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts index 8060bf10da99c..95ff357937d47 100644 --- a/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts +++ b/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts @@ -11,14 +11,17 @@ import { SERVICE_ENVIRONMENT, } from '../../../common/elasticsearch_fieldnames'; import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values'; +import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; export async function getAllEnvironments({ serviceName, setup, + searchAggregatedTransactions, includeMissing = false, }: { serviceName?: string; setup: Setup; + searchAggregatedTransactions: boolean; includeMissing?: boolean; }) { const { apmEventClient } = setup; @@ -31,7 +34,9 @@ export async function getAllEnvironments({ const params = { apm: { events: [ - ProcessorEvent.transaction, + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), ProcessorEvent.error, ProcessorEvent.metric, ], diff --git a/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/index.ts b/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/index.ts new file mode 100644 index 0000000000000..167bcfd634520 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/index.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchAggregatedTransactionSetting } from '../../../../common/aggregated_transactions'; +import { rangeFilter } from '../../../../common/utils/range_filter'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { + TRANSACTION_DURATION, + TRANSACTION_DURATION_HISTOGRAM, +} from '../../../../common/elasticsearch_fieldnames'; +import { APMConfig } from '../../..'; +import { APMEventClient } from '../create_es_client/create_apm_event_client'; + +export async function getHasAggregatedTransactions({ + start, + end, + apmEventClient, +}: { + start?: number; + end?: number; + apmEventClient: APMEventClient; +}) { + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.metric], + }, + body: { + query: { + bool: { + filter: [ + { exists: { field: TRANSACTION_DURATION_HISTOGRAM } }, + ...(start && end ? [{ range: rangeFilter(start, end) }] : []), + ], + }, + }, + }, + terminateAfter: 1, + }); + + if (response.hits.total.value > 0) { + return true; + } + + return false; +} + +export async function getSearchAggregatedTransactions({ + config, + start, + end, + apmEventClient, +}: { + config: APMConfig; + start?: number; + end?: number; + apmEventClient: APMEventClient; +}): Promise { + const searchAggregatedTransactions = + config['xpack.apm.searchAggregatedTransactions']; + + if ( + searchAggregatedTransactions === SearchAggregatedTransactionSetting.auto + ) { + return getHasAggregatedTransactions({ start, end, apmEventClient }); + } + + return ( + searchAggregatedTransactions === SearchAggregatedTransactionSetting.always + ); +} + +export function getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions: boolean +) { + return searchAggregatedTransactions + ? TRANSACTION_DURATION_HISTOGRAM + : TRANSACTION_DURATION; +} + +export function getDocumentTypeFilterForAggregatedTransactions( + searchAggregatedTransactions: boolean +) { + return searchAggregatedTransactions + ? [{ exists: { field: TRANSACTION_DURATION_HISTOGRAM } }] + : []; +} + +export function getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions: boolean +): ProcessorEvent.metric | ProcessorEvent.transaction { + return searchAggregatedTransactions + ? ProcessorEvent.metric + : ProcessorEvent.transaction; +} diff --git a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts index 6ff98a9be75f9..ea8d02eb833cf 100644 --- a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts +++ b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts @@ -5,11 +5,14 @@ */ import { ESFilter } from '../../../../typings/elasticsearch'; -import { ENVIRONMENT_NOT_DEFINED } from '../../../../common/environment_filter_values'; +import { + ENVIRONMENT_NOT_DEFINED, + ENVIRONMENT_ALL, +} from '../../../../common/environment_filter_values'; import { SERVICE_ENVIRONMENT } from '../../../../common/elasticsearch_fieldnames'; export function getEnvironmentUiFilterES(environment?: string): ESFilter[] { - if (!environment) { + if (!environment || environment === ENVIRONMENT_ALL.value) { return []; } if (environment === ENVIRONMENT_NOT_DEFINED.value) { diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts index d35403ad35d94..736c7ad2d1089 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts @@ -32,9 +32,9 @@ export function unpackProcessorEvents( ) { const { apm, ...params } = request; - const index = uniq( - apm.events.map((event) => indices[processorEventIndexMap[event]]) - ); + const events = uniq(apm.events); + + const index = events.map((event) => indices[processorEventIndexMap[event]]); const withFilterForProcessorEvent: ESSearchRequest & { body: { query: { bool: { filter: ESFilter[] } } }; @@ -50,7 +50,7 @@ export function unpackProcessorEvents( withFilterForProcessorEvent.body.query.bool.filter.push({ terms: { - [PROCESSOR_EVENT]: apm.events, + [PROCESSOR_EVENT]: events, }, }); diff --git a/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap index b88c90a213c67..2868dcfda97b6 100644 --- a/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap @@ -203,16 +203,50 @@ Object { "memoryUsedAvg": Object { "avg": Object { "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + "lang": "painless", + "source": " + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = 'system.process.cgroup.memory.mem.limit.bytes'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value; + + double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value; + + return used / total; + ", }, }, }, "memoryUsedMax": Object { "max": Object { "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + "lang": "painless", + "source": " + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = 'system.process.cgroup.memory.mem.limit.bytes'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value; + + double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value; + + return used / total; + ", }, }, }, @@ -221,16 +255,50 @@ Object { "memoryUsedAvg": Object { "avg": Object { "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + "lang": "painless", + "source": " + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = 'system.process.cgroup.memory.mem.limit.bytes'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value; + + double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value; + + return used / total; + ", }, }, }, "memoryUsedMax": Object { "max": Object { "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + "lang": "painless", + "source": " + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = 'system.process.cgroup.memory.mem.limit.bytes'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value; + + double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value; + + return used / total; + ", }, }, }, @@ -275,12 +343,7 @@ Object { }, Object { "exists": Object { - "field": "system.memory.actual.free", - }, - }, - Object { - "exists": Object { - "field": "system.memory.total", + "field": "system.process.cgroup.memory.mem.usage.bytes", }, }, ], @@ -682,16 +745,50 @@ Object { "memoryUsedAvg": Object { "avg": Object { "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + "lang": "painless", + "source": " + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = 'system.process.cgroup.memory.mem.limit.bytes'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value; + + double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value; + + return used / total; + ", }, }, }, "memoryUsedMax": Object { "max": Object { "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + "lang": "painless", + "source": " + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = 'system.process.cgroup.memory.mem.limit.bytes'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value; + + double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value; + + return used / total; + ", }, }, }, @@ -700,16 +797,50 @@ Object { "memoryUsedAvg": Object { "avg": Object { "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + "lang": "painless", + "source": " + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = 'system.process.cgroup.memory.mem.limit.bytes'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value; + + double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value; + + return used / total; + ", }, }, }, "memoryUsedMax": Object { "max": Object { "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + "lang": "painless", + "source": " + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = 'system.process.cgroup.memory.mem.limit.bytes'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value; + + double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value; + + return used / total; + ", }, }, }, @@ -760,12 +891,7 @@ Object { }, Object { "exists": Object { - "field": "system.memory.actual.free", - }, - }, - Object { - "exists": Object { - "field": "system.memory.total", + "field": "system.process.cgroup.memory.mem.usage.bytes", }, }, ], @@ -1157,16 +1283,50 @@ Object { "memoryUsedAvg": Object { "avg": Object { "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + "lang": "painless", + "source": " + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = 'system.process.cgroup.memory.mem.limit.bytes'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value; + + double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value; + + return used / total; + ", }, }, }, "memoryUsedMax": Object { "max": Object { "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + "lang": "painless", + "source": " + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = 'system.process.cgroup.memory.mem.limit.bytes'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value; + + double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value; + + return used / total; + ", }, }, }, @@ -1175,16 +1335,50 @@ Object { "memoryUsedAvg": Object { "avg": Object { "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + "lang": "painless", + "source": " + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = 'system.process.cgroup.memory.mem.limit.bytes'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value; + + double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value; + + return used / total; + ", }, }, }, "memoryUsedMax": Object { "max": Object { "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + "lang": "painless", + "source": " + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = 'system.process.cgroup.memory.mem.limit.bytes'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value; + + double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value; + + return used / total; + ", }, }, }, @@ -1224,12 +1418,7 @@ Object { }, Object { "exists": Object { - "field": "system.memory.actual.free", - }, - }, - Object { - "exists": Object { - "field": "system.memory.total", + "field": "system.process.cgroup.memory.mem.usage.bytes", }, }, ], diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts index 316b0d59d2c5b..a60576ca0c175 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts @@ -6,6 +6,8 @@ import { i18n } from '@kbn/i18n'; import { + METRIC_CGROUP_MEMORY_LIMIT_BYTES, + METRIC_CGROUP_MEMORY_USAGE_BYTES, METRIC_SYSTEM_FREE_MEMORY, METRIC_SYSTEM_TOTAL_MEMORY, } from '../../../../../../common/elasticsearch_fieldnames'; @@ -14,8 +16,8 @@ import { SetupTimeRange, SetupUIFilters, } from '../../../../helpers/setup_request'; -import { ChartBase } from '../../../types'; import { fetchAndTransformMetrics } from '../../../fetch_and_transform_metrics'; +import { ChartBase } from '../../../types'; const series = { memoryUsedMax: { @@ -43,36 +45,68 @@ const chartBase: ChartBase = { series, }; -export const percentMemoryUsedScript = { +export const percentSystemMemoryUsedScript = { lang: 'expression', source: `1 - doc['${METRIC_SYSTEM_FREE_MEMORY}'] / doc['${METRIC_SYSTEM_TOTAL_MEMORY}']`, }; +export const percentCgroupMemoryUsedScript = { + lang: 'painless', + source: ` + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = '${METRIC_CGROUP_MEMORY_LIMIT_BYTES}'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['${METRIC_SYSTEM_TOTAL_MEMORY}'].value; + + double used = doc['${METRIC_CGROUP_MEMORY_USAGE_BYTES}'].value; + + return used / total; + `, +}; + export async function getMemoryChartData( setup: Setup & SetupTimeRange & SetupUIFilters, serviceName: string, serviceNodeName?: string ) { - return fetchAndTransformMetrics({ + const cgroupResponse = await fetchAndTransformMetrics({ setup, serviceName, serviceNodeName, chartBase, aggs: { - memoryUsedAvg: { avg: { script: percentMemoryUsedScript } }, - memoryUsedMax: { max: { script: percentMemoryUsedScript } }, + memoryUsedAvg: { avg: { script: percentCgroupMemoryUsedScript } }, + memoryUsedMax: { max: { script: percentCgroupMemoryUsedScript } }, }, additionalFilters: [ - { - exists: { - field: METRIC_SYSTEM_FREE_MEMORY, - }, - }, - { - exists: { - field: METRIC_SYSTEM_TOTAL_MEMORY, - }, - }, + { exists: { field: METRIC_CGROUP_MEMORY_USAGE_BYTES } }, ], }); + + if (cgroupResponse.noHits) { + return await fetchAndTransformMetrics({ + setup, + serviceName, + serviceNodeName, + chartBase, + aggs: { + memoryUsedAvg: { avg: { script: percentSystemMemoryUsedScript } }, + memoryUsedMax: { max: { script: percentSystemMemoryUsedScript } }, + }, + additionalFilters: [ + { exists: { field: METRIC_SYSTEM_FREE_MEMORY } }, + { exists: { field: METRIC_SYSTEM_TOTAL_MEMORY } }, + ], + }); + } + + return cgroupResponse; } diff --git a/x-pack/plugins/apm/server/lib/observability_overview/get_service_count.ts b/x-pack/plugins/apm/server/lib/observability_overview/get_service_count.ts index 8a1f3cb0e0149..a915457bf4a1f 100644 --- a/x-pack/plugins/apm/server/lib/observability_overview/get_service_count.ts +++ b/x-pack/plugins/apm/server/lib/observability_overview/get_service_count.ts @@ -8,18 +8,23 @@ import { ProcessorEvent } from '../../../common/processor_event'; import { rangeFilter } from '../../../common/utils/range_filter'; import { SERVICE_NAME } from '../../../common/elasticsearch_fieldnames'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; +import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; export async function getServiceCount({ setup, + searchAggregatedTransactions, }: { setup: Setup & SetupTimeRange; + searchAggregatedTransactions: boolean; }) { const { apmEventClient, start, end } = setup; const params = { apm: { events: [ - ProcessorEvent.transaction, + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), ProcessorEvent.error, ProcessorEvent.metric, ], diff --git a/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts b/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts index 4eb5ff05b45e3..5531944fc7180 100644 --- a/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts +++ b/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts @@ -11,20 +11,29 @@ import { rangeFilter } from '../../../common/utils/range_filter'; import { Coordinates } from '../../../../observability/typings/common'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; -import { ProcessorEvent } from '../../../common/processor_event'; +import { + getProcessorEventForAggregatedTransactions, + getTransactionDurationFieldForAggregatedTransactions, +} from '../helpers/aggregated_transactions'; export async function getTransactionCoordinates({ setup, bucketSize, + searchAggregatedTransactions, }: { setup: Setup & SetupTimeRange; bucketSize: string; + searchAggregatedTransactions: boolean; }): Promise { const { apmEventClient, start, end } = setup; const { aggregations } = await apmEventClient.search({ apm: { - events: [ProcessorEvent.transaction], + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], }, body: { size: 0, @@ -40,6 +49,15 @@ export async function getTransactionCoordinates({ fixed_interval: bucketSize, min_doc_count: 0, }, + aggs: { + count: { + value_count: { + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), + }, + }, + }, }, }, }, @@ -50,7 +68,7 @@ export async function getTransactionCoordinates({ return ( aggregations?.distribution.buckets.map((bucket) => ({ x: bucket.key, - y: bucket.doc_count / deltaAsMinutes, + y: bucket.count.value / deltaAsMinutes, })) || [] ); } diff --git a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap index 32b4343a7f81b..ceffb4f4d6654 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap @@ -33,7 +33,7 @@ Object { }, "pageViews": Object { "value_count": Object { - "field": "transaction.type", + "field": "transaction.duration.us", }, }, }, diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts b/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts index e0dec183f06d6..b3f9646f64029 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRumOverviewProjection } from '../../projections/rum_overview'; +import { TRANSACTION_DURATION } from '../../../common/elasticsearch_fieldnames'; +import { getRumPageLoadTransactionsProjection } from '../../projections/rum_page_load_transactions'; import { mergeProjection } from '../../projections/util/merge_projection'; import { Setup, @@ -21,18 +22,19 @@ export async function getClientMetrics({ }: { setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const projection = getRumOverviewProjection({ + const projection = getRumPageLoadTransactionsProjection({ setup, }); const params = mergeProjection(projection, { body: { size: 0, - query: { - bool: projection.body.query.bool, - }, aggs: { - pageViews: { value_count: { field: 'transaction.type' } }, + pageViews: { + value_count: { + field: TRANSACTION_DURATION, + }, + }, backEnd: { percentiles: { field: TRANSACTION_TIME_TO_FIRST_BYTE, diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_long_task_metrics.ts b/x-pack/plugins/apm/server/lib/rum_client/get_long_task_metrics.ts index fba1b6a0a634d..1faee52034580 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_long_task_metrics.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_long_task_metrics.ts @@ -6,8 +6,8 @@ import { getRumLongTasksProjection, - getRumOverviewProjection, -} from '../../projections/rum_overview'; + getRumPageLoadTransactionsProjection, +} from '../../projections/rum_page_load_transactions'; import { mergeProjection } from '../../projections/util/merge_projection'; import { Setup, @@ -87,7 +87,7 @@ async function filterPageLoadTransactions( setup: Setup & SetupTimeRange & SetupUIFilters, transactionIds: string[] ) { - const projection = getRumOverviewProjection({ + const projection = getRumPageLoadTransactionsProjection({ setup, }); diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts index 2a0c709ea9235..3d8ab7a72654d 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRumOverviewProjection } from '../../projections/rum_overview'; +import { TRANSACTION_DURATION } from '../../../common/elasticsearch_fieldnames'; +import { getRumPageLoadTransactionsProjection } from '../../projections/rum_page_load_transactions'; import { mergeProjection } from '../../projections/util/merge_projection'; import { Setup, @@ -14,10 +15,27 @@ import { export const MICRO_TO_SEC = 1000000; +const NUMBER_OF_PLD_STEPS = 100; + export function microToSec(val: number) { return Math.round((val / MICRO_TO_SEC + Number.EPSILON) * 100) / 100; } +export const getPLDChartSteps = ({ + maxDuration, + minDuration, +}: { + maxDuration: number; + minDuration: number; +}) => { + const stepValue = (maxDuration - minDuration) / NUMBER_OF_PLD_STEPS; + const stepValues = []; + for (let i = 1; i < NUMBER_OF_PLD_STEPS + 1; i++) { + stepValues.push((stepValue * i + minDuration).toFixed(2)); + } + return stepValues; +}; + export async function getPageLoadDistribution({ setup, minPercentile, @@ -27,26 +45,23 @@ export async function getPageLoadDistribution({ minPercentile?: string; maxPercentile?: string; }) { - const projection = getRumOverviewProjection({ + const projection = getRumPageLoadTransactionsProjection({ setup, }); const params = mergeProjection(projection, { body: { size: 0, - query: { - bool: projection.body.query.bool, - }, aggs: { minDuration: { min: { - field: 'transaction.duration.us', + field: TRANSACTION_DURATION, missing: 0, }, }, durPercentiles: { percentiles: { - field: 'transaction.duration.us', + field: TRANSACTION_DURATION, percents: [50, 75, 90, 95, 99], hdr: { number_of_significant_value_digits: 3, @@ -78,7 +93,11 @@ export async function getPageLoadDistribution({ const maxPerc = maxPercentile ? +maxPercentile * MICRO_TO_SEC : maxPercQuery; - const pageDist = await getPercentilesDistribution(setup, minPerc, maxPerc); + const pageDist = await getPercentilesDistribution({ + setup, + minDuration: minPerc, + maxDuration: maxPerc, + }); Object.entries(durPercentiles?.values ?? {}).forEach(([key, val]) => { if (durPercentiles?.values?.[key]) { @@ -94,31 +113,28 @@ export async function getPageLoadDistribution({ }; } -const getPercentilesDistribution = async ( - setup: Setup & SetupTimeRange & SetupUIFilters, - minDuration: number, - maxDuration: number -) => { - const stepValue = (maxDuration - minDuration) / 100; - const stepValues = []; - for (let i = 1; i < 101; i++) { - stepValues.push((stepValue * i + minDuration).toFixed(2)); - } +const getPercentilesDistribution = async ({ + setup, + minDuration, + maxDuration, +}: { + setup: Setup & SetupTimeRange & SetupUIFilters; + minDuration: number; + maxDuration: number; +}) => { + const stepValues = getPLDChartSteps({ maxDuration, minDuration }); - const projection = getRumOverviewProjection({ + const projection = getRumPageLoadTransactionsProjection({ setup, }); const params = mergeProjection(projection, { body: { size: 0, - query: { - bool: projection.body.query.bool, - }, aggs: { loadDistribution: { percentile_ranks: { - field: 'transaction.duration.us', + field: TRANSACTION_DURATION, values: stepValues, keyed: false, hdr: { diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts index 114137e9fad17..f25062c67f87a 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts @@ -3,8 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import { getRumOverviewProjection } from '../../projections/rum_overview'; +import { getRumPageLoadTransactionsProjection } from '../../projections/rum_page_load_transactions'; import { mergeProjection } from '../../projections/util/merge_projection'; import { Setup, @@ -20,7 +19,7 @@ export async function getPageViewTrends({ setup: Setup & SetupTimeRange & SetupUIFilters; breakdowns?: string; }) { - const projection = getRumOverviewProjection({ + const projection = getRumPageLoadTransactionsProjection({ setup, }); let breakdownItem: BreakdownItem | null = null; @@ -68,17 +67,15 @@ export async function getPageViewTrends({ x: xVal, y: bCount, }; - if (breakdownItem) { - const categoryBuckets = (bucket.breakdown as any).buckets; - categoryBuckets.forEach( - ({ key, doc_count: docCount }: { key: string; doc_count: number }) => { - if (key === 'Other') { - res[key + `(${breakdownItem?.name})`] = docCount; - } else { - res[key] = docCount; - } + if ('breakdown' in bucket) { + const categoryBuckets = bucket.breakdown.buckets; + categoryBuckets.forEach(({ key, doc_count: docCount }) => { + if (key === 'Other') { + res[key + `(${breakdownItem?.name})`] = docCount; + } else { + res[key] = docCount; } - ); + }); } return res; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts b/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts index ffb06e649b9be..1945140e35777 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { getRumPageLoadTransactionsProjection } from '../../projections/rum_page_load_transactions'; import { ProcessorEvent } from '../../../common/processor_event'; -import { getRumOverviewProjection } from '../../projections/rum_overview'; import { mergeProjection } from '../../projections/util/merge_projection'; import { Setup, @@ -19,7 +19,11 @@ import { USER_AGENT_OS, TRANSACTION_DURATION, } from '../../../common/elasticsearch_fieldnames'; -import { MICRO_TO_SEC, microToSec } from './get_page_load_distribution'; +import { + getPLDChartSteps, + MICRO_TO_SEC, + microToSec, +} from './get_page_load_distribution'; export const getBreakdownField = (breakdown: string) => { switch (breakdown) { @@ -35,22 +39,24 @@ export const getBreakdownField = (breakdown: string) => { } }; -export const getPageLoadDistBreakdown = async ( - setup: Setup & SetupTimeRange & SetupUIFilters, - minDuration: number, - maxDuration: number, - breakdown: string -) => { +export const getPageLoadDistBreakdown = async ({ + setup, + minDuration, + maxDuration, + breakdown, +}: { + setup: Setup & SetupTimeRange & SetupUIFilters; + minDuration: number; + maxDuration: number; + breakdown: string; +}) => { // convert secs to micros - const stepValue = - (maxDuration * MICRO_TO_SEC - minDuration * MICRO_TO_SEC) / 50; - const stepValues = []; - - for (let i = 1; i < 51; i++) { - stepValues.push((stepValue * i + minDuration).toFixed(2)); - } + const stepValues = getPLDChartSteps({ + minDuration: minDuration * MICRO_TO_SEC, + maxDuration: maxDuration * MICRO_TO_SEC, + }); - const projection = getRumOverviewProjection({ + const projection = getRumPageLoadTransactionsProjection({ setup, }); diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts b/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts index 9bfa109f00faf..3adad0868ed4b 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import { SERVICE_NAME } from '../../../common/elasticsearch_fieldnames'; import { Setup, SetupTimeRange, SetupUIFilters, } from '../helpers/setup_request'; -import { getRumOverviewProjection } from '../../projections/rum_overview'; +import { getRumPageLoadTransactionsProjection } from '../../projections/rum_page_load_transactions'; import { mergeProjection } from '../../projections/util/merge_projection'; export async function getRumServices({ @@ -17,7 +18,7 @@ export async function getRumServices({ }: { setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const projection = getRumOverviewProjection({ + const projection = getRumPageLoadTransactionsProjection({ setup, }); @@ -30,7 +31,7 @@ export async function getRumServices({ aggs: { services: { terms: { - field: 'service.name', + field: SERVICE_NAME, size: 1000, }, }, diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts b/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts index 3681923b484b0..3493307929f42 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRumOverviewProjection } from '../../projections/rum_overview'; +import { getRumPageLoadTransactionsProjection } from '../../projections/rum_page_load_transactions'; import { mergeProjection } from '../../projections/util/merge_projection'; import { Setup, @@ -22,7 +22,7 @@ export async function getVisitorBreakdown({ }: { setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const projection = getRumOverviewProjection({ + const projection = getRumPageLoadTransactionsProjection({ setup, }); diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts b/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts index 25d1877cbb123..4fcfb53a05887 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRumOverviewProjection } from '../../projections/rum_overview'; +import { getRumPageLoadTransactionsProjection } from '../../projections/rum_page_load_transactions'; import { mergeProjection } from '../../projections/util/merge_projection'; import { Setup, @@ -16,6 +16,7 @@ import { FCP_FIELD, FID_FIELD, LCP_FIELD, + USER_AGENT_NAME, TBT_FIELD, } from '../../../common/elasticsearch_fieldnames'; @@ -24,7 +25,7 @@ export async function getWebCoreVitals({ }: { setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const projection = getRumOverviewProjection({ + const projection = getRumPageLoadTransactionsProjection({ setup, }); @@ -37,7 +38,7 @@ export async function getWebCoreVitals({ ...projection.body.query.bool.filter, { term: { - 'user_agent.name': 'Chrome', + [USER_AGENT_NAME]: 'Chrome', }, }, ], @@ -123,7 +124,7 @@ export async function getWebCoreVitals({ // Divide by 1000 to convert ms into seconds return { - cls: String(cls?.values['50.0'] || 0), + cls: String(cls?.values['50.0']?.toFixed(2) || 0), fid: ((fid?.values['50.0'] || 0) / 1000).toFixed(2), lcp: ((lcp?.values['50.0'] || 0) / 1000).toFixed(2), tbt: ((tbt?.values['50.0'] || 0) / 1000).toFixed(2), diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts index b162c3b61d928..402beb1999240 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts @@ -27,6 +27,7 @@ export interface IEnvOptions { setup: Setup & SetupTimeRange; serviceName?: string; environment?: string; + searchAggregatedTransactions: boolean; logger: Logger; } @@ -77,10 +78,11 @@ async function getConnectionData({ } async function getServicesData(options: IEnvOptions) { - const { setup } = options; + const { setup, searchAggregatedTransactions } = options; const projection = getServicesProjection({ setup: { ...setup, uiFiltersES: [] }, + searchAggregatedTransactions, }); const { filter } = projection.body.query.bool; diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts index 1e26b6f3f58f9..7af1607697ef3 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts @@ -26,6 +26,7 @@ describe('getServiceMapServiceNodeInfo', () => { uiFilters: { environment }, setup, serviceName, + searchAggregatedTransactions: false, }); expect(result).toEqual({ @@ -52,7 +53,12 @@ describe('getServiceMapServiceNodeInfo', () => { apmEventClient: { search: () => Promise.resolve({ - hits: { total: { value: 1 } }, + aggregations: { + count: { value: 1 }, + duration: { value: null }, + avgCpuUsage: { value: null }, + avgMemoryUsage: { value: null }, + }, }), }, indices: {}, @@ -68,6 +74,7 @@ describe('getServiceMapServiceNodeInfo', () => { uiFilters: { environment }, setup, serviceName, + searchAggregatedTransactions: false, }); expect(result).toEqual({ diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts index 330d38739a063..5c183fd9150dd 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts @@ -4,24 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ +import { + TRANSACTION_REQUEST, + TRANSACTION_PAGE_LOAD, +} from '../../../common/transaction_types'; import { UIFilters } from '../../../typings/ui_filters'; import { SERVICE_NAME, - TRANSACTION_DURATION, - TRANSACTION_TYPE, METRIC_SYSTEM_CPU_PERCENT, METRIC_SYSTEM_FREE_MEMORY, METRIC_SYSTEM_TOTAL_MEMORY, + METRIC_CGROUP_MEMORY_USAGE_BYTES, + TRANSACTION_TYPE, } from '../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../common/processor_event'; import { rangeFilter } from '../../../common/utils/range_filter'; import { ESFilter } from '../../../typings/elasticsearch'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; -import { percentMemoryUsedScript } from '../metrics/by_agent/shared/memory'; import { - TRANSACTION_REQUEST, - TRANSACTION_PAGE_LOAD, -} from '../../../common/transaction_types'; + percentCgroupMemoryUsedScript, + percentSystemMemoryUsedScript, +} from '../metrics/by_agent/shared/memory'; +import { + getProcessorEventForAggregatedTransactions, + getTransactionDurationFieldForAggregatedTransactions, + getDocumentTypeFilterForAggregatedTransactions, +} from '../helpers/aggregated_transactions'; import { getErrorRate } from '../transaction_groups/get_error_rate'; import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; @@ -29,11 +37,13 @@ interface Options { setup: Setup & SetupTimeRange; environment?: string; serviceName: string; + searchAggregatedTransactions: boolean; } interface TaskParameters { environment?: string; filter: ESFilter[]; + searchAggregatedTransactions: boolean; minutes: number; serviceName?: string; setup: Setup; @@ -42,6 +52,7 @@ interface TaskParameters { export async function getServiceMapServiceNodeInfo({ serviceName, setup, + searchAggregatedTransactions, uiFilters, }: Options & { serviceName: string; uiFilters: UIFilters }) { const { start, end } = setup; @@ -56,6 +67,7 @@ export async function getServiceMapServiceNodeInfo({ const taskParams = { environment: uiFilters.environment, filter, + searchAggregatedTransactions, minutes, serviceName, setup, @@ -104,6 +116,7 @@ async function getTransactionStats({ setup, filter, minutes, + searchAggregatedTransactions, }: TaskParameters): Promise<{ avgTransactionDuration: number | null; avgRequestsPerMinute: number | null; @@ -112,7 +125,11 @@ async function getTransactionStats({ const params = { apm: { - events: [ProcessorEvent.transaction], + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], }, body: { size: 0, @@ -120,6 +137,9 @@ async function getTransactionStats({ bool: { filter: [ ...filter, + ...getDocumentTypeFilterForAggregatedTransactions( + searchAggregatedTransactions + ), { terms: { [TRANSACTION_TYPE]: [ @@ -132,15 +152,31 @@ async function getTransactionStats({ }, }, track_total_hits: true, - aggs: { duration: { avg: { field: TRANSACTION_DURATION } } }, + aggs: { + duration: { + avg: { + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), + }, + }, + count: { + value_count: { + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), + }, + }, + }, }, }; const response = await apmEventClient.search(params); - const docCount = response.hits.total.value; + + const totalRequests = response.aggregations?.count.value ?? 0; return { avgTransactionDuration: response.aggregations?.duration.value ?? null, - avgRequestsPerMinute: docCount > 0 ? docCount / minutes : null, + avgRequestsPerMinute: totalRequests > 0 ? totalRequests / minutes : null, }; } @@ -173,25 +209,50 @@ async function getMemoryStats({ filter, }: TaskParameters): Promise<{ avgMemoryUsage: number | null }> { const { apmEventClient } = setup; - const response = await apmEventClient.search({ - apm: { - events: [ProcessorEvent.metric], - }, - body: { - query: { - bool: { - filter: [ - ...filter, - { exists: { field: METRIC_SYSTEM_FREE_MEMORY } }, - { exists: { field: METRIC_SYSTEM_TOTAL_MEMORY } }, - ], + + const getAvgMemoryUsage = async ({ + additionalFilters, + script, + }: { + additionalFilters: ESFilter[]; + script: typeof percentCgroupMemoryUsedScript; + }) => { + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.metric], + }, + body: { + size: 0, + query: { + bool: { + filter: [...filter, ...additionalFilters], + }, + }, + aggs: { + avgMemoryUsage: { avg: { script } }, }, }, - aggs: { avgMemoryUsage: { avg: { script: percentMemoryUsedScript } } }, - }, - }); + }); - return { - avgMemoryUsage: response.aggregations?.avgMemoryUsage.value ?? null, + return response.aggregations?.avgMemoryUsage.value ?? null; }; + + let avgMemoryUsage = await getAvgMemoryUsage({ + additionalFilters: [ + { exists: { field: METRIC_CGROUP_MEMORY_USAGE_BYTES } }, + ], + script: percentCgroupMemoryUsedScript, + }); + + if (!avgMemoryUsage) { + avgMemoryUsage = await getAvgMemoryUsage({ + additionalFilters: [ + { exists: { field: METRIC_SYSTEM_FREE_MEMORY } }, + { exists: { field: METRIC_SYSTEM_TOTAL_MEMORY } }, + ], + script: percentSystemMemoryUsedScript, + }); + } + + return { avgMemoryUsage }; } diff --git a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap index c5e072e073992..a65536df37bc8 100644 --- a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap @@ -156,9 +156,9 @@ Array [ Object { "apm": Object { "events": Array [ + "transaction", "metric", "error", - "transaction", ], }, "body": Object { @@ -213,7 +213,19 @@ Array [ "aggs": Object { "services": Object { "aggs": Object { + "count": Object { + "value_count": Object { + "field": "transaction.duration.us", + }, + }, "timeseries": Object { + "aggs": Object { + "count": Object { + "value_count": Object { + "field": "transaction.duration.us", + }, + }, + }, "date_histogram": Object { "extended_bounds": Object { "max": 1528977600000, @@ -328,8 +340,8 @@ Array [ Object { "apm": Object { "events": Array [ - "metric", "transaction", + "metric", "error", ], }, diff --git a/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts b/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts index ad3f47d443b87..b80e86d53f292 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ import { isNumber } from 'lodash'; -import { ProcessorEvent } from '../../../../common/processor_event'; import { Annotation, AnnotationType } from '../../../../common/annotations'; import { SetupTimeRange, Setup } from '../../helpers/setup_request'; import { ESFilter } from '../../../../typings/elasticsearch'; @@ -14,20 +13,29 @@ import { SERVICE_VERSION, } from '../../../../common/elasticsearch_fieldnames'; import { getEnvironmentUiFilterES } from '../../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { + getDocumentTypeFilterForAggregatedTransactions, + getProcessorEventForAggregatedTransactions, +} from '../../helpers/aggregated_transactions'; export async function getDerivedServiceAnnotations({ setup, serviceName, environment, + searchAggregatedTransactions, }: { serviceName: string; environment?: string; setup: Setup & SetupTimeRange; + searchAggregatedTransactions: boolean; }) { const { start, end, apmEventClient } = setup; const filter: ESFilter[] = [ { term: { [SERVICE_NAME]: serviceName } }, + ...getDocumentTypeFilterForAggregatedTransactions( + searchAggregatedTransactions + ), ...getEnvironmentUiFilterES(environment), ]; @@ -35,7 +43,11 @@ export async function getDerivedServiceAnnotations({ ( await apmEventClient.search({ apm: { - events: [ProcessorEvent.transaction], + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], }, body: { size: 0, @@ -62,7 +74,11 @@ export async function getDerivedServiceAnnotations({ versions.map(async (version) => { const response = await apmEventClient.search({ apm: { - events: [ProcessorEvent.transaction], + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], }, body: { size: 0, diff --git a/x-pack/plugins/apm/server/lib/services/annotations/index.test.ts b/x-pack/plugins/apm/server/lib/services/annotations/index.test.ts index 04e6ef322e9ec..e1a3ee1c9380d 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/index.test.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/index.test.ts @@ -28,6 +28,7 @@ describe('getServiceAnnotations', () => { setup, serviceName: 'foo', environment: 'bar', + searchAggregatedTransactions: false, }), { mockResponse: () => noVersions, @@ -46,6 +47,7 @@ describe('getServiceAnnotations', () => { setup, serviceName: 'foo', environment: 'bar', + searchAggregatedTransactions: false, }), { mockResponse: () => oneVersion, @@ -69,6 +71,7 @@ describe('getServiceAnnotations', () => { setup, serviceName: 'foo', environment: 'bar', + searchAggregatedTransactions: false, }), { mockResponse: () => responses.shift(), diff --git a/x-pack/plugins/apm/server/lib/services/annotations/index.ts b/x-pack/plugins/apm/server/lib/services/annotations/index.ts index a5f8e595ccf54..9516ed3777297 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/index.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/index.ts @@ -11,6 +11,7 @@ import { getStoredAnnotations } from './get_stored_annotations'; export async function getServiceAnnotations({ setup, + searchAggregatedTransactions, serviceName, environment, annotationsClient, @@ -20,6 +21,7 @@ export async function getServiceAnnotations({ serviceName: string; environment?: string; setup: Setup & SetupTimeRange; + searchAggregatedTransactions: boolean; annotationsClient?: ScopedAnnotationsClient; apiCaller: LegacyAPICaller; logger: Logger; @@ -30,6 +32,7 @@ export async function getServiceAnnotations({ setup, serviceName, environment, + searchAggregatedTransactions, }); const storedAnnotations = annotationsClient diff --git a/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts b/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts index a95c27df0e502..17c27108f339d 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts @@ -10,11 +10,17 @@ import { } from '../../../common/elasticsearch_fieldnames'; import { rangeFilter } from '../../../common/utils/range_filter'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; +import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; -export async function getServiceAgentName( - serviceName: string, - setup: Setup & SetupTimeRange -) { +export async function getServiceAgentName({ + serviceName, + setup, + searchAggregatedTransactions, +}: { + serviceName: string; + setup: Setup & SetupTimeRange; + searchAggregatedTransactions: boolean; +}) { const { start, end, apmEventClient } = setup; const params = { @@ -22,7 +28,9 @@ export async function getServiceAgentName( apm: { events: [ ProcessorEvent.error, - ProcessorEvent.transaction, + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), ProcessorEvent.metric, ], }, diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts index 6c6e03ab0b46f..50b01484e9b44 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts @@ -3,29 +3,44 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { ProcessorEvent } from '../../../common/processor_event'; import { SERVICE_NAME, TRANSACTION_TYPE, } from '../../../common/elasticsearch_fieldnames'; import { rangeFilter } from '../../../common/utils/range_filter'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; +import { + getDocumentTypeFilterForAggregatedTransactions, + getProcessorEventForAggregatedTransactions, +} from '../helpers/aggregated_transactions'; -export async function getServiceTransactionTypes( - serviceName: string, - setup: Setup & SetupTimeRange -) { +export async function getServiceTransactionTypes({ + setup, + serviceName, + searchAggregatedTransactions, +}: { + serviceName: string; + setup: Setup & SetupTimeRange; + searchAggregatedTransactions: boolean; +}) { const { start, end, apmEventClient } = setup; const params = { apm: { - events: [ProcessorEvent.transaction], + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], }, body: { size: 0, query: { bool: { filter: [ + ...getDocumentTypeFilterForAggregatedTransactions( + searchAggregatedTransactions + ), { term: { [SERVICE_NAME]: serviceName } }, { range: rangeFilter(start, end) }, ], diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts index 50a968467fb4b..c09be7aacc784 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts @@ -26,14 +26,20 @@ export type ServicesItemsProjection = ReturnType; export async function getServicesItems({ setup, + searchAggregatedTransactions, mlAnomaliesEnvironment, }: { setup: ServicesItemsSetup; + searchAggregatedTransactions: boolean; mlAnomaliesEnvironment?: string; }) { const params = { - projection: getServicesProjection({ setup }), + projection: getServicesProjection({ + setup, + searchAggregatedTransactions, + }), setup, + searchAggregatedTransactions, }; const [ diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts index ab6b61ca21746..e7e18cbff1c15 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts @@ -8,7 +8,6 @@ import { EventOutcome } from '../../../../common/event_outcome'; import { getSeverity } from '../../../../common/anomaly_detection'; import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; import { - TRANSACTION_DURATION, AGENT_NAME, SERVICE_ENVIRONMENT, EVENT_OUTCOME, @@ -19,6 +18,11 @@ import { ServicesItemsSetup, ServicesItemsProjection, } from './get_services_items'; +import { + getDocumentTypeFilterForAggregatedTransactions, + getProcessorEventForAggregatedTransactions, + getTransactionDurationFieldForAggregatedTransactions, +} from '../../helpers/aggregated_transactions'; import { getBucketSize } from '../../helpers/get_bucket_size'; import { getMLJobIds, @@ -43,21 +47,37 @@ const getDeltaAsMinutes = (setup: ServicesItemsSetup) => interface AggregationParams { setup: ServicesItemsSetup; projection: ServicesItemsProjection; + searchAggregatedTransactions: boolean; } export const getTransactionDurationAverages = async ({ setup, projection, + searchAggregatedTransactions, }: AggregationParams) => { const { apmEventClient, start, end } = setup; const response = await apmEventClient.search( mergeProjection(projection, { apm: { - events: [ProcessorEvent.transaction], + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], }, body: { size: 0, + query: { + bool: { + filter: [ + ...projection.body.query.bool.filter, + ...getDocumentTypeFilterForAggregatedTransactions( + searchAggregatedTransactions + ), + ], + }, + }, aggs: { services: { terms: { @@ -67,7 +87,9 @@ export const getTransactionDurationAverages = async ({ aggs: { average: { avg: { - field: TRANSACTION_DURATION, + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), }, }, timeseries: { @@ -75,7 +97,9 @@ export const getTransactionDurationAverages = async ({ aggs: { average: { avg: { - field: TRANSACTION_DURATION, + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), }, }, }, @@ -112,13 +136,6 @@ export const getAgentNames = async ({ const { apmEventClient } = setup; const response = await apmEventClient.search( mergeProjection(projection, { - apm: { - events: [ - ProcessorEvent.metric, - ProcessorEvent.error, - ProcessorEvent.transaction, - ], - }, body: { size: 0, aggs: { @@ -157,15 +174,30 @@ export const getAgentNames = async ({ export const getTransactionRates = async ({ setup, projection, + searchAggregatedTransactions, }: AggregationParams) => { const { apmEventClient, start, end } = setup; const response = await apmEventClient.search( mergeProjection(projection, { apm: { - events: [ProcessorEvent.transaction], + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], }, body: { size: 0, + query: { + bool: { + filter: [ + ...projection.body.query.bool.filter, + ...getDocumentTypeFilterForAggregatedTransactions( + searchAggregatedTransactions + ), + ], + }, + }, aggs: { services: { terms: { @@ -173,8 +205,24 @@ export const getTransactionRates = async ({ size: MAX_NUMBER_OF_SERVICES, }, aggs: { + count: { + value_count: { + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), + }, + }, timeseries: { date_histogram: getDateHistogramOpts(start, end), + aggs: { + count: { + value_count: { + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), + }, + }, + }, }, }, }, @@ -192,14 +240,14 @@ export const getTransactionRates = async ({ const deltaAsMinutes = getDeltaAsMinutes(setup); return aggregations.services.buckets.map((serviceBucket) => { - const transactionsPerMinute = serviceBucket.doc_count / deltaAsMinutes; + const transactionsPerMinute = serviceBucket.count.value / deltaAsMinutes; return { serviceName: serviceBucket.key as string, transactionsPerMinute: { value: transactionsPerMinute, timeseries: serviceBucket.timeseries.buckets.map((dateBucket) => ({ x: dateBucket.key, - y: dateBucket.doc_count / deltaAsMinutes, + y: dateBucket.count.value / deltaAsMinutes, })), }, }; @@ -305,13 +353,6 @@ export const getEnvironments = async ({ const { apmEventClient } = setup; const response = await apmEventClient.search( mergeProjection(projection, { - apm: { - events: [ - ProcessorEvent.metric, - ProcessorEvent.transaction, - ProcessorEvent.error, - ], - }, body: { size: 0, aggs: { diff --git a/x-pack/plugins/apm/server/lib/services/get_services/index.ts b/x-pack/plugins/apm/server/lib/services/get_services/index.ts index 28b4c64a4af47..351457b2a815e 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/index.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/index.ts @@ -19,13 +19,19 @@ export type ServiceListAPIResponse = PromiseReturnType; export async function getServices({ setup, + searchAggregatedTransactions, mlAnomaliesEnvironment, }: { setup: Setup & SetupTimeRange & SetupUIFilters; + searchAggregatedTransactions: boolean; mlAnomaliesEnvironment?: string; }) { const [items, hasLegacyData] = await Promise.all([ - getServicesItems({ setup, mlAnomaliesEnvironment }), + getServicesItems({ + setup, + searchAggregatedTransactions, + mlAnomaliesEnvironment, + }), getLegacyDataStatus(setup), ]); diff --git a/x-pack/plugins/apm/server/lib/services/queries.test.ts b/x-pack/plugins/apm/server/lib/services/queries.test.ts index 9b0dd7a03ca5b..11adbe894f779 100644 --- a/x-pack/plugins/apm/server/lib/services/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/services/queries.test.ts @@ -23,7 +23,11 @@ describe('services queries', () => { it('fetches the service agent name', async () => { mock = await inspectSearchParams((setup) => - getServiceAgentName('foo', setup) + getServiceAgentName({ + serviceName: 'foo', + setup, + searchAggregatedTransactions: false, + }) ); expect(mock.params).toMatchSnapshot(); @@ -31,14 +35,20 @@ describe('services queries', () => { it('fetches the service transaction types', async () => { mock = await inspectSearchParams((setup) => - getServiceTransactionTypes('foo', setup) + getServiceTransactionTypes({ + serviceName: 'foo', + setup, + searchAggregatedTransactions: false, + }) ); expect(mock.params).toMatchSnapshot(); }); it('fetches the service items', async () => { - mock = await inspectSearchParams((setup) => getServicesItems({ setup })); + mock = await inspectSearchParams((setup) => + getServicesItems({ setup, searchAggregatedTransactions: false }) + ); const allParams = mock.spy.mock.calls.map((call) => call[0]); diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap index c01e5c87eeea2..8db97a4929eb0 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap @@ -84,6 +84,41 @@ Object { } `; +exports[`agent configuration queries getAllEnvironments fetches all environments 1`] = ` +Object { + "apm": Object { + "events": Array [ + "transaction", + "error", + "metric", + ], + }, + "body": Object { + "aggs": Object { + "environments": Object { + "terms": Object { + "field": "service.environment", + "missing": undefined, + "size": 100, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "foo", + }, + }, + ], + }, + }, + "size": 0, + }, +} +`; + exports[`agent configuration queries getExistingEnvironmentsForService fetches unavailable environments 1`] = ` Object { "body": Object { diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/index.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/index.ts index 630249052be0b..521fa9351cbfe 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/index.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/index.ts @@ -17,12 +17,14 @@ export type AgentConfigurationEnvironmentsAPIResponse = PromiseReturnType< export async function getEnvironments({ serviceName, setup, + searchAggregatedTransactions, }: { serviceName: string | undefined; setup: Setup; + searchAggregatedTransactions: boolean; }) { const [allEnvironments, existingEnvironments] = await Promise.all([ - getAllEnvironments({ serviceName, setup }), + getAllEnvironments({ serviceName, setup, searchAggregatedTransactions }), getExistingEnvironmentsForService({ serviceName, setup }), ]); diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts index 91bdfeef003f1..3ca583c30c29e 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts @@ -9,17 +9,26 @@ import { Setup } from '../../helpers/setup_request'; import { PromiseReturnType } from '../../../../../observability/typings/common'; import { SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames'; import { ALL_OPTION_VALUE } from '../../../../common/agent_configuration/all_option'; +import { getProcessorEventForAggregatedTransactions } from '../../helpers/aggregated_transactions'; export type AgentConfigurationServicesAPIResponse = PromiseReturnType< typeof getServiceNames >; -export async function getServiceNames({ setup }: { setup: Setup }) { +export async function getServiceNames({ + setup, + searchAggregatedTransactions, +}: { + setup: Setup; + searchAggregatedTransactions: boolean; +}) { const { apmEventClient } = setup; const params = { apm: { events: [ - ProcessorEvent.transaction, + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), ProcessorEvent.error, ProcessorEvent.metric, ], diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts index f035aa937c364..1efb50580d531 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts @@ -13,6 +13,7 @@ import { inspectSearchParams, } from '../../../utils/test_helpers'; import { findExactConfiguration } from './find_exact_configuration'; +import { getAllEnvironments } from '../../environments/get_all_environments'; describe('agent configuration queries', () => { let mock: SearchParamsMock; @@ -21,6 +22,20 @@ describe('agent configuration queries', () => { mock.teardown(); }); + describe('getAllEnvironments', () => { + it('fetches all environments', async () => { + mock = await inspectSearchParams((setup) => + getAllEnvironments({ + serviceName: 'foo', + setup, + searchAggregatedTransactions: false, + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); + }); + describe('getExistingEnvironmentsForService', () => { it('fetches unavailable environments', async () => { mock = await inspectSearchParams((setup) => @@ -39,6 +54,7 @@ describe('agent configuration queries', () => { mock = await inspectSearchParams((setup) => getServiceNames({ setup, + searchAggregatedTransactions: false, }) ); diff --git a/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts b/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts index 17f9743ae9f00..9aebadba23b2a 100644 --- a/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts +++ b/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts @@ -88,7 +88,7 @@ export async function getTraceItems( // explicit intermediary types to avoid TS "excessively deep" error PromiseValueType, PromiseValueType - ] = await Promise.all([errorResponsePromise, traceResponsePromise]); + ] = (await Promise.all([errorResponsePromise, traceResponsePromise])) as any; const exceedsMax = traceResponse.hits.total.value > maxTraceItems; diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap index 0ea7bcf7ce8ab..bd6cefa793467 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap @@ -12,8 +12,16 @@ Array [ "aggs": Object { "transaction_groups": Object { "aggs": Object { - "sample": Object { + "count": Object { + "value_count": Object { + "field": "transaction.duration.us", + }, + }, + "transaction_type": Object { "top_hits": Object { + "_source": Array [ + "transaction.type", + ], "size": 1, }, }, @@ -64,27 +72,10 @@ Array [ }, }, ], - "should": Array [ - Object { - "term": Object { - "transaction.sampled": true, - }, - }, - ], }, }, - "sort": Array [ - Object { - "_score": "desc", - }, - Object { - "@timestamp": Object { - "order": "desc", - }, - }, - ], + "size": 0, }, - "size": 0, }, Object { "apm": Object { @@ -150,8 +141,8 @@ Array [ ], }, }, + "size": 0, }, - "size": 0, }, Object { "apm": Object { @@ -217,8 +208,8 @@ Array [ ], }, }, + "size": 0, }, - "size": 0, }, ] `; @@ -235,8 +226,16 @@ Array [ "aggs": Object { "transaction_groups": Object { "aggs": Object { - "sample": Object { + "count": Object { + "value_count": Object { + "field": "transaction.duration.us", + }, + }, + "transaction_type": Object { "top_hits": Object { + "_source": Array [ + "transaction.type", + ], "size": 1, }, }, @@ -275,27 +274,10 @@ Array [ }, }, ], - "should": Array [ - Object { - "term": Object { - "transaction.sampled": true, - }, - }, - ], }, }, - "sort": Array [ - Object { - "_score": "desc", - }, - Object { - "@timestamp": Object { - "order": "desc", - }, - }, - ], + "size": 0, }, - "size": 0, }, Object { "apm": Object { @@ -349,8 +331,8 @@ Array [ ], }, }, + "size": 0, }, - "size": 0, }, Object { "apm": Object { @@ -404,8 +386,8 @@ Array [ ], }, }, + "size": 0, }, - "size": 0, }, Object { "apm": Object { @@ -465,8 +447,8 @@ Array [ ], }, }, + "size": 0, }, - "size": 0, }, ] `; diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts index 5c1e1839d9c53..5d581149db667 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts @@ -15,17 +15,16 @@ import { getTransactionGroupsProjection } from '../../projections/transaction_gr import { mergeProjection } from '../../projections/util/merge_projection'; import { PromiseReturnType } from '../../../../observability/typings/common'; import { AggregationOptionsByType } from '../../../typings/elasticsearch/aggregations'; -import { Transaction } from '../../../typings/es_schemas/ui/transaction'; import { Setup, SetupTimeRange, SetupUIFilters, } from '../helpers/setup_request'; import { - getSamples, getAverages, getSums, getPercentiles, + getCounts, } from './get_transaction_group_stats'; interface TopTransactionOptions { @@ -33,11 +32,13 @@ interface TopTransactionOptions { serviceName: string; transactionType: string; transactionName?: string; + searchAggregatedTransactions: boolean; } interface TopTraceOptions { type: 'top_traces'; transactionName?: string; + searchAggregatedTransactions: boolean; } export type Options = TopTransactionOptions | TopTraceOptions; @@ -62,11 +63,11 @@ function getItemsWithRelativeImpact( setup: TransactionGroupSetup, items: Array<{ sum?: number | null; - key: string | Record; + key: string | Record<'service.name' | 'transaction.name', string>; avg?: number | null; count?: number | null; + transactionType?: string; p95?: number | null; - sample?: Transaction; }> ) { const values = items @@ -79,21 +80,19 @@ function getItemsWithRelativeImpact( const duration = moment.duration(setup.end - setup.start); const minutes = duration.asMinutes(); - const itemsWithRelativeImpact: TransactionGroup[] = items - .map((item) => { - return { - key: item.key, - averageResponseTime: item.avg, - transactionsPerMinute: (item.count ?? 0) / minutes, - impact: - item.sum !== null && item.sum !== undefined - ? ((item.sum - min) / (max - min)) * 100 || 0 - : 0, - p95: item.p95, - sample: item.sample!, - }; - }) - .filter((item) => item.sample); + const itemsWithRelativeImpact = items.map((item) => { + return { + key: item.key, + averageResponseTime: item.avg, + transactionsPerMinute: (item.count ?? 0) / minutes, + transactionType: item.transactionType || '', + impact: + item.sum !== null && item.sum !== undefined + ? ((item.sum - min) / (max - min)) * 100 || 0 + : 0, + p95: item.p95, + }; + }); return itemsWithRelativeImpact; } @@ -119,8 +118,8 @@ export async function transactionGroupsFetcher( const size = isTopTraces ? 10000 : expectedBucketSize + 1; const request = mergeProjection(projection, { - size: 0, body: { + size: 0, aggs: { transaction_groups: { ...(isTopTraces @@ -151,18 +150,19 @@ export async function transactionGroupsFetcher( const params = { request, setup, + searchAggregatedTransactions: options.searchAggregatedTransactions, }; - const [samples, averages, sums, percentiles] = await Promise.all([ - getSamples(params), + const [counts, averages, sums, percentiles] = await Promise.all([ + getCounts(params), getAverages(params), getSums(params), !isTopTraces ? getPercentiles(params) : Promise.resolve(undefined), ]); const stats = [ - ...samples, ...averages, + ...counts, ...sums, ...(percentiles ? percentiles : []), ]; @@ -171,11 +171,35 @@ export async function transactionGroupsFetcher( const itemsWithRelativeImpact = getItemsWithRelativeImpact(setup, items); + const defaultServiceName = + options.type === 'top_transactions' ? options.serviceName : undefined; + + const itemsWithKeys: TransactionGroup[] = itemsWithRelativeImpact.map( + (item) => { + let transactionName: string; + let serviceName: string; + + if (typeof item.key === 'string') { + transactionName = item.key; + serviceName = defaultServiceName!; + } else { + transactionName = item.key[TRANSACTION_NAME]; + serviceName = item.key[SERVICE_NAME]; + } + + return { + ...item, + transactionName, + serviceName, + }; + } + ); + return { items: take( // sort by impact by default so most impactful services are not cut off - sortBy(itemsWithRelativeImpact, 'impact').reverse(), - expectedBucketSize + sortBy(itemsWithKeys, 'impact').reverse(), + bucketSize ), // The aggregation is considered accurate if the configured bucket size is larger or equal to the number of buckets returned // the actual number of buckets retrieved are `bucketsize + 1` to detect whether it's above the limit @@ -185,12 +209,12 @@ export async function transactionGroupsFetcher( } export interface TransactionGroup { - name?: string; - key?: Record | string; + key: string | Record<'service.name' | 'transaction.name', string>; + serviceName: string; + transactionName: string; + transactionType: string; averageResponseTime: number | null | undefined; transactionsPerMinute: number; p95: number | null | undefined; impact: number; - impactRelative?: number; - sample: Transaction; } diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts index 1e08b04416e17..82595317342f1 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts @@ -87,12 +87,9 @@ export async function getErrorRate({ doc_count: totalTransactions, erroneous_transactions: erroneousTransactions, }) => { - const errornousTransactionsCount = - // @ts-expect-error - erroneousTransactions.doc_count; return { x: key, - y: errornousTransactionsCount / totalTransactions, + y: erroneousTransactions.doc_count / totalTransactions, }; } ) || []; diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts index 7d45f39e08a83..2550bd70c527d 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts @@ -4,20 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ import { merge } from 'lodash'; +import { TRANSACTION_TYPE } from '../../../common/elasticsearch_fieldnames'; import { arrayUnionToCallable } from '../../../common/utils/array_union_to_callable'; -import { - TRANSACTION_SAMPLED, - TRANSACTION_DURATION, -} from '../../../common/elasticsearch_fieldnames'; -import { - AggregationInputMap, - SortOptions, -} from '../../../typings/elasticsearch/aggregations'; +import { AggregationInputMap } from '../../../typings/elasticsearch/aggregations'; import { TransactionGroupRequestBase, TransactionGroupSetup } from './fetcher'; +import { getTransactionDurationFieldForAggregatedTransactions } from '../helpers/aggregated_transactions'; interface MetricParams { request: TransactionGroupRequestBase; setup: TransactionGroupSetup; + searchAggregatedTransactions: boolean; } type BucketKey = string | Record; @@ -37,51 +33,50 @@ function mergeRequestWithAggs< }); } -export async function getSamples({ request, setup }: MetricParams) { +export async function getAverages({ + request, + setup, + searchAggregatedTransactions, +}: MetricParams) { const params = mergeRequestWithAggs(request, { - sample: { - top_hits: { - size: 1, + avg: { + avg: { + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), }, }, }); - const sort: SortOptions = [ - { _score: 'desc' as const }, // sort by _score to ensure that buckets with sampled:true ends up on top - { '@timestamp': { order: 'desc' as const } }, - ]; - - const response = await setup.apmEventClient.search({ - ...params, - body: { - ...params.body, - query: { - ...params.body.query, - bool: { - ...params.body.query.bool, - should: [{ term: { [TRANSACTION_SAMPLED]: true } }], - }, - }, - sort, - }, - }); + const response = await setup.apmEventClient.search(params); return arrayUnionToCallable( response.aggregations?.transaction_groups.buckets ?? [] ).map((bucket) => { return { key: bucket.key as BucketKey, - count: bucket.doc_count, - sample: bucket.sample.hits.hits[0]._source, + avg: bucket.avg.value, }; }); } -export async function getAverages({ request, setup }: MetricParams) { +export async function getCounts({ + request, + setup, + searchAggregatedTransactions, +}: MetricParams) { const params = mergeRequestWithAggs(request, { - avg: { - avg: { - field: TRANSACTION_DURATION, + count: { + value_count: { + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), + }, + }, + transaction_type: { + top_hits: { + size: 1, + _source: [TRANSACTION_TYPE], }, }, }); @@ -91,18 +86,29 @@ export async function getAverages({ request, setup }: MetricParams) { return arrayUnionToCallable( response.aggregations?.transaction_groups.buckets ?? [] ).map((bucket) => { + // type is Transaction | APMBaseDoc because it could be a metric document + const source = (bucket.transaction_type.hits.hits[0] + ._source as unknown) as { transaction: { type: string } }; + return { key: bucket.key as BucketKey, - avg: bucket.avg.value, + count: bucket.count.value, + transactionType: source.transaction.type, }; }); } -export async function getSums({ request, setup }: MetricParams) { +export async function getSums({ + request, + setup, + searchAggregatedTransactions, +}: MetricParams) { const params = mergeRequestWithAggs(request, { sum: { sum: { - field: TRANSACTION_DURATION, + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), }, }, }); @@ -119,11 +125,17 @@ export async function getSums({ request, setup }: MetricParams) { }); } -export async function getPercentiles({ request, setup }: MetricParams) { +export async function getPercentiles({ + request, + setup, + searchAggregatedTransactions, +}: MetricParams) { const params = mergeRequestWithAggs(request, { p95: { percentiles: { - field: TRANSACTION_DURATION, + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), hdr: { number_of_significant_value_digits: 2 }, percents: [95], }, diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_sample_for_group.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_sample_for_group.ts new file mode 100644 index 0000000000000..6c9b23b3dc079 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_sample_for_group.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { maybe } from '../../../common/utils/maybe'; +import { + SERVICE_NAME, + TRANSACTION_NAME, + TRANSACTION_SAMPLED, +} from '../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { rangeFilter } from '../../../common/utils/range_filter'; +import { + Setup, + SetupTimeRange, + SetupUIFilters, +} from '../helpers/setup_request'; + +export async function getTransactionSampleForGroup({ + serviceName, + transactionName, + setup, +}: { + serviceName: string; + transactionName: string; + setup: Setup & SetupTimeRange & SetupUIFilters; +}) { + const { apmEventClient, start, end, uiFiltersES } = setup; + + const filter = [ + { + range: rangeFilter(start, end), + }, + { + term: { + [SERVICE_NAME]: serviceName, + }, + }, + { + term: { + [TRANSACTION_NAME]: transactionName, + }, + }, + ...uiFiltersES, + ]; + + const getSampledTransaction = async () => { + const response = await apmEventClient.search({ + terminateAfter: 1, + apm: { + events: [ProcessorEvent.transaction], + }, + body: { + size: 1, + query: { + bool: { + filter: [...filter, { term: { [TRANSACTION_SAMPLED]: true } }], + }, + }, + }, + }); + + return maybe(response.hits.hits[0]?._source); + }; + + const getUnsampledTransaction = async () => { + const response = await apmEventClient.search({ + terminateAfter: 1, + apm: { + events: [ProcessorEvent.transaction], + }, + body: { + size: 1, + query: { + bool: { + filter: [...filter, { term: { [TRANSACTION_SAMPLED]: false } }], + }, + }, + }, + }); + + return maybe(response.hits.hits[0]?._source); + }; + + const [sampledTransaction, unsampledTransaction] = await Promise.all([ + getSampledTransaction(), + getUnsampledTransaction(), + ]); + + return sampledTransaction || unsampledTransaction; +} diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/queries.test.ts b/x-pack/plugins/apm/server/lib/transaction_groups/queries.test.ts index 5f36189224534..225e0c33dfabb 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/queries.test.ts @@ -25,6 +25,7 @@ describe('transaction group queries', () => { type: 'top_transactions', serviceName: 'foo', transactionType: 'bar', + searchAggregatedTransactions: false, }, setup, bucketSize @@ -42,6 +43,7 @@ describe('transaction group queries', () => { transactionGroupsFetcher( { type: 'top_traces', + searchAggregatedTransactions: false, }, setup, bucketSize diff --git a/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap index 9bc4b1d69d9ac..e532265de24ec 100644 --- a/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap @@ -358,6 +358,13 @@ Object { "transaction_results": Object { "aggs": Object { "timeseries": Object { + "aggs": Object { + "count": Object { + "value_count": Object { + "field": "transaction.duration.us", + }, + }, + }, "date_histogram": Object { "extended_bounds": Object { "max": 1528977600000, @@ -452,6 +459,13 @@ Object { "transaction_results": Object { "aggs": Object { "timeseries": Object { + "aggs": Object { + "count": Object { + "value_count": Object { + "field": "transaction.duration.us", + }, + }, + }, "date_histogram": Object { "extended_bounds": Object { "max": 1528977600000, @@ -551,6 +565,13 @@ Object { "transaction_results": Object { "aggs": Object { "timeseries": Object { + "aggs": Object { + "count": Object { + "value_count": Object { + "field": "transaction.duration.us", + }, + }, + }, "date_histogram": Object { "extended_bounds": Object { "max": 1528977600000, diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/__fixtures__/responses.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/__fixtures__/responses.ts index 520add0566fa1..44878aa6c1f2e 100644 --- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/__fixtures__/responses.ts +++ b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/__fixtures__/responses.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; +import { APMBaseDoc } from '../../../../../typings/es_schemas/raw/apm_base_doc'; import { ESSearchResponse, ESSearchRequest, @@ -68,7 +70,7 @@ export const response = ({ }, }, } as unknown) as ESSearchResponse< - unknown, + APMBaseDoc | Transaction, ESSearchRequest, { restTotalHitsAsInt: false } >; diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.test.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.test.ts index 278819ea20a83..aec124e4f4623 100644 --- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.test.ts @@ -20,7 +20,11 @@ describe('fetcher', () => { uiFiltersES: [], } as unknown) as Setup & SetupTimeRange & SetupUIFilters; - await fetcher({ serviceName: 'testServiceName', setup }); + await fetcher({ + serviceName: 'testServiceName', + setup, + searchAggregatedTransactions: false, + }); expect(search).toHaveBeenCalled(); }); diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts index 51118278fb824..d40fcaaa02f60 100644 --- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts @@ -10,20 +10,27 @@ import { SERVICE_NAME, TRANSACTION_TYPE, USER_AGENT_NAME, - TRANSACTION_DURATION, TRANSACTION_NAME, } from '../../../../common/elasticsearch_fieldnames'; import { rangeFilter } from '../../../../common/utils/range_filter'; import { getBucketSize } from '../../helpers/get_bucket_size'; import { Options } from '.'; import { TRANSACTION_PAGE_LOAD } from '../../../../common/transaction_types'; -import { ProcessorEvent } from '../../../../common/processor_event'; +import { + getDocumentTypeFilterForAggregatedTransactions, + getTransactionDurationFieldForAggregatedTransactions, + getProcessorEventForAggregatedTransactions, +} from '../../helpers/aggregated_transactions'; export type ESResponse = PromiseReturnType; export function fetcher(options: Options) { const { end, apmEventClient, start, uiFiltersES } = options.setup; - const { serviceName, transactionName } = options; + const { + serviceName, + searchAggregatedTransactions, + transactionName, + } = options; const { intervalString } = getBucketSize(start, end); const transactionNameFilter = transactionName @@ -34,13 +41,20 @@ export function fetcher(options: Options) { { term: { [SERVICE_NAME]: serviceName } }, { term: { [TRANSACTION_TYPE]: TRANSACTION_PAGE_LOAD } }, { range: rangeFilter(start, end) }, + ...getDocumentTypeFilterForAggregatedTransactions( + searchAggregatedTransactions + ), ...uiFiltersES, ...transactionNameFilter, ]; const params = { apm: { - events: [ProcessorEvent.transaction], + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], }, body: { size: 0, @@ -69,7 +83,9 @@ export function fetcher(options: Options) { aggs: { avg_duration: { avg: { - field: TRANSACTION_DURATION, + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), }, }, }, diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/index.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/index.ts index e3a0d9e26142a..2c259edaa26ab 100644 --- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/index.ts @@ -16,6 +16,7 @@ import { transformer } from './transformer'; export interface Options { serviceName: string; setup: Setup & SetupTimeRange & SetupUIFilters; + searchAggregatedTransactions: boolean; transactionName?: string; } diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts index 3954d99cd52a8..bc1e0af051ace 100644 --- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ProcessorEvent } from '../../../../common/processor_event'; import { CLIENT_GEO_COUNTRY_ISO_CODE, SERVICE_NAME, - TRANSACTION_DURATION, TRANSACTION_TYPE, TRANSACTION_NAME, } from '../../../../common/elasticsearch_fieldnames'; @@ -19,15 +17,22 @@ import { } from '../../helpers/setup_request'; import { rangeFilter } from '../../../../common/utils/range_filter'; import { TRANSACTION_PAGE_LOAD } from '../../../../common/transaction_types'; +import { + getProcessorEventForAggregatedTransactions, + getTransactionDurationFieldForAggregatedTransactions, + getDocumentTypeFilterForAggregatedTransactions, +} from '../../helpers/aggregated_transactions'; export async function getTransactionAvgDurationByCountry({ setup, serviceName, transactionName, + searchAggregatedTransactions, }: { setup: Setup & SetupTimeRange & SetupUIFilters; serviceName: string; transactionName?: string; + searchAggregatedTransactions: boolean; }) { const { uiFiltersES, apmEventClient, start, end } = setup; const transactionNameFilter = transactionName @@ -35,7 +40,11 @@ export async function getTransactionAvgDurationByCountry({ : []; const params = { apm: { - events: [ProcessorEvent.transaction], + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], }, body: { size: 0, @@ -48,6 +57,9 @@ export async function getTransactionAvgDurationByCountry({ { exists: { field: CLIENT_GEO_COUNTRY_ISO_CODE } }, { range: rangeFilter(start, end) }, ...uiFiltersES, + ...getDocumentTypeFilterForAggregatedTransactions( + searchAggregatedTransactions + ), ], }, }, @@ -58,8 +70,19 @@ export async function getTransactionAvgDurationByCountry({ size: 500, }, aggs: { + count: { + value_count: { + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), + }, + }, avg_duration: { - avg: { field: TRANSACTION_DURATION }, + avg: { + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), + }, }, }, }, @@ -75,10 +98,9 @@ export async function getTransactionAvgDurationByCountry({ const buckets = resp.aggregations.country_code.buckets; const avgDurationsByCountry = buckets.map( - // eslint-disable-next-line @typescript-eslint/naming-convention - ({ key, doc_count, avg_duration: { value } }) => ({ + ({ key, count, avg_duration: { value } }) => ({ key: key as string, - docCount: doc_count, + docCount: count.value, value: value === null ? 0 : value, }) ); diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/fetcher.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/fetcher.test.ts.snap index 7bc60a7fc7f1a..fb696b40f4ab4 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/fetcher.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/fetcher.test.ts.snap @@ -49,6 +49,13 @@ Array [ "transaction_results": Object { "aggs": Object { "timeseries": Object { + "aggs": Object { + "count": Object { + "value_count": Object { + "field": "transaction.duration.us", + }, + }, + }, "date_histogram": Object { "extended_bounds": Object { "max": 1528977600000, diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/transform.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/transform.test.ts.snap index fc9edb496784f..46d6c1425d599 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/transform.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/transform.test.ts.snap @@ -2,2304 +2,4096 @@ exports[`timeseriesTransformer should match snapshot 1`] = ` Object { - "overallAvgDuration": 32861.15660262639, + "overallAvgDuration": 73065.05176360115, "responseTimes": Object { "avg": Array [ Object { - "x": 1528113600000, - "y": 26310.63483891513, + "x": 1593852000000, + "y": null, + }, + Object { + "x": 1593852600000, + "y": null, + }, + Object { + "x": 1593853200000, + "y": null, + }, + Object { + "x": 1593853800000, + "y": null, + }, + Object { + "x": 1593854400000, + "y": null, + }, + Object { + "x": 1593855000000, + "y": null, + }, + Object { + "x": 1593855600000, + "y": null, + }, + Object { + "x": 1593856200000, + "y": null, + }, + Object { + "x": 1593856800000, + "y": null, + }, + Object { + "x": 1593857400000, + "y": null, + }, + Object { + "x": 1593858000000, + "y": null, + }, + Object { + "x": 1593858600000, + "y": null, + }, + Object { + "x": 1593859200000, + "y": null, + }, + Object { + "x": 1593859800000, + "y": null, + }, + Object { + "x": 1593860400000, + "y": null, + }, + Object { + "x": 1593861000000, + "y": null, + }, + Object { + "x": 1593861600000, + "y": null, + }, + Object { + "x": 1593862200000, + "y": null, + }, + Object { + "x": 1593862800000, + "y": null, + }, + Object { + "x": 1593863400000, + "y": null, + }, + Object { + "x": 1593864000000, + "y": null, + }, + Object { + "x": 1593864600000, + "y": null, }, Object { - "x": 1528124400000, - "y": 26193.277795595466, + "x": 1593865200000, + "y": null, }, Object { - "x": 1528135200000, - "y": 25291.787065995228, + "x": 1593865800000, + "y": null, }, Object { - "x": 1528146000000, - "y": 24690.306474667796, + "x": 1593866400000, + "y": null, }, Object { - "x": 1528156800000, - "y": 24809.8953814219, + "x": 1593867000000, + "y": null, }, Object { - "x": 1528167600000, - "y": 25460.0394764508, + "x": 1593867600000, + "y": null, }, Object { - "x": 1528178400000, - "y": 26360.440733498916, + "x": 1593868200000, + "y": null, }, Object { - "x": 1528189200000, - "y": 27050.95205479452, + "x": 1593868800000, + "y": null, }, Object { - "x": 1528200000000, - "y": 26555.857333903925, + "x": 1593869400000, + "y": null, }, Object { - "x": 1528210800000, - "y": 26164.343359049206, + "x": 1593870000000, + "y": null, }, Object { - "x": 1528221600000, - "y": 26989.84546419098, + "x": 1593870600000, + "y": null, }, Object { - "x": 1528232400000, - "y": 26314.409430068266, + "x": 1593871200000, + "y": null, }, Object { - "x": 1528243200000, - "y": 27460.774575018477, + "x": 1593871800000, + "y": null, }, Object { - "x": 1528254000000, - "y": 26461.469107431974, + "x": 1593872400000, + "y": null, }, Object { - "x": 1528264800000, - "y": 27657.584946692834, + "x": 1593873000000, + "y": null, }, Object { - "x": 1528275600000, - "y": 27940.445967005213, + "x": 1593873600000, + "y": null, }, Object { - "x": 1528286400000, - "y": 34454.377581534434, + "x": 1593874200000, + "y": null, }, Object { - "x": 1528297200000, - "y": 44024.31809353839, + "x": 1593874800000, + "y": null, }, Object { - "x": 1528308000000, - "y": 36374.53333333333, + "x": 1593875400000, + "y": null, }, Object { - "x": 1528318800000, - "y": 36991.29442471209, + "x": 1593876000000, + "y": null, }, Object { - "x": 1528329600000, - "y": 37178.002701986756, + "x": 1593876600000, + "y": null, }, Object { - "x": 1528340400000, - "y": 37605.57078923814, + "x": 1593877200000, + "y": null, }, Object { - "x": 1528351200000, - "y": 37319.89767295267, + "x": 1593877800000, + "y": null, }, Object { - "x": 1528362000000, - "y": 38709.5041348433, + "x": 1593878400000, + "y": null, }, Object { - "x": 1528372800000, - "y": 38140.131856255066, + "x": 1593879000000, + "y": null, }, Object { - "x": 1528383600000, - "y": 34564.81091043125, + "x": 1593879600000, + "y": null, }, Object { - "x": 1528394400000, - "y": 33256.37743828302, + "x": 1593880200000, + "y": null, }, Object { - "x": 1528405200000, - "y": 37251.5625266752, + "x": 1593880800000, + "y": null, }, Object { - "x": 1528416000000, - "y": 38681.89084929791, + "x": 1593881400000, + "y": null, }, Object { - "x": 1528426800000, - "y": 40677.801045709355, + "x": 1593882000000, + "y": null, }, Object { - "x": 1528437600000, - "y": 39987.86453616932, + "x": 1593882600000, + "y": null, }, Object { - "x": 1528448400000, - "y": 41059.392914139804, + "x": 1593883200000, + "y": null, }, Object { - "x": 1528459200000, - "y": 39630.710111535845, + "x": 1593883800000, + "y": null, }, Object { - "x": 1528470000000, - "y": 41561.81331074284, + "x": 1593884400000, + "y": null, }, Object { - "x": 1528480800000, - "y": 43079.490738297536, + "x": 1593885000000, + "y": null, }, Object { - "x": 1528491600000, - "y": 43925.39609283509, + "x": 1593885600000, + "y": null, }, Object { - "x": 1528502400000, - "y": 25821.91424646782, + "x": 1593886200000, + "y": null, }, Object { - "x": 1528513200000, - "y": 27343.60011755486, + "x": 1593886800000, + "y": null, }, Object { - "x": 1528524000000, - "y": 25249.95060523233, + "x": 1593887400000, + "y": null, }, Object { - "x": 1528534800000, - "y": 25492.77199074074, + "x": 1593888000000, + "y": null, }, Object { - "x": 1528545600000, - "y": 25991.647281682137, + "x": 1593888600000, + "y": 43364.46153846154, }, Object { - "x": 1528556400000, - "y": 26273.31290445375, + "x": 1593889200000, + "y": 147903.58671586716, }, Object { - "x": 1528567200000, - "y": 26234.98976780795, + "x": 1593889800000, + "y": 57370.52342487884, }, Object { - "x": 1528578000000, - "y": 23494.54873786408, + "x": 1593890400000, + "y": 59687.82558139535, }, Object { - "x": 1528588800000, - "y": 22008.80482069371, + "x": 1593891000000, + "y": 51810.68111455108, }, Object { - "x": 1528599600000, - "y": 22828.136655635586, + "x": 1593891600000, + "y": 51736.59420289855, }, Object { - "x": 1528610400000, - "y": 22138.7081404321, + "x": 1593892200000, + "y": 37241.293224299065, }, Object { - "x": 1528621200000, - "y": 22634.985579811735, + "x": 1593892800000, + "y": 49444.90771558245, }, Object { - "x": 1528632000000, - "y": 22202.780998080616, + "x": 1593893400000, + "y": 56807.80495356037, }, Object { - "x": 1528642800000, - "y": 23084.082780163997, + "x": 1593894000000, + "y": 43238.74519846351, }, Object { - "x": 1528653600000, - "y": 23109.666146341464, + "x": 1593894600000, + "y": 51754.80149253731, }, Object { - "x": 1528664400000, - "y": 23306.89028152719, + "x": 1593895200000, + "y": 47166.5964343598, }, Object { - "x": 1528675200000, - "y": 39341.022704095325, + "x": 1593895800000, + "y": 41854.688405797104, }, Object { - "x": 1528686000000, - "y": 37467.17153341258, + "x": 1593896400000, + "y": 30464.317912218266, }, Object { - "x": 1528696800000, - "y": 52457.50554180566, + "x": 1593897000000, + "y": 41558.531380753135, }, Object { - "x": 1528707600000, - "y": 31327.95780166252, + "x": 1593897600000, + "y": 41159.68345323741, }, Object { - "x": 1528718400000, - "y": 30695.334941163997, + "x": 1593898200000, + "y": 34211.03967168263, }, Object { - "x": 1528729200000, - "y": 28895.042785967435, + "x": 1593898800000, + "y": 41322.30621301775, }, Object { - "x": 1528740000000, - "y": 30649.363989982416, + "x": 1593899400000, + "y": 42301.523605150214, }, Object { - "x": 1528750800000, - "y": 29802.63622014101, + "x": 1593900000000, + "y": 59615.69343065693, }, Object { - "x": 1528761600000, - "y": 30759.03002829892, + "x": 1593900600000, + "y": 29567.520050125313, }, Object { - "x": 1528772400000, - "y": 30399.76549608631, + "x": 1593901200000, + "y": 56104.7484375, }, Object { - "x": 1528783200000, - "y": 29421.610233534506, + "x": 1593901800000, + "y": 40900.70954356847, }, Object { - "x": 1528794000000, - "y": 32641.679897096656, + "x": 1593902400000, + "y": null, }, Object { - "x": 1528804800000, - "y": 30621.65440666204, + "x": 1593903000000, + "y": null, }, Object { - "x": 1528815600000, - "y": 31039.60391005818, + "x": 1593903600000, + "y": 141618.04, }, Object { - "x": 1528826400000, - "y": 30954.760723541545, + "x": 1593904200000, + "y": null, }, Object { - "x": 1528837200000, - "y": 31902.050234568553, + "x": 1593904800000, + "y": null, }, Object { - "x": 1528848000000, - "y": 31594.350653473728, + "x": 1593905400000, + "y": null, }, Object { - "x": 1528858800000, - "y": 31343.87243248879, + "x": 1593906000000, + "y": 380742.48780487804, }, Object { - "x": 1528869600000, - "y": 31200.14450867052, + "x": 1593906600000, + "y": null, }, Object { - "x": 1528880400000, - "y": 28560.946668743833, + "x": 1593907200000, + "y": null, }, Object { - "x": 1528891200000, - "y": 24700.216146371717, + "x": 1593907800000, + "y": null, }, Object { - "x": 1528902000000, - "y": 25261.025210523563, + "x": 1593908400000, + "y": null, }, Object { - "x": 1528912800000, - "y": 26041.39789649068, + "x": 1593909000000, + "y": null, }, Object { - "x": 1528923600000, - "y": 26123.556295209142, + "x": 1593909600000, + "y": null, }, Object { - "x": 1528934400000, - "y": 46231.36177177638, + "x": 1593910200000, + "y": 122524.7027027027, }, Object { - "x": 1528945200000, - "y": 45350.42005506141, + "x": 1593910800000, + "y": null, }, Object { - "x": 1528956000000, - "y": 48256.049354513096, + "x": 1593911400000, + "y": null, }, Object { - "x": 1528966800000, - "y": 52360.30017052116, + "x": 1593912000000, + "y": null, }, Object { - "x": 1528977600000, + "x": 1593912600000, "y": null, }, - ], - "p95": Array [ Object { - "x": 1528113600000, - "y": 82172.85648714812, + "x": 1593913200000, + "y": null, }, Object { - "x": 1528124400000, - "y": 80738.78571428556, + "x": 1593913800000, + "y": null, }, Object { - "x": 1528135200000, - "y": 77058.03529411761, + "x": 1593914400000, + "y": null, }, Object { - "x": 1528146000000, - "y": 77892.20721980717, + "x": 1593915000000, + "y": null, }, Object { - "x": 1528156800000, - "y": 77085.86687499998, + "x": 1593915600000, + "y": null, }, Object { - "x": 1528167600000, - "y": 80048.3462981744, + "x": 1593916200000, + "y": null, }, Object { - "x": 1528178400000, - "y": 84089.21370223971, + "x": 1593916800000, + "y": 160060.1081081081, }, Object { - "x": 1528189200000, - "y": 84880.90143416924, + "x": 1593917400000, + "y": null, }, Object { - "x": 1528200000000, - "y": 84554.8884781166, + "x": 1593918000000, + "y": null, }, Object { - "x": 1528210800000, - "y": 81839.39583333326, + "x": 1593918600000, + "y": null, }, Object { - "x": 1528221600000, - "y": 85993.55410163336, + "x": 1593919200000, + "y": null, }, Object { - "x": 1528232400000, - "y": 85001.44588628765, + "x": 1593919800000, + "y": null, }, Object { - "x": 1528243200000, - "y": 86980.16445312503, + "x": 1593920400000, + "y": null, }, Object { - "x": 1528254000000, - "y": 84961.8710743802, + "x": 1593921000000, + "y": null, }, Object { - "x": 1528264800000, - "y": 88906.54601889332, + "x": 1593921600000, + "y": null, }, Object { - "x": 1528275600000, - "y": 90198.34708994703, + "x": 1593922200000, + "y": null, }, Object { - "x": 1528286400000, - "y": 135627.71242424246, + "x": 1593922800000, + "y": null, }, Object { - "x": 1528297200000, - "y": 167037.1993837535, + "x": 1593923400000, + "y": 70357.234375, }, Object { - "x": 1528308000000, - "y": 128293.12184873945, + "x": 1593924000000, + "y": null, }, Object { - "x": 1528318800000, - "y": 130653.54236263742, + "x": 1593924600000, + "y": null, }, Object { - "x": 1528329600000, - "y": 131630.8902645502, + "x": 1593925200000, + "y": null, }, Object { - "x": 1528340400000, - "y": 133581.33541666638, + "x": 1593925800000, + "y": null, }, Object { - "x": 1528351200000, - "y": 132697.92762266204, + "x": 1593926400000, + "y": null, }, Object { - "x": 1528362000000, - "y": 140003.6918918918, + "x": 1593927000000, + "y": null, }, Object { - "x": 1528372800000, - "y": 138149.5673529411, + "x": 1593927600000, + "y": null, }, Object { - "x": 1528383600000, - "y": 121872.37504835591, + "x": 1593928200000, + "y": null, }, Object { - "x": 1528394400000, - "y": 116378.03873517792, + "x": 1593928800000, + "y": null, }, Object { - "x": 1528405200000, - "y": 131545.40999999995, + "x": 1593929400000, + "y": null, }, Object { - "x": 1528416000000, - "y": 133111.25804878055, + "x": 1593930000000, + "y": 269745.9036144578, }, Object { - "x": 1528426800000, - "y": 144821.9855278593, + "x": 1593930600000, + "y": null, }, Object { - "x": 1528437600000, - "y": 134737.3997727272, + "x": 1593931200000, + "y": null, }, Object { - "x": 1528448400000, - "y": 141206.57726666646, + "x": 1593931800000, + "y": null, }, Object { - "x": 1528459200000, - "y": 137731.8994082841, + "x": 1593932400000, + "y": 313349.95238095237, }, Object { - "x": 1528470000000, - "y": 141476.23189033198, + "x": 1593933000000, + "y": null, }, Object { - "x": 1528480800000, - "y": 149636.31340909077, + "x": 1593933600000, + "y": null, }, Object { - "x": 1528491600000, - "y": 151934.55000000002, + "x": 1593934200000, + "y": null, }, Object { - "x": 1528502400000, - "y": 82198.17857142858, + "x": 1593934800000, + "y": null, }, Object { - "x": 1528513200000, - "y": 85946.43199999983, + "x": 1593935400000, + "y": null, }, Object { - "x": 1528524000000, - "y": 78617.66249999996, + "x": 1593936000000, + "y": null, }, Object { - "x": 1528534800000, - "y": 79606.48333333322, + "x": 1593936600000, + "y": 397251.288372093, }, Object { - "x": 1528545600000, - "y": 76297.93999999986, + "x": 1593937200000, + "y": 361953.5931174089, }, Object { - "x": 1528556400000, - "y": 80742.63333333324, + "x": 1593937800000, + "y": 259173.0694980695, }, Object { - "x": 1528567200000, - "y": 81291.45969696966, + "x": 1593938400000, + "y": 79648.20935412026, }, + ], + "p95": Array [ Object { - "x": 1528578000000, - "y": 73467.02500000004, + "x": 1593852000000, + "y": null, }, Object { - "x": 1528588800000, - "y": 69177.66999999993, + "x": 1593852600000, + "y": null, }, Object { - "x": 1528599600000, - "y": 71956.06111111109, + "x": 1593853200000, + "y": null, }, Object { - "x": 1528610400000, - "y": 68480.91142857139, + "x": 1593853800000, + "y": null, }, Object { - "x": 1528621200000, - "y": 68957.0999999999, + "x": 1593854400000, + "y": null, }, Object { - "x": 1528632000000, - "y": 67489.50416666668, + "x": 1593855000000, + "y": null, }, Object { - "x": 1528642800000, - "y": 71556.91249999998, + "x": 1593855600000, + "y": null, }, Object { - "x": 1528653600000, - "y": 72157.65128205132, + "x": 1593856200000, + "y": null, }, Object { - "x": 1528664400000, - "y": 76124.5625, + "x": 1593856800000, + "y": null, }, Object { - "x": 1528675200000, - "y": 141709.34661835746, + "x": 1593857400000, + "y": null, }, Object { - "x": 1528686000000, - "y": 132371.48641975303, + "x": 1593858000000, + "y": null, }, Object { - "x": 1528696800000, - "y": 186783.51503759398, + "x": 1593858600000, + "y": null, }, Object { - "x": 1528707600000, - "y": 99540.17819499348, + "x": 1593859200000, + "y": null, }, Object { - "x": 1528718400000, - "y": 95982.62454212455, + "x": 1593859800000, + "y": null, }, Object { - "x": 1528729200000, - "y": 89559.3525925925, + "x": 1593860400000, + "y": null, }, Object { - "x": 1528740000000, - "y": 95769.83153735634, + "x": 1593861000000, + "y": null, }, Object { - "x": 1528750800000, - "y": 94063.90833755062, + "x": 1593861600000, + "y": null, }, Object { - "x": 1528761600000, - "y": 96399.67269119772, + "x": 1593862200000, + "y": null, }, Object { - "x": 1528772400000, - "y": 96436.42520161276, + "x": 1593862800000, + "y": null, }, Object { - "x": 1528783200000, - "y": 91860.16988095238, + "x": 1593863400000, + "y": null, }, Object { - "x": 1528794000000, - "y": 105989.8333333334, + "x": 1593864000000, + "y": null, }, Object { - "x": 1528804800000, - "y": 97937.60342555979, + "x": 1593864600000, + "y": null, }, Object { - "x": 1528815600000, - "y": 98967.2249999999, + "x": 1593865200000, + "y": null, }, Object { - "x": 1528826400000, - "y": 97561.02469135808, + "x": 1593865800000, + "y": null, }, Object { - "x": 1528837200000, - "y": 102557.78813357186, + "x": 1593866400000, + "y": null, }, Object { - "x": 1528848000000, - "y": 100137.87578595306, + "x": 1593867000000, + "y": null, }, Object { - "x": 1528858800000, - "y": 98412.97120445351, + "x": 1593867600000, + "y": null, }, Object { - "x": 1528869600000, - "y": 101607.8328012912, + "x": 1593868200000, + "y": null, }, Object { - "x": 1528880400000, - "y": 92000.51368421057, + "x": 1593868800000, + "y": null, }, Object { - "x": 1528891200000, - "y": 78027.29473684198, + "x": 1593869400000, + "y": null, }, Object { - "x": 1528902000000, - "y": 80762.078801789, + "x": 1593870000000, + "y": null, }, Object { - "x": 1528912800000, - "y": 81160.83425925927, + "x": 1593870600000, + "y": null, }, Object { - "x": 1528923600000, - "y": 84215.58945578222, + "x": 1593871200000, + "y": null, }, Object { - "x": 1528934400000, - "y": 194188.21428571426, + "x": 1593871800000, + "y": null, }, Object { - "x": 1528945200000, - "y": 172616.2293896504, + "x": 1593872400000, + "y": null, }, Object { - "x": 1528956000000, - "y": 182653.81858220184, + "x": 1593873000000, + "y": null, }, Object { - "x": 1528966800000, - "y": 194970.75667682925, + "x": 1593873600000, + "y": null, }, Object { - "x": 1528977600000, + "x": 1593874200000, "y": null, }, - ], - "p99": Array [ Object { - "x": 1528113600000, - "y": 293866.3866666665, + "x": 1593874800000, + "y": null, }, Object { - "x": 1528124400000, - "y": 293257.27333333343, + "x": 1593875400000, + "y": null, }, Object { - "x": 1528135200000, - "y": 290195.8800000004, + "x": 1593876000000, + "y": null, }, Object { - "x": 1528146000000, - "y": 278548.1649999994, + "x": 1593876600000, + "y": null, }, Object { - "x": 1528156800000, - "y": 290701.8973333341, + "x": 1593877200000, + "y": null, }, Object { - "x": 1528167600000, - "y": 286839.5897777779, + "x": 1593877800000, + "y": null, }, Object { - "x": 1528178400000, - "y": 287979.5149999999, + "x": 1593878400000, + "y": null, }, Object { - "x": 1528189200000, - "y": 300107.5009999992, + "x": 1593879000000, + "y": null, }, Object { - "x": 1528200000000, - "y": 294402.2179999999, + "x": 1593879600000, + "y": null, }, Object { - "x": 1528210800000, - "y": 289849.459333332, + "x": 1593880200000, + "y": null, }, Object { - "x": 1528221600000, - "y": 296942.86299999955, + "x": 1593880800000, + "y": null, }, Object { - "x": 1528232400000, - "y": 292048.20571428596, + "x": 1593881400000, + "y": null, }, Object { - "x": 1528243200000, - "y": 299308.7371666667, + "x": 1593882000000, + "y": null, }, Object { - "x": 1528254000000, - "y": 292151.2377777781, + "x": 1593882600000, + "y": null, }, Object { - "x": 1528264800000, - "y": 302274.4192592592, + "x": 1593883200000, + "y": null, }, Object { - "x": 1528275600000, - "y": 299457.1612121209, + "x": 1593883800000, + "y": null, }, Object { - "x": 1528286400000, - "y": 350398.59259259375, + "x": 1593884400000, + "y": null, }, Object { - "x": 1528297200000, - "y": 421204.23333333334, + "x": 1593885000000, + "y": null, }, Object { - "x": 1528308000000, - "y": 368166.68976190523, + "x": 1593885600000, + "y": null, }, Object { - "x": 1528318800000, - "y": 367193.6128571426, + "x": 1593886200000, + "y": null, }, Object { - "x": 1528329600000, - "y": 375658.10190476174, + "x": 1593886800000, + "y": null, }, Object { - "x": 1528340400000, - "y": 368152.03822222137, + "x": 1593887400000, + "y": null, }, Object { - "x": 1528351200000, - "y": 365705.8319999995, + "x": 1593888000000, + "y": null, }, Object { - "x": 1528362000000, - "y": 380075.48533333326, + "x": 1593888600000, + "y": 114680, }, Object { - "x": 1528372800000, - "y": 375697.1923809518, + "x": 1593889200000, + "y": 659448, }, Object { - "x": 1528383600000, - "y": 351080.94111111073, + "x": 1593889800000, + "y": 122360, }, Object { - "x": 1528394400000, - "y": 339294.12799999997, + "x": 1593890400000, + "y": 121336, }, Object { - "x": 1528405200000, - "y": 378902.90649999987, + "x": 1593891000000, + "y": 120828, }, Object { - "x": 1528416000000, - "y": 384483.3233333327, + "x": 1593891600000, + "y": 139256, }, Object { - "x": 1528426800000, - "y": 394692.25000000105, + "x": 1593892200000, + "y": 76792, }, Object { - "x": 1528437600000, - "y": 403362.50399999996, + "x": 1593892800000, + "y": 129528, }, Object { - "x": 1528448400000, - "y": 396559.0274999993, + "x": 1593893400000, + "y": 378872, }, Object { - "x": 1528459200000, - "y": 371815.8320000008, + "x": 1593894000000, + "y": 97272, }, Object { - "x": 1528470000000, - "y": 405477.6133333326, + "x": 1593894600000, + "y": 102904, }, Object { - "x": 1528480800000, - "y": 413542.18133333366, + "x": 1593895200000, + "y": 100856, }, Object { - "x": 1528491600000, - "y": 424399.340000001, + "x": 1593895800000, + "y": 97784, }, Object { - "x": 1528502400000, - "y": 303815.9000000001, + "x": 1593896400000, + "y": 72700, }, Object { - "x": 1528513200000, - "y": 306305.0800000006, + "x": 1593897000000, + "y": 98296, }, Object { - "x": 1528524000000, - "y": 297521.94999999984, + "x": 1593897600000, + "y": 112120, }, Object { - "x": 1528534800000, - "y": 317938.0900000003, + "x": 1593898200000, + "y": 91640, }, Object { - "x": 1528545600000, - "y": 312262.3000000003, + "x": 1593898800000, + "y": 83448, }, Object { - "x": 1528556400000, - "y": 318428.8700000002, + "x": 1593899400000, + "y": 84476, }, Object { - "x": 1528567200000, - "y": 295421.4099999999, + "x": 1593900000000, + "y": 117756, }, Object { - "x": 1528578000000, - "y": 293067.86000000004, + "x": 1593900600000, + "y": 66556, }, Object { - "x": 1528588800000, - "y": 264935.71999999933, + "x": 1593901200000, + "y": 130552, }, Object { - "x": 1528599600000, - "y": 282795.0400000003, + "x": 1593901800000, + "y": 111608, }, Object { - "x": 1528610400000, - "y": 285390.8400000001, + "x": 1593902400000, + "y": null, }, Object { - "x": 1528621200000, - "y": 290402.24, + "x": 1593903000000, + "y": null, }, Object { - "x": 1528632000000, - "y": 293655.53, + "x": 1593903600000, + "y": 276448, }, Object { - "x": 1528642800000, - "y": 292723.56999999995, + "x": 1593904200000, + "y": null, }, Object { - "x": 1528653600000, - "y": 301051.32000000105, + "x": 1593904800000, + "y": null, }, Object { - "x": 1528664400000, - "y": 291322.0499999998, + "x": 1593905400000, + "y": null, }, Object { - "x": 1528675200000, - "y": 379855.2444444447, + "x": 1593906000000, + "y": 1028088, }, Object { - "x": 1528686000000, - "y": 371175.2592000001, + "x": 1593906600000, + "y": null, }, Object { - "x": 1528696800000, - "y": 498378.4238888898, + "x": 1593907200000, + "y": null, }, Object { - "x": 1528707600000, - "y": 331118.6599999997, + "x": 1593907800000, + "y": null, }, Object { - "x": 1528718400000, - "y": 328101.3999999988, + "x": 1593908400000, + "y": null, }, Object { - "x": 1528729200000, - "y": 313951.54249999986, + "x": 1593909000000, + "y": null, }, Object { - "x": 1528740000000, - "y": 323340.5274074075, + "x": 1593909600000, + "y": null, }, Object { - "x": 1528750800000, - "y": 315055.5047619052, + "x": 1593910200000, + "y": 352128, }, Object { - "x": 1528761600000, - "y": 330070.03599999985, + "x": 1593910800000, + "y": null, }, Object { - "x": 1528772400000, - "y": 320531.54416666675, + "x": 1593911400000, + "y": null, }, Object { - "x": 1528783200000, - "y": 315137.16628571344, + "x": 1593912000000, + "y": null, }, Object { - "x": 1528794000000, - "y": 337251.4042424246, + "x": 1593912600000, + "y": null, }, Object { - "x": 1528804800000, - "y": 327054.9243636365, + "x": 1593913200000, + "y": null, }, Object { - "x": 1528815600000, - "y": 327653.0000000006, + "x": 1593913800000, + "y": null, }, Object { - "x": 1528826400000, - "y": 324505.1399999999, + "x": 1593914400000, + "y": null, }, Object { - "x": 1528837200000, - "y": 338040.3999999998, + "x": 1593915000000, + "y": null, }, Object { - "x": 1528848000000, - "y": 328600.5173333335, + "x": 1593915600000, + "y": null, }, Object { - "x": 1528858800000, - "y": 334060.93628571345, + "x": 1593916200000, + "y": null, }, Object { - "x": 1528869600000, - "y": 328569.4964999998, + "x": 1593916800000, + "y": 348144, }, Object { - "x": 1528880400000, - "y": 320227.32399999973, + "x": 1593917400000, + "y": null, }, Object { - "x": 1528891200000, - "y": 292019.2899999998, + "x": 1593918000000, + "y": null, }, Object { - "x": 1528902000000, - "y": 297757.72666666657, + "x": 1593918600000, + "y": null, }, Object { - "x": 1528912800000, - "y": 308034.4466666669, + "x": 1593919200000, + "y": null, }, Object { - "x": 1528923600000, - "y": 301128.4895238093, + "x": 1593919800000, + "y": null, }, Object { - "x": 1528934400000, - "y": 447266.9, + "x": 1593920400000, + "y": null, }, Object { - "x": 1528945200000, - "y": 409147.332500001, + "x": 1593921000000, + "y": null, }, Object { - "x": 1528956000000, - "y": 423121.9773333328, + "x": 1593921600000, + "y": null, }, Object { - "x": 1528966800000, - "y": 473485.4199999998, + "x": 1593922200000, + "y": null, }, Object { - "x": 1528977600000, + "x": 1593922800000, "y": null, }, - ], - }, - "tpmBuckets": Array [ - Object { - "avg": 112708, - "dataPoints": Array [ + Object { + "x": 1593923400000, + "y": 270328, + }, + Object { + "x": 1593924000000, + "y": null, + }, + Object { + "x": 1593924600000, + "y": null, + }, + Object { + "x": 1593925200000, + "y": null, + }, + Object { + "x": 1593925800000, + "y": null, + }, + Object { + "x": 1593926400000, + "y": null, + }, + Object { + "x": 1593927000000, + "y": null, + }, + Object { + "x": 1593927600000, + "y": null, + }, + Object { + "x": 1593928200000, + "y": null, + }, + Object { + "x": 1593928800000, + "y": null, + }, + Object { + "x": 1593929400000, + "y": null, + }, + Object { + "x": 1593930000000, + "y": 1687544, + }, + Object { + "x": 1593930600000, + "y": null, + }, + Object { + "x": 1593931200000, + "y": null, + }, + Object { + "x": 1593931800000, + "y": null, + }, + Object { + "x": 1593932400000, + "y": 798656, + }, + Object { + "x": 1593933000000, + "y": null, + }, + Object { + "x": 1593933600000, + "y": null, + }, + Object { + "x": 1593934200000, + "y": null, + }, + Object { + "x": 1593934800000, + "y": null, + }, + Object { + "x": 1593935400000, + "y": null, + }, + Object { + "x": 1593936000000, + "y": null, + }, + Object { + "x": 1593936600000, + "y": 3653624, + }, + Object { + "x": 1593937200000, + "y": 3276768, + }, + Object { + "x": 1593937800000, + "y": 522208, + }, + Object { + "x": 1593938400000, + "y": 372728, + }, + ], + "p99": Array [ + Object { + "x": 1593852000000, + "y": null, + }, + Object { + "x": 1593852600000, + "y": null, + }, + Object { + "x": 1593853200000, + "y": null, + }, + Object { + "x": 1593853800000, + "y": null, + }, + Object { + "x": 1593854400000, + "y": null, + }, + Object { + "x": 1593855000000, + "y": null, + }, + Object { + "x": 1593855600000, + "y": null, + }, + Object { + "x": 1593856200000, + "y": null, + }, + Object { + "x": 1593856800000, + "y": null, + }, + Object { + "x": 1593857400000, + "y": null, + }, + Object { + "x": 1593858000000, + "y": null, + }, + Object { + "x": 1593858600000, + "y": null, + }, + Object { + "x": 1593859200000, + "y": null, + }, + Object { + "x": 1593859800000, + "y": null, + }, + Object { + "x": 1593860400000, + "y": null, + }, + Object { + "x": 1593861000000, + "y": null, + }, + Object { + "x": 1593861600000, + "y": null, + }, + Object { + "x": 1593862200000, + "y": null, + }, + Object { + "x": 1593862800000, + "y": null, + }, + Object { + "x": 1593863400000, + "y": null, + }, + Object { + "x": 1593864000000, + "y": null, + }, + Object { + "x": 1593864600000, + "y": null, + }, + Object { + "x": 1593865200000, + "y": null, + }, + Object { + "x": 1593865800000, + "y": null, + }, + Object { + "x": 1593866400000, + "y": null, + }, + Object { + "x": 1593867000000, + "y": null, + }, + Object { + "x": 1593867600000, + "y": null, + }, + Object { + "x": 1593868200000, + "y": null, + }, + Object { + "x": 1593868800000, + "y": null, + }, + Object { + "x": 1593869400000, + "y": null, + }, + Object { + "x": 1593870000000, + "y": null, + }, + Object { + "x": 1593870600000, + "y": null, + }, + Object { + "x": 1593871200000, + "y": null, + }, + Object { + "x": 1593871800000, + "y": null, + }, + Object { + "x": 1593872400000, + "y": null, + }, + Object { + "x": 1593873000000, + "y": null, + }, + Object { + "x": 1593873600000, + "y": null, + }, + Object { + "x": 1593874200000, + "y": null, + }, + Object { + "x": 1593874800000, + "y": null, + }, + Object { + "x": 1593875400000, + "y": null, + }, + Object { + "x": 1593876000000, + "y": null, + }, + Object { + "x": 1593876600000, + "y": null, + }, + Object { + "x": 1593877200000, + "y": null, + }, + Object { + "x": 1593877800000, + "y": null, + }, + Object { + "x": 1593878400000, + "y": null, + }, + Object { + "x": 1593879000000, + "y": null, + }, + Object { + "x": 1593879600000, + "y": null, + }, + Object { + "x": 1593880200000, + "y": null, + }, + Object { + "x": 1593880800000, + "y": null, + }, + Object { + "x": 1593881400000, + "y": null, + }, + Object { + "x": 1593882000000, + "y": null, + }, + Object { + "x": 1593882600000, + "y": null, + }, + Object { + "x": 1593883200000, + "y": null, + }, + Object { + "x": 1593883800000, + "y": null, + }, + Object { + "x": 1593884400000, + "y": null, + }, + Object { + "x": 1593885000000, + "y": null, + }, + Object { + "x": 1593885600000, + "y": null, + }, + Object { + "x": 1593886200000, + "y": null, + }, + Object { + "x": 1593886800000, + "y": null, + }, + Object { + "x": 1593887400000, + "y": null, + }, + Object { + "x": 1593888000000, + "y": null, + }, + Object { + "x": 1593888600000, + "y": 827384, + }, + Object { + "x": 1593889200000, + "y": 2326520, + }, + Object { + "x": 1593889800000, + "y": 1130488, + }, + Object { + "x": 1593890400000, + "y": 1032184, + }, + Object { + "x": 1593891000000, + "y": 770044, + }, + Object { + "x": 1593891600000, + "y": 651256, + }, + Object { + "x": 1593892200000, + "y": 667640, + }, + Object { + "x": 1593892800000, + "y": 708600, + }, + Object { + "x": 1593893400000, + "y": 815096, + }, + Object { + "x": 1593894000000, + "y": 688120, + }, + Object { + "x": 1593894600000, + "y": 978936, + }, + Object { + "x": 1593895200000, + "y": 839672, + }, + Object { + "x": 1593895800000, + "y": 757752, + }, + Object { + "x": 1593896400000, + "y": 577532, + }, + Object { + "x": 1593897000000, + "y": 618488, + }, + Object { + "x": 1593897600000, + "y": 565240, + }, + Object { + "x": 1593898200000, + "y": 618488, + }, + Object { + "x": 1593898800000, + "y": 655352, + }, + Object { + "x": 1593899400000, + "y": 843772, + }, + Object { + "x": 1593900000000, + "y": 831484, + }, + Object { + "x": 1593900600000, + "y": 430076, + }, + Object { + "x": 1593901200000, + "y": 864248, + }, + Object { + "x": 1593901800000, + "y": 655352, + }, + Object { + "x": 1593902400000, + "y": null, + }, + Object { + "x": 1593903000000, + "y": null, + }, + Object { + "x": 1593903600000, + "y": 2883552, + }, + Object { + "x": 1593904200000, + "y": null, + }, + Object { + "x": 1593904800000, + "y": null, + }, + Object { + "x": 1593905400000, + "y": null, + }, + Object { + "x": 1593906000000, + "y": 6094840, + }, + Object { + "x": 1593906600000, + "y": null, + }, + Object { + "x": 1593907200000, + "y": null, + }, + Object { + "x": 1593907800000, + "y": null, + }, + Object { + "x": 1593908400000, + "y": null, + }, + Object { + "x": 1593909000000, + "y": null, + }, + Object { + "x": 1593909600000, + "y": null, + }, + Object { + "x": 1593910200000, + "y": 446336, + }, + Object { + "x": 1593910800000, + "y": null, + }, + Object { + "x": 1593911400000, + "y": null, + }, + Object { + "x": 1593912000000, + "y": null, + }, + Object { + "x": 1593912600000, + "y": null, + }, + Object { + "x": 1593913200000, + "y": null, + }, + Object { + "x": 1593913800000, + "y": null, + }, + Object { + "x": 1593914400000, + "y": null, + }, + Object { + "x": 1593915000000, + "y": null, + }, + Object { + "x": 1593915600000, + "y": null, + }, + Object { + "x": 1593916200000, + "y": null, + }, + Object { + "x": 1593916800000, + "y": 3293168, + }, + Object { + "x": 1593917400000, + "y": null, + }, + Object { + "x": 1593918000000, + "y": null, + }, + Object { + "x": 1593918600000, + "y": null, + }, + Object { + "x": 1593919200000, + "y": null, + }, + Object { + "x": 1593919800000, + "y": null, + }, + Object { + "x": 1593920400000, + "y": null, + }, + Object { + "x": 1593921000000, + "y": null, + }, + Object { + "x": 1593921600000, + "y": null, + }, + Object { + "x": 1593922200000, + "y": null, + }, + Object { + "x": 1593922800000, + "y": null, + }, + Object { + "x": 1593923400000, + "y": 299000, + }, + Object { + "x": 1593924000000, + "y": null, + }, + Object { + "x": 1593924600000, + "y": null, + }, + Object { + "x": 1593925200000, + "y": null, + }, + Object { + "x": 1593925800000, + "y": null, + }, + Object { + "x": 1593926400000, + "y": null, + }, + Object { + "x": 1593927000000, + "y": null, + }, + Object { + "x": 1593927600000, + "y": null, + }, + Object { + "x": 1593928200000, + "y": null, + }, + Object { + "x": 1593928800000, + "y": null, + }, + Object { + "x": 1593929400000, + "y": null, + }, + Object { + "x": 1593930000000, + "y": 5046264, + }, + Object { + "x": 1593930600000, + "y": null, + }, + Object { + "x": 1593931200000, + "y": null, + }, + Object { + "x": 1593931800000, + "y": null, + }, + Object { + "x": 1593932400000, + "y": 4292544, + }, + Object { + "x": 1593933000000, + "y": null, + }, + Object { + "x": 1593933600000, + "y": null, + }, + Object { + "x": 1593934200000, + "y": null, + }, + Object { + "x": 1593934800000, + "y": null, + }, + Object { + "x": 1593935400000, + "y": null, + }, + Object { + "x": 1593936000000, + "y": null, + }, + Object { + "x": 1593936600000, + "y": 5046264, + }, + Object { + "x": 1593937200000, + "y": 4292576, + }, + Object { + "x": 1593937800000, + "y": 4128736, + }, + Object { + "x": 1593938400000, + "y": 843768, + }, + ], + }, + "tpmBuckets": Array [ + Object { + "avg": 1215, + "dataPoints": Array [ + Object { + "x": 1593852000000, + "y": 0, + }, + Object { + "x": 1593852600000, + "y": 0, + }, + Object { + "x": 1593853200000, + "y": 0, + }, + Object { + "x": 1593853800000, + "y": 0, + }, + Object { + "x": 1593854400000, + "y": 0, + }, + Object { + "x": 1593855000000, + "y": 0, + }, + Object { + "x": 1593855600000, + "y": 0, + }, + Object { + "x": 1593856200000, + "y": 0, + }, + Object { + "x": 1593856800000, + "y": 0, + }, + Object { + "x": 1593857400000, + "y": 0, + }, + Object { + "x": 1593858000000, + "y": 0, + }, + Object { + "x": 1593858600000, + "y": 0, + }, + Object { + "x": 1593859200000, + "y": 0, + }, + Object { + "x": 1593859800000, + "y": 0, + }, + Object { + "x": 1593860400000, + "y": 0, + }, + Object { + "x": 1593861000000, + "y": 0, + }, + Object { + "x": 1593861600000, + "y": 0, + }, + Object { + "x": 1593862200000, + "y": 0, + }, + Object { + "x": 1593862800000, + "y": 0, + }, + Object { + "x": 1593863400000, + "y": 0, + }, + Object { + "x": 1593864000000, + "y": 0, + }, + Object { + "x": 1593864600000, + "y": 0, + }, + Object { + "x": 1593865200000, + "y": 0, + }, + Object { + "x": 1593865800000, + "y": 0, + }, + Object { + "x": 1593866400000, + "y": 0, + }, + Object { + "x": 1593867000000, + "y": 0, + }, + Object { + "x": 1593867600000, + "y": 0, + }, + Object { + "x": 1593868200000, + "y": 0, + }, + Object { + "x": 1593868800000, + "y": 0, + }, + Object { + "x": 1593869400000, + "y": 0, + }, + Object { + "x": 1593870000000, + "y": 0, + }, + Object { + "x": 1593870600000, + "y": 0, + }, + Object { + "x": 1593871200000, + "y": 0, + }, + Object { + "x": 1593871800000, + "y": 0, + }, + Object { + "x": 1593872400000, + "y": 0, + }, + Object { + "x": 1593873000000, + "y": 0, + }, + Object { + "x": 1593873600000, + "y": 0, + }, + Object { + "x": 1593874200000, + "y": 0, + }, + Object { + "x": 1593874800000, + "y": 0, + }, + Object { + "x": 1593875400000, + "y": 0, + }, + Object { + "x": 1593876000000, + "y": 0, + }, + Object { + "x": 1593876600000, + "y": 0, + }, + Object { + "x": 1593877200000, + "y": 0, + }, + Object { + "x": 1593877800000, + "y": 0, + }, + Object { + "x": 1593878400000, + "y": 0, + }, + Object { + "x": 1593879000000, + "y": 0, + }, + Object { + "x": 1593879600000, + "y": 0, + }, + Object { + "x": 1593880200000, + "y": 0, + }, + Object { + "x": 1593880800000, + "y": 0, + }, + Object { + "x": 1593881400000, + "y": 0, + }, + Object { + "x": 1593882000000, + "y": 0, + }, + Object { + "x": 1593882600000, + "y": 0, + }, + Object { + "x": 1593883200000, + "y": 0, + }, + Object { + "x": 1593883800000, + "y": 0, + }, + Object { + "x": 1593884400000, + "y": 0, + }, + Object { + "x": 1593885000000, + "y": 0, + }, + Object { + "x": 1593885600000, + "y": 0, + }, + Object { + "x": 1593886200000, + "y": 0, + }, + Object { + "x": 1593886800000, + "y": 0, + }, + Object { + "x": 1593887400000, + "y": 0, + }, + Object { + "x": 1593888000000, + "y": 0, + }, + Object { + "x": 1593888600000, + "y": 84.5, + }, + Object { + "x": 1593889200000, + "y": 222, + }, + Object { + "x": 1593889800000, + "y": 230, + }, + Object { + "x": 1593890400000, + "y": 253, + }, + Object { + "x": 1593891000000, + "y": 239.5, + }, + Object { + "x": 1593891600000, + "y": 228.5, + }, + Object { + "x": 1593892200000, + "y": 257, + }, + Object { + "x": 1593892800000, + "y": 241, + }, + Object { + "x": 1593893400000, + "y": 252, + }, + Object { + "x": 1593894000000, + "y": 266, + }, + Object { + "x": 1593894600000, + "y": 229, + }, + Object { + "x": 1593895200000, + "y": 224, + }, + Object { + "x": 1593895800000, + "y": 234, + }, + Object { + "x": 1593896400000, + "y": 263, + }, + Object { + "x": 1593897000000, + "y": 247.5, + }, + Object { + "x": 1593897600000, + "y": 246, + }, + Object { + "x": 1593898200000, + "y": 243.5, + }, + Object { + "x": 1593898800000, + "y": 245.5, + }, + Object { + "x": 1593899400000, + "y": 243, + }, + Object { + "x": 1593900000000, + "y": 229, + }, + Object { + "x": 1593900600000, + "y": 264, + }, + Object { + "x": 1593901200000, + "y": 233.5, + }, + Object { + "x": 1593901800000, + "y": 89.5, + }, + Object { + "x": 1593902400000, + "y": 0, + }, + Object { + "x": 1593903000000, + "y": 0, + }, + Object { + "x": 1593903600000, + "y": 19.5, + }, + Object { + "x": 1593904200000, + "y": 0, + }, + Object { + "x": 1593904800000, + "y": 0, + }, + Object { + "x": 1593905400000, + "y": 0, + }, + Object { + "x": 1593906000000, + "y": 18, + }, + Object { + "x": 1593906600000, + "y": 0, + }, + Object { + "x": 1593907200000, + "y": 0, + }, + Object { + "x": 1593907800000, + "y": 0, + }, + Object { + "x": 1593908400000, + "y": 0, + }, + Object { + "x": 1593909000000, + "y": 0, + }, + Object { + "x": 1593909600000, + "y": 0, + }, + Object { + "x": 1593910200000, + "y": 17, + }, + Object { + "x": 1593910800000, + "y": 0, + }, + Object { + "x": 1593911400000, + "y": 0, + }, + Object { + "x": 1593912000000, + "y": 0, + }, + Object { + "x": 1593912600000, + "y": 0, + }, + Object { + "x": 1593913200000, + "y": 0, + }, + Object { + "x": 1593913800000, + "y": 0, + }, + Object { + "x": 1593914400000, + "y": 0, + }, + Object { + "x": 1593915000000, + "y": 0, + }, + Object { + "x": 1593915600000, + "y": 0, + }, + Object { + "x": 1593916200000, + "y": 0, + }, + Object { + "x": 1593916800000, + "y": 15.5, + }, + Object { + "x": 1593917400000, + "y": 0, + }, + Object { + "x": 1593918000000, + "y": 0, + }, + Object { + "x": 1593918600000, + "y": 0, + }, + Object { + "x": 1593919200000, + "y": 0, + }, + Object { + "x": 1593919800000, + "y": 0, + }, + Object { + "x": 1593920400000, + "y": 0, + }, + Object { + "x": 1593921000000, + "y": 0, + }, + Object { + "x": 1593921600000, + "y": 0, + }, + Object { + "x": 1593922200000, + "y": 0, + }, + Object { + "x": 1593922800000, + "y": 0, + }, + Object { + "x": 1593923400000, + "y": 24.5, + }, + Object { + "x": 1593924000000, + "y": 0, + }, + Object { + "x": 1593924600000, + "y": 0, + }, + Object { + "x": 1593925200000, + "y": 0, + }, + Object { + "x": 1593925800000, + "y": 0, + }, + Object { + "x": 1593926400000, + "y": 0, + }, + Object { + "x": 1593927000000, + "y": 0, + }, + Object { + "x": 1593927600000, + "y": 0, + }, + Object { + "x": 1593928200000, + "y": 0, + }, + Object { + "x": 1593928800000, + "y": 0, + }, + Object { + "x": 1593929400000, + "y": 0, + }, + Object { + "x": 1593930000000, + "y": 25, + }, + Object { + "x": 1593930600000, + "y": 0, + }, + Object { + "x": 1593931200000, + "y": 0, + }, + Object { + "x": 1593931800000, + "y": 0, + }, + Object { + "x": 1593932400000, + "y": 18.5, + }, + Object { + "x": 1593933000000, + "y": 0, + }, + Object { + "x": 1593933600000, + "y": 0, + }, + Object { + "x": 1593934200000, + "y": 0, + }, + Object { + "x": 1593934800000, + "y": 0, + }, + Object { + "x": 1593935400000, + "y": 0, + }, + Object { + "x": 1593936000000, + "y": 0, + }, + Object { + "x": 1593936600000, + "y": 97, + }, + Object { + "x": 1593937200000, + "y": 192.5, + }, + Object { + "x": 1593937800000, + "y": 210.5, + }, + Object { + "x": 1593938400000, + "y": 172, + }, + ], + "key": "HTTP 2xx", + }, + Object { + "avg": 382.8, + "dataPoints": Array [ + Object { + "x": 1593852000000, + "y": 0, + }, + Object { + "x": 1593852600000, + "y": 0, + }, + Object { + "x": 1593853200000, + "y": 0, + }, + Object { + "x": 1593853800000, + "y": 0, + }, + Object { + "x": 1593854400000, + "y": 0, + }, + Object { + "x": 1593855000000, + "y": 0, + }, + Object { + "x": 1593855600000, + "y": 0, + }, + Object { + "x": 1593856200000, + "y": 0, + }, + Object { + "x": 1593856800000, + "y": 0, + }, + Object { + "x": 1593857400000, + "y": 0, + }, + Object { + "x": 1593858000000, + "y": 0, + }, + Object { + "x": 1593858600000, + "y": 0, + }, + Object { + "x": 1593859200000, + "y": 0, + }, + Object { + "x": 1593859800000, + "y": 0, + }, + Object { + "x": 1593860400000, + "y": 0, + }, + Object { + "x": 1593861000000, + "y": 0, + }, + Object { + "x": 1593861600000, + "y": 0, + }, + Object { + "x": 1593862200000, + "y": 0, + }, + Object { + "x": 1593862800000, + "y": 0, + }, + Object { + "x": 1593863400000, + "y": 0, + }, + Object { + "x": 1593864000000, + "y": 0, + }, + Object { + "x": 1593864600000, + "y": 0, + }, + Object { + "x": 1593865200000, + "y": 0, + }, + Object { + "x": 1593865800000, + "y": 0, + }, + Object { + "x": 1593866400000, + "y": 0, + }, + Object { + "x": 1593867000000, + "y": 0, + }, + Object { + "x": 1593867600000, + "y": 0, + }, + Object { + "x": 1593868200000, + "y": 0, + }, + Object { + "x": 1593868800000, + "y": 0, + }, + Object { + "x": 1593869400000, + "y": 0, + }, + Object { + "x": 1593870000000, + "y": 0, + }, + Object { + "x": 1593870600000, + "y": 0, + }, + Object { + "x": 1593871200000, + "y": 0, + }, + Object { + "x": 1593871800000, + "y": 0, + }, + Object { + "x": 1593872400000, + "y": 0, + }, + Object { + "x": 1593873000000, + "y": 0, + }, + Object { + "x": 1593873600000, + "y": 0, + }, + Object { + "x": 1593874200000, + "y": 0, + }, + Object { + "x": 1593874800000, + "y": 0, + }, + Object { + "x": 1593875400000, + "y": 0, + }, + Object { + "x": 1593876000000, + "y": 0, + }, + Object { + "x": 1593876600000, + "y": 0, + }, + Object { + "x": 1593877200000, + "y": 0, + }, + Object { + "x": 1593877800000, + "y": 0, + }, + Object { + "x": 1593878400000, + "y": 0, + }, + Object { + "x": 1593879000000, + "y": 0, + }, + Object { + "x": 1593879600000, + "y": 0, + }, + Object { + "x": 1593880200000, + "y": 0, + }, + Object { + "x": 1593880800000, + "y": 0, + }, + Object { + "x": 1593881400000, + "y": 0, + }, + Object { + "x": 1593882000000, + "y": 0, + }, + Object { + "x": 1593882600000, + "y": 0, + }, + Object { + "x": 1593883200000, + "y": 0, + }, + Object { + "x": 1593883800000, + "y": 0, + }, + Object { + "x": 1593884400000, + "y": 0, + }, + Object { + "x": 1593885000000, + "y": 0, + }, + Object { + "x": 1593885600000, + "y": 0, + }, + Object { + "x": 1593886200000, + "y": 0, + }, + Object { + "x": 1593886800000, + "y": 0, + }, + Object { + "x": 1593887400000, + "y": 0, + }, + Object { + "x": 1593888000000, + "y": 0, + }, + Object { + "x": 1593888600000, + "y": 31, + }, + Object { + "x": 1593889200000, + "y": 26, + }, + Object { + "x": 1593889800000, + "y": 64, + }, + Object { + "x": 1593890400000, + "y": 71.5, + }, + Object { + "x": 1593891000000, + "y": 64.5, + }, + Object { + "x": 1593891600000, + "y": 60.5, + }, + Object { + "x": 1593892200000, + "y": 146, + }, + Object { + "x": 1593892800000, + "y": 69.5, + }, + Object { + "x": 1593893400000, + "y": 52, + }, + Object { + "x": 1593894000000, + "y": 99, + }, + Object { + "x": 1593894600000, + "y": 89.5, + }, + Object { + "x": 1593895200000, + "y": 58.5, + }, + Object { + "x": 1593895800000, + "y": 91.5, + }, + Object { + "x": 1593896400000, + "y": 132, + }, + Object { + "x": 1593897000000, + "y": 90, + }, + Object { + "x": 1593897600000, + "y": 80, + }, + Object { + "x": 1593898200000, + "y": 104, + }, + Object { + "x": 1593898800000, + "y": 79, + }, + Object { + "x": 1593899400000, + "y": 88, + }, + Object { + "x": 1593900000000, + "y": 91.5, + }, + Object { + "x": 1593900600000, + "y": 117, + }, + Object { + "x": 1593901200000, + "y": 62.5, + }, + Object { + "x": 1593901800000, + "y": 24, + }, + Object { + "x": 1593902400000, + "y": 0, + }, + Object { + "x": 1593903000000, + "y": 0, + }, + Object { + "x": 1593903600000, + "y": 4.5, + }, + Object { + "x": 1593904200000, + "y": 0, + }, + Object { + "x": 1593904800000, + "y": 0, + }, + Object { + "x": 1593905400000, + "y": 0, + }, + Object { + "x": 1593906000000, + "y": 0, + }, + Object { + "x": 1593906600000, + "y": 0, + }, + Object { + "x": 1593907200000, + "y": 0, + }, + Object { + "x": 1593907800000, + "y": 0, + }, + Object { + "x": 1593908400000, + "y": 0, + }, + Object { + "x": 1593909000000, + "y": 0, + }, + Object { + "x": 1593909600000, + "y": 0, + }, + Object { + "x": 1593910200000, + "y": 0, + }, + Object { + "x": 1593910800000, + "y": 0, + }, + Object { + "x": 1593911400000, + "y": 0, + }, + Object { + "x": 1593912000000, + "y": 0, + }, + Object { + "x": 1593912600000, + "y": 0, + }, + Object { + "x": 1593913200000, + "y": 0, + }, + Object { + "x": 1593913800000, + "y": 0, + }, + Object { + "x": 1593914400000, + "y": 0, + }, + Object { + "x": 1593915000000, + "y": 0, + }, + Object { + "x": 1593915600000, + "y": 0, + }, + Object { + "x": 1593916200000, + "y": 0, + }, + Object { + "x": 1593916800000, + "y": 0, + }, Object { - "x": 1528113600000, - "y": 8223, + "x": 1593917400000, + "y": 0, }, Object { - "x": 1528124400000, - "y": 8146, + "x": 1593918000000, + "y": 0, }, Object { - "x": 1528135200000, - "y": 8232, + "x": 1593918600000, + "y": 0, }, Object { - "x": 1528146000000, - "y": 8248.5, + "x": 1593919200000, + "y": 0, }, Object { - "x": 1528156800000, - "y": 8399.5, + "x": 1593919800000, + "y": 0, }, Object { - "x": 1528167600000, - "y": 8280.5, + "x": 1593920400000, + "y": 0, }, Object { - "x": 1528178400000, - "y": 8215.5, + "x": 1593921000000, + "y": 0, }, Object { - "x": 1528189200000, - "y": 8191.5, + "x": 1593921600000, + "y": 0, }, Object { - "x": 1528200000000, - "y": 8147.5, + "x": 1593922200000, + "y": 0, }, Object { - "x": 1528210800000, - "y": 8351, + "x": 1593922800000, + "y": 0, }, Object { - "x": 1528221600000, - "y": 8234.5, + "x": 1593923400000, + "y": 5.5, }, Object { - "x": 1528232400000, - "y": 8233, + "x": 1593924000000, + "y": 0, }, Object { - "x": 1528243200000, - "y": 8275.5, + "x": 1593924600000, + "y": 0, }, Object { - "x": 1528254000000, - "y": 8337.5, + "x": 1593925200000, + "y": 0, }, Object { - "x": 1528264800000, - "y": 8205, + "x": 1593925800000, + "y": 0, }, Object { - "x": 1528275600000, - "y": 8123.5, + "x": 1593926400000, + "y": 0, }, Object { - "x": 1528286400000, - "y": 7572.5, + "x": 1593927000000, + "y": 0, }, Object { - "x": 1528297200000, - "y": 8089, + "x": 1593927600000, + "y": 0, }, Object { - "x": 1528308000000, - "y": 8265, + "x": 1593928200000, + "y": 0, }, Object { - "x": 1528318800000, - "y": 8105.5, + "x": 1593928800000, + "y": 0, }, Object { - "x": 1528329600000, - "y": 8226.5, + "x": 1593929400000, + "y": 0, }, Object { - "x": 1528340400000, - "y": 8251.5, + "x": 1593930000000, + "y": 14, }, Object { - "x": 1528351200000, - "y": 8302, + "x": 1593930600000, + "y": 0, }, Object { - "x": 1528362000000, - "y": 8261, + "x": 1593931200000, + "y": 0, }, Object { - "x": 1528372800000, - "y": 8082, + "x": 1593931800000, + "y": 0, }, Object { - "x": 1528383600000, - "y": 8260, + "x": 1593932400000, + "y": 1, }, Object { - "x": 1528394400000, - "y": 8267, + "x": 1593933000000, + "y": 0, }, Object { - "x": 1528405200000, - "y": 8155.5, + "x": 1593933600000, + "y": 0, }, Object { - "x": 1528416000000, - "y": 8335, + "x": 1593934200000, + "y": 0, }, Object { - "x": 1528426800000, - "y": 8096, + "x": 1593934800000, + "y": 0, }, Object { - "x": 1528437600000, - "y": 8289.5, + "x": 1593935400000, + "y": 0, }, Object { - "x": 1528448400000, - "y": 8165, + "x": 1593936000000, + "y": 0, }, Object { - "x": 1528459200000, - "y": 8282.5, + "x": 1593936600000, + "y": 0, }, Object { - "x": 1528470000000, - "y": 8271.5, + "x": 1593937200000, + "y": 31.5, }, Object { - "x": 1528480800000, - "y": 8246, + "x": 1593937800000, + "y": 25, }, Object { - "x": 1528491600000, - "y": 8202, + "x": 1593938400000, + "y": 41, }, + ], + "key": "HTTP 3xx", + }, + Object { + "avg": 68.3, + "dataPoints": Array [ Object { - "x": 1528502400000, - "y": 2264, + "x": 1593852000000, + "y": 0, }, Object { - "x": 1528513200000, - "y": 2278.5, + "x": 1593852600000, + "y": 0, }, Object { - "x": 1528524000000, - "y": 2283, + "x": 1593853200000, + "y": 0, }, Object { - "x": 1528534800000, - "y": 2293, + "x": 1593853800000, + "y": 0, }, Object { - "x": 1528545600000, - "y": 2336, + "x": 1593854400000, + "y": 0, }, Object { - "x": 1528556400000, - "y": 2342.5, + "x": 1593855000000, + "y": 0, }, Object { - "x": 1528567200000, - "y": 2260.5, + "x": 1593855600000, + "y": 0, }, Object { - "x": 1528578000000, - "y": 2306, + "x": 1593856200000, + "y": 0, }, Object { - "x": 1528588800000, - "y": 2267.5, + "x": 1593856800000, + "y": 0, }, Object { - "x": 1528599600000, - "y": 2303, + "x": 1593857400000, + "y": 0, }, Object { - "x": 1528610400000, - "y": 2307, + "x": 1593858000000, + "y": 0, }, Object { - "x": 1528621200000, - "y": 2253.5, + "x": 1593858600000, + "y": 0, }, Object { - "x": 1528632000000, - "y": 2305.5, + "x": 1593859200000, + "y": 0, }, Object { - "x": 1528642800000, - "y": 2293.5, + "x": 1593859800000, + "y": 0, }, Object { - "x": 1528653600000, - "y": 2291, + "x": 1593860400000, + "y": 0, }, Object { - "x": 1528664400000, - "y": 2307.5, + "x": 1593861000000, + "y": 0, }, Object { - "x": 1528675200000, - "y": 8125.5, + "x": 1593861600000, + "y": 0, }, Object { - "x": 1528686000000, - "y": 8412.5, + "x": 1593862200000, + "y": 0, }, Object { - "x": 1528696800000, - "y": 8144, + "x": 1593862800000, + "y": 0, }, Object { - "x": 1528707600000, - "y": 8246, + "x": 1593863400000, + "y": 0, }, Object { - "x": 1528718400000, - "y": 8217, + "x": 1593864000000, + "y": 0, }, Object { - "x": 1528729200000, - "y": 8501.5, + "x": 1593864600000, + "y": 0, }, Object { - "x": 1528740000000, - "y": 8182, + "x": 1593865200000, + "y": 0, }, Object { - "x": 1528750800000, - "y": 8322.5, + "x": 1593865800000, + "y": 0, }, Object { - "x": 1528761600000, - "y": 8347.5, + "x": 1593866400000, + "y": 0, }, Object { - "x": 1528772400000, - "y": 8249, + "x": 1593867000000, + "y": 0, }, Object { - "x": 1528783200000, - "y": 8294, + "x": 1593867600000, + "y": 0, }, Object { - "x": 1528794000000, - "y": 8342.5, + "x": 1593868200000, + "y": 0, }, Object { - "x": 1528804800000, - "y": 8180.5, + "x": 1593868800000, + "y": 0, }, Object { - "x": 1528815600000, - "y": 8329, + "x": 1593869400000, + "y": 0, }, Object { - "x": 1528826400000, - "y": 8253.5, + "x": 1593870000000, + "y": 0, }, Object { - "x": 1528837200000, - "y": 8209, + "x": 1593870600000, + "y": 0, }, Object { - "x": 1528848000000, - "y": 8238.5, + "x": 1593871200000, + "y": 0, }, Object { - "x": 1528858800000, - "y": 8377.5, + "x": 1593871800000, + "y": 0, }, Object { - "x": 1528869600000, - "y": 8297, + "x": 1593872400000, + "y": 0, }, Object { - "x": 1528880400000, - "y": 8406, + "x": 1593873000000, + "y": 0, }, Object { - "x": 1528891200000, - "y": 8431.5, + "x": 1593873600000, + "y": 0, }, Object { - "x": 1528902000000, - "y": 8327.5, + "x": 1593874200000, + "y": 0, }, Object { - "x": 1528912800000, - "y": 8361.5, + "x": 1593874800000, + "y": 0, }, Object { - "x": 1528923600000, - "y": 8288.5, + "x": 1593875400000, + "y": 0, }, Object { - "x": 1528934400000, - "y": 7562.5, + "x": 1593876000000, + "y": 0, }, Object { - "x": 1528945200000, - "y": 8216, + "x": 1593876600000, + "y": 0, }, Object { - "x": 1528956000000, - "y": 8232, + "x": 1593877200000, + "y": 0, }, Object { - "x": 1528966800000, - "y": 8184.5, + "x": 1593877800000, + "y": 0, }, Object { - "x": 1528977600000, + "x": 1593878400000, "y": 0, }, - ], - "key": "HTTP 2xx", - }, - Object { - "avg": 665, - "dataPoints": Array [ Object { - "x": 1528113600000, + "x": 1593879000000, "y": 0, }, Object { - "x": 1528124400000, + "x": 1593879600000, "y": 0, }, Object { - "x": 1528135200000, + "x": 1593880200000, "y": 0, }, Object { - "x": 1528146000000, + "x": 1593880800000, "y": 0, }, Object { - "x": 1528156800000, + "x": 1593881400000, "y": 0, }, Object { - "x": 1528167600000, + "x": 1593882000000, "y": 0, }, Object { - "x": 1528178400000, + "x": 1593882600000, "y": 0, }, Object { - "x": 1528189200000, + "x": 1593883200000, "y": 0, }, Object { - "x": 1528200000000, + "x": 1593883800000, "y": 0, }, Object { - "x": 1528210800000, + "x": 1593884400000, "y": 0, }, Object { - "x": 1528221600000, + "x": 1593885000000, "y": 0, }, Object { - "x": 1528232400000, + "x": 1593885600000, "y": 0, }, Object { - "x": 1528243200000, + "x": 1593886200000, "y": 0, }, Object { - "x": 1528254000000, + "x": 1593886800000, "y": 0, }, Object { - "x": 1528264800000, + "x": 1593887400000, "y": 0, }, Object { - "x": 1528275600000, + "x": 1593888000000, "y": 0, }, Object { - "x": 1528286400000, - "y": 2020.5, + "x": 1593888600000, + "y": 5.5, }, Object { - "x": 1528297200000, - "y": 227, + "x": 1593889200000, + "y": 15.5, }, Object { - "x": 1528308000000, - "y": 0, + "x": 1593889800000, + "y": 9.5, }, Object { - "x": 1528318800000, - "y": 0, + "x": 1593890400000, + "y": 11.5, }, Object { - "x": 1528329600000, - "y": 0, + "x": 1593891000000, + "y": 13, }, Object { - "x": 1528340400000, - "y": 0, + "x": 1593891600000, + "y": 13.5, }, Object { - "x": 1528351200000, - "y": 0, + "x": 1593892200000, + "y": 13.5, }, Object { - "x": 1528362000000, - "y": 0, + "x": 1593892800000, + "y": 15, }, Object { - "x": 1528372800000, - "y": 0, + "x": 1593893400000, + "y": 14, }, Object { - "x": 1528383600000, - "y": 0, + "x": 1593894000000, + "y": 16.5, }, Object { - "x": 1528394400000, - "y": 0, + "x": 1593894600000, + "y": 11.5, }, Object { - "x": 1528405200000, - "y": 0, + "x": 1593895200000, + "y": 17.5, }, Object { - "x": 1528416000000, - "y": 0, + "x": 1593895800000, + "y": 13, }, Object { - "x": 1528426800000, - "y": 0, + "x": 1593896400000, + "y": 17.5, }, Object { - "x": 1528437600000, - "y": 0, + "x": 1593897000000, + "y": 12.5, }, Object { - "x": 1528448400000, - "y": 0, + "x": 1593897600000, + "y": 13, }, Object { - "x": 1528459200000, - "y": 0, + "x": 1593898200000, + "y": 12.5, }, Object { - "x": 1528470000000, - "y": 0, + "x": 1593898800000, + "y": 8.5, }, Object { - "x": 1528480800000, - "y": 0, + "x": 1593899400000, + "y": 9.5, }, Object { - "x": 1528491600000, - "y": 0, + "x": 1593900000000, + "y": 14, }, Object { - "x": 1528502400000, - "y": 0, + "x": 1593900600000, + "y": 12, }, Object { - "x": 1528513200000, - "y": 0, + "x": 1593901200000, + "y": 15, }, Object { - "x": 1528524000000, - "y": 0, + "x": 1593901800000, + "y": 3, }, Object { - "x": 1528534800000, + "x": 1593902400000, "y": 0, }, Object { - "x": 1528545600000, + "x": 1593903000000, "y": 0, }, Object { - "x": 1528556400000, - "y": 0, + "x": 1593903600000, + "y": 1, }, Object { - "x": 1528567200000, + "x": 1593904200000, "y": 0, }, Object { - "x": 1528578000000, + "x": 1593904800000, "y": 0, }, Object { - "x": 1528588800000, + "x": 1593905400000, "y": 0, }, Object { - "x": 1528599600000, - "y": 0, + "x": 1593906000000, + "y": 2, }, Object { - "x": 1528610400000, + "x": 1593906600000, "y": 0, }, Object { - "x": 1528621200000, + "x": 1593907200000, "y": 0, }, Object { - "x": 1528632000000, + "x": 1593907800000, "y": 0, }, Object { - "x": 1528642800000, + "x": 1593908400000, "y": 0, }, Object { - "x": 1528653600000, + "x": 1593909000000, "y": 0, }, Object { - "x": 1528664400000, + "x": 1593909600000, "y": 0, }, Object { - "x": 1528675200000, + "x": 1593910200000, "y": 0, }, Object { - "x": 1528686000000, + "x": 1593910800000, "y": 0, }, Object { - "x": 1528696800000, + "x": 1593911400000, "y": 0, }, Object { - "x": 1528707600000, + "x": 1593912000000, "y": 0, }, Object { - "x": 1528718400000, + "x": 1593912600000, "y": 0, }, Object { - "x": 1528729200000, + "x": 1593913200000, "y": 0, }, Object { - "x": 1528740000000, + "x": 1593913800000, "y": 0, }, Object { - "x": 1528750800000, + "x": 1593914400000, "y": 0, }, Object { - "x": 1528761600000, + "x": 1593915000000, "y": 0, }, Object { - "x": 1528772400000, + "x": 1593915600000, "y": 0, }, Object { - "x": 1528783200000, + "x": 1593916200000, "y": 0, }, Object { - "x": 1528794000000, + "x": 1593916800000, + "y": 1.5, + }, + Object { + "x": 1593917400000, "y": 0, }, Object { - "x": 1528804800000, + "x": 1593918000000, "y": 0, }, Object { - "x": 1528815600000, + "x": 1593918600000, "y": 0, }, Object { - "x": 1528826400000, + "x": 1593919200000, "y": 0, }, Object { - "x": 1528837200000, + "x": 1593919800000, "y": 0, }, Object { - "x": 1528848000000, + "x": 1593920400000, "y": 0, }, Object { - "x": 1528858800000, + "x": 1593921000000, "y": 0, }, Object { - "x": 1528869600000, + "x": 1593921600000, "y": 0, }, Object { - "x": 1528880400000, + "x": 1593922200000, "y": 0, }, Object { - "x": 1528891200000, + "x": 1593922800000, "y": 0, }, Object { - "x": 1528902000000, + "x": 1593923400000, + "y": 1, + }, + Object { + "x": 1593924000000, "y": 0, }, Object { - "x": 1528912800000, + "x": 1593924600000, "y": 0, }, Object { - "x": 1528923600000, + "x": 1593925200000, "y": 0, }, Object { - "x": 1528934400000, - "y": 1077.5, + "x": 1593925800000, + "y": 0, }, Object { - "x": 1528945200000, + "x": 1593926400000, "y": 0, }, Object { - "x": 1528956000000, + "x": 1593927000000, "y": 0, }, Object { - "x": 1528966800000, + "x": 1593927600000, "y": 0, }, Object { - "x": 1528977600000, + "x": 1593928200000, "y": 0, }, - ], - "key": "HTTP 3xx", - }, - Object { - "avg": 8190.7, - "dataPoints": Array [ Object { - "x": 1528113600000, - "y": 593, + "x": 1593928800000, + "y": 0, }, Object { - "x": 1528124400000, - "y": 606.5, + "x": 1593929400000, + "y": 0, }, Object { - "x": 1528135200000, - "y": 602.5, + "x": 1593930000000, + "y": 1.5, }, Object { - "x": 1528146000000, - "y": 581, + "x": 1593930600000, + "y": 0, }, Object { - "x": 1528156800000, - "y": 619, + "x": 1593931200000, + "y": 0, }, Object { - "x": 1528167600000, - "y": 595.5, + "x": 1593931800000, + "y": 0, }, Object { - "x": 1528178400000, - "y": 637, + "x": 1593932400000, + "y": 1, }, Object { - "x": 1528189200000, - "y": 617, + "x": 1593933000000, + "y": 0, }, Object { - "x": 1528200000000, - "y": 582, + "x": 1593933600000, + "y": 0, }, Object { - "x": 1528210800000, - "y": 616.5, + "x": 1593934200000, + "y": 0, }, Object { - "x": 1528221600000, - "y": 611.5, + "x": 1593934800000, + "y": 0, }, Object { - "x": 1528232400000, - "y": 608, + "x": 1593935400000, + "y": 0, }, Object { - "x": 1528243200000, - "y": 600, + "x": 1593936000000, + "y": 0, }, Object { - "x": 1528254000000, - "y": 618.5, + "x": 1593936600000, + "y": 7.5, }, Object { - "x": 1528264800000, - "y": 615.5, + "x": 1593937200000, + "y": 14.5, }, Object { - "x": 1528275600000, - "y": 591, + "x": 1593937800000, + "y": 15.5, }, Object { - "x": 1528286400000, - "y": 562.5, + "x": 1593938400000, + "y": 9, }, + ], + "key": "HTTP 4xx", + }, + Object { + "avg": 37.8, + "dataPoints": Array [ Object { - "x": 1528297200000, - "y": 621.5, + "x": 1593852000000, + "y": 0, }, Object { - "x": 1528308000000, - "y": 623.5, + "x": 1593852600000, + "y": 0, }, Object { - "x": 1528318800000, - "y": 581.5, + "x": 1593853200000, + "y": 0, }, Object { - "x": 1528329600000, - "y": 610, + "x": 1593853800000, + "y": 0, }, Object { - "x": 1528340400000, - "y": 601, + "x": 1593854400000, + "y": 0, }, Object { - "x": 1528351200000, - "y": 596, + "x": 1593855000000, + "y": 0, }, Object { - "x": 1528362000000, - "y": 624, + "x": 1593855600000, + "y": 0, }, Object { - "x": 1528372800000, - "y": 594.5, + "x": 1593856200000, + "y": 0, }, Object { - "x": 1528383600000, - "y": 615, + "x": 1593856800000, + "y": 0, }, Object { - "x": 1528394400000, - "y": 603, + "x": 1593857400000, + "y": 0, }, Object { - "x": 1528405200000, - "y": 595, + "x": 1593858000000, + "y": 0, }, Object { - "x": 1528416000000, - "y": 616, + "x": 1593858600000, + "y": 0, }, Object { - "x": 1528426800000, - "y": 585.5, + "x": 1593859200000, + "y": 0, }, Object { - "x": 1528437600000, - "y": 616, + "x": 1593859800000, + "y": 0, }, Object { - "x": 1528448400000, - "y": 626.5, + "x": 1593860400000, + "y": 0, }, Object { - "x": 1528459200000, - "y": 625, + "x": 1593861000000, + "y": 0, }, Object { - "x": 1528470000000, - "y": 583.5, + "x": 1593861600000, + "y": 0, }, Object { - "x": 1528480800000, - "y": 629, + "x": 1593862200000, + "y": 0, }, Object { - "x": 1528491600000, - "y": 574, + "x": 1593862800000, + "y": 0, }, Object { - "x": 1528502400000, - "y": 142, + "x": 1593863400000, + "y": 0, }, Object { - "x": 1528513200000, - "y": 120, + "x": 1593864000000, + "y": 0, }, Object { - "x": 1528524000000, - "y": 136.5, + "x": 1593864600000, + "y": 0, }, Object { - "x": 1528534800000, - "y": 147.5, + "x": 1593865200000, + "y": 0, }, Object { - "x": 1528545600000, - "y": 140.5, + "x": 1593865800000, + "y": 0, }, Object { - "x": 1528556400000, - "y": 150, + "x": 1593866400000, + "y": 0, }, Object { - "x": 1528567200000, - "y": 132, + "x": 1593867000000, + "y": 0, }, Object { - "x": 1528578000000, - "y": 130, + "x": 1593867600000, + "y": 0, }, Object { - "x": 1528588800000, - "y": 139.5, + "x": 1593868200000, + "y": 0, }, Object { - "x": 1528599600000, - "y": 129.5, + "x": 1593868800000, + "y": 0, }, Object { - "x": 1528610400000, - "y": 145.5, + "x": 1593869400000, + "y": 0, }, Object { - "x": 1528621200000, - "y": 124, + "x": 1593870000000, + "y": 0, }, Object { - "x": 1528632000000, - "y": 155.5, + "x": 1593870600000, + "y": 0, }, Object { - "x": 1528642800000, - "y": 138.5, + "x": 1593871200000, + "y": 0, }, Object { - "x": 1528653600000, - "y": 139.5, + "x": 1593871800000, + "y": 0, }, Object { - "x": 1528664400000, - "y": 137.5, + "x": 1593872400000, + "y": 0, }, Object { - "x": 1528675200000, - "y": 583.5, + "x": 1593873000000, + "y": 0, }, Object { - "x": 1528686000000, - "y": 635, + "x": 1593873600000, + "y": 0, }, Object { - "x": 1528696800000, - "y": 581.5, + "x": 1593874200000, + "y": 0, }, Object { - "x": 1528707600000, - "y": 577.5, + "x": 1593874800000, + "y": 0, }, Object { - "x": 1528718400000, - "y": 608.5, + "x": 1593875400000, + "y": 0, }, Object { - "x": 1528729200000, - "y": 613.5, + "x": 1593876000000, + "y": 0, }, Object { - "x": 1528740000000, - "y": 597, + "x": 1593876600000, + "y": 0, }, Object { - "x": 1528750800000, - "y": 576.5, + "x": 1593877200000, + "y": 0, }, Object { - "x": 1528761600000, - "y": 605.5, + "x": 1593877800000, + "y": 0, }, Object { - "x": 1528772400000, - "y": 601.5, + "x": 1593878400000, + "y": 0, }, Object { - "x": 1528783200000, - "y": 634.5, + "x": 1593879000000, + "y": 0, }, Object { - "x": 1528794000000, - "y": 598.5, + "x": 1593879600000, + "y": 0, }, Object { - "x": 1528804800000, - "y": 592, + "x": 1593880200000, + "y": 0, }, Object { - "x": 1528815600000, - "y": 588, + "x": 1593880800000, + "y": 0, }, Object { - "x": 1528826400000, - "y": 581, + "x": 1593881400000, + "y": 0, }, Object { - "x": 1528837200000, - "y": 635, + "x": 1593882000000, + "y": 0, }, Object { - "x": 1528848000000, - "y": 612, + "x": 1593882600000, + "y": 0, }, Object { - "x": 1528858800000, - "y": 627.5, + "x": 1593883200000, + "y": 0, }, Object { - "x": 1528869600000, - "y": 603.5, + "x": 1593883800000, + "y": 0, }, Object { - "x": 1528880400000, - "y": 603, + "x": 1593884400000, + "y": 0, }, Object { - "x": 1528891200000, - "y": 627, + "x": 1593885000000, + "y": 0, }, Object { - "x": 1528902000000, - "y": 608, + "x": 1593885600000, + "y": 0, }, Object { - "x": 1528912800000, - "y": 631.5, + "x": 1593886200000, + "y": 0, }, Object { - "x": 1528923600000, - "y": 638.5, + "x": 1593886800000, + "y": 0, }, Object { - "x": 1528934400000, - "y": 591.5, + "x": 1593887400000, + "y": 0, }, Object { - "x": 1528945200000, - "y": 610.5, + "x": 1593888000000, + "y": 0, }, Object { - "x": 1528956000000, - "y": 599, + "x": 1593888600000, + "y": 2.5, }, Object { - "x": 1528966800000, - "y": 607, + "x": 1593889200000, + "y": 7.5, }, Object { - "x": 1528977600000, - "y": 0, + "x": 1593889800000, + "y": 6, }, - ], - "key": "HTTP 4xx", - }, - Object { - "avg": 8203.6, - "dataPoints": Array [ Object { - "x": 1528113600000, - "y": 604.5, + "x": 1593890400000, + "y": 8, }, Object { - "x": 1528124400000, - "y": 601.5, + "x": 1593891000000, + "y": 6, }, Object { - "x": 1528135200000, - "y": 598, + "x": 1593891600000, + "y": 8, }, Object { - "x": 1528146000000, - "y": 615, + "x": 1593892200000, + "y": 11.5, }, Object { - "x": 1528156800000, - "y": 616.5, + "x": 1593892800000, + "y": 5, }, Object { - "x": 1528167600000, - "y": 636, + "x": 1593893400000, + "y": 5, }, Object { - "x": 1528178400000, - "y": 609, + "x": 1593894000000, + "y": 9, }, Object { - "x": 1528189200000, - "y": 608.5, + "x": 1593894600000, + "y": 5, }, Object { - "x": 1528200000000, - "y": 617.5, + "x": 1593895200000, + "y": 8.5, }, Object { - "x": 1528210800000, - "y": 624.5, + "x": 1593895800000, + "y": 6.5, }, Object { - "x": 1528221600000, - "y": 579, + "x": 1593896400000, + "y": 9, }, Object { - "x": 1528232400000, - "y": 607.5, + "x": 1593897000000, + "y": 8.5, }, Object { - "x": 1528243200000, - "y": 595.5, + "x": 1593897600000, + "y": 8.5, }, Object { - "x": 1528254000000, - "y": 617.5, + "x": 1593898200000, + "y": 5.5, }, Object { - "x": 1528264800000, - "y": 606, + "x": 1593898800000, + "y": 5, }, Object { - "x": 1528275600000, - "y": 590, + "x": 1593899400000, + "y": 9, }, Object { - "x": 1528286400000, - "y": 545.5, + "x": 1593900000000, + "y": 8, }, Object { - "x": 1528297200000, - "y": 588, + "x": 1593900600000, + "y": 6, }, Object { - "x": 1528308000000, - "y": 621.5, + "x": 1593901200000, + "y": 9, }, Object { - "x": 1528318800000, - "y": 604, + "x": 1593901800000, + "y": 4, }, Object { - "x": 1528329600000, - "y": 601, + "x": 1593902400000, + "y": 0, }, Object { - "x": 1528340400000, - "y": 644, + "x": 1593903000000, + "y": 0, }, Object { - "x": 1528351200000, - "y": 620.5, + "x": 1593903600000, + "y": 0, }, Object { - "x": 1528362000000, - "y": 607.5, + "x": 1593904200000, + "y": 0, }, Object { - "x": 1528372800000, - "y": 576, + "x": 1593904800000, + "y": 0, }, Object { - "x": 1528383600000, - "y": 620.5, + "x": 1593905400000, + "y": 0, }, Object { - "x": 1528394400000, - "y": 588.5, + "x": 1593906000000, + "y": 0.5, }, Object { - "x": 1528405200000, - "y": 621.5, + "x": 1593906600000, + "y": 0, }, Object { - "x": 1528416000000, - "y": 627.5, + "x": 1593907200000, + "y": 0, }, Object { - "x": 1528426800000, - "y": 594.5, + "x": 1593907800000, + "y": 0, }, Object { - "x": 1528437600000, - "y": 591.5, + "x": 1593908400000, + "y": 0, }, Object { - "x": 1528448400000, - "y": 607.5, + "x": 1593909000000, + "y": 0, }, Object { - "x": 1528459200000, - "y": 641, + "x": 1593909600000, + "y": 0, }, Object { - "x": 1528470000000, - "y": 588.5, + "x": 1593910200000, + "y": 1.5, }, Object { - "x": 1528480800000, - "y": 599.5, + "x": 1593910800000, + "y": 0, }, Object { - "x": 1528491600000, - "y": 617, + "x": 1593911400000, + "y": 0, }, Object { - "x": 1528502400000, - "y": 142, + "x": 1593912000000, + "y": 0, }, Object { - "x": 1528513200000, - "y": 153.5, + "x": 1593912600000, + "y": 0, }, Object { - "x": 1528524000000, - "y": 141.5, + "x": 1593913200000, + "y": 0, }, Object { - "x": 1528534800000, - "y": 151.5, + "x": 1593913800000, + "y": 0, }, Object { - "x": 1528545600000, - "y": 163, + "x": 1593914400000, + "y": 0, }, Object { - "x": 1528556400000, - "y": 134.5, + "x": 1593915000000, + "y": 0, }, Object { - "x": 1528567200000, - "y": 148.5, + "x": 1593915600000, + "y": 0, }, Object { - "x": 1528578000000, - "y": 139, + "x": 1593916200000, + "y": 0, }, Object { - "x": 1528588800000, - "y": 144.5, + "x": 1593916800000, + "y": 1.5, }, Object { - "x": 1528599600000, - "y": 136, + "x": 1593917400000, + "y": 0, }, Object { - "x": 1528610400000, - "y": 139.5, + "x": 1593918000000, + "y": 0, }, Object { - "x": 1528621200000, - "y": 119, + "x": 1593918600000, + "y": 0, }, Object { - "x": 1528632000000, - "y": 144, + "x": 1593919200000, + "y": 0, }, Object { - "x": 1528642800000, - "y": 129, + "x": 1593919800000, + "y": 0, }, Object { - "x": 1528653600000, - "y": 132, + "x": 1593920400000, + "y": 0, }, Object { - "x": 1528664400000, - "y": 148, + "x": 1593921000000, + "y": 0, }, Object { - "x": 1528675200000, - "y": 606.5, + "x": 1593921600000, + "y": 0, }, Object { - "x": 1528686000000, - "y": 627, + "x": 1593922200000, + "y": 0, }, Object { - "x": 1528696800000, - "y": 567.5, + "x": 1593922800000, + "y": 0, }, Object { - "x": 1528707600000, - "y": 620, + "x": 1593923400000, + "y": 1, }, Object { - "x": 1528718400000, - "y": 607.5, + "x": 1593924000000, + "y": 0, }, Object { - "x": 1528729200000, - "y": 619.5, + "x": 1593924600000, + "y": 0, }, Object { - "x": 1528740000000, - "y": 604.5, + "x": 1593925200000, + "y": 0, }, Object { - "x": 1528750800000, - "y": 604, + "x": 1593925800000, + "y": 0, }, Object { - "x": 1528761600000, - "y": 588, + "x": 1593926400000, + "y": 0, }, Object { - "x": 1528772400000, - "y": 603.5, + "x": 1593927000000, + "y": 0, }, Object { - "x": 1528783200000, - "y": 599, + "x": 1593927600000, + "y": 0, }, Object { - "x": 1528794000000, - "y": 582.5, + "x": 1593928200000, + "y": 0, }, Object { - "x": 1528804800000, - "y": 594, + "x": 1593928800000, + "y": 0, }, Object { - "x": 1528815600000, - "y": 622.5, + "x": 1593929400000, + "y": 0, }, Object { - "x": 1528826400000, - "y": 619, + "x": 1593930000000, + "y": 1, }, Object { - "x": 1528837200000, - "y": 641.5, + "x": 1593930600000, + "y": 0, }, Object { - "x": 1528848000000, - "y": 599, + "x": 1593931200000, + "y": 0, }, Object { - "x": 1528858800000, - "y": 586, + "x": 1593931800000, + "y": 0, }, Object { - "x": 1528869600000, - "y": 614.5, + "x": 1593932400000, + "y": 0.5, }, Object { - "x": 1528880400000, - "y": 619.5, + "x": 1593933000000, + "y": 0, }, Object { - "x": 1528891200000, - "y": 615.5, + "x": 1593933600000, + "y": 0, }, Object { - "x": 1528902000000, - "y": 624, + "x": 1593934200000, + "y": 0, }, Object { - "x": 1528912800000, - "y": 610, + "x": 1593934800000, + "y": 0, }, Object { - "x": 1528923600000, - "y": 612, + "x": 1593935400000, + "y": 0, }, Object { - "x": 1528934400000, - "y": 544, + "x": 1593936000000, + "y": 0, }, Object { - "x": 1528945200000, - "y": 617.5, + "x": 1593936600000, + "y": 3, }, Object { - "x": 1528956000000, - "y": 580.5, + "x": 1593937200000, + "y": 8.5, }, Object { - "x": 1528966800000, - "y": 591.5, + "x": 1593937800000, + "y": 8, }, Object { - "x": 1528977600000, - "y": 0, + "x": 1593938400000, + "y": 2.5, }, ], "key": "HTTP 5xx", diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts index a7a740a239ea7..fdbd99bf274d6 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts @@ -47,6 +47,7 @@ describe('timeseriesFetcher', () => { apmCustomLinkIndex: 'myIndex', }, }, + searchAggregatedTransactions: false, }); }); diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts index f39529b59caa6..5a3948f577430 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ProcessorEvent } from '../../../../../common/processor_event'; import { ESFilter } from '../../../../../typings/elasticsearch'; import { SERVICE_NAME, - TRANSACTION_DURATION, TRANSACTION_NAME, TRANSACTION_RESULT, TRANSACTION_TYPE, @@ -21,6 +19,11 @@ import { SetupTimeRange, SetupUIFilters, } from '../../../helpers/setup_request'; +import { + getProcessorEventForAggregatedTransactions, + getTransactionDurationFieldForAggregatedTransactions, + getDocumentTypeFilterForAggregatedTransactions, +} from '../../../helpers/aggregated_transactions'; export type ESResponse = PromiseReturnType; export function timeseriesFetcher({ @@ -28,11 +31,13 @@ export function timeseriesFetcher({ transactionType, transactionName, setup, + searchAggregatedTransactions, }: { serviceName: string; transactionType: string | undefined; transactionName: string | undefined; setup: Setup & SetupTimeRange & SetupUIFilters; + searchAggregatedTransactions: boolean; }) { const { start, end, uiFiltersES, apmEventClient } = setup; const { intervalString } = getBucketSize(start, end); @@ -40,6 +45,9 @@ export function timeseriesFetcher({ const filter: ESFilter[] = [ { term: { [SERVICE_NAME]: serviceName } }, { range: rangeFilter(start, end) }, + ...getDocumentTypeFilterForAggregatedTransactions( + searchAggregatedTransactions + ), ...uiFiltersES, ]; @@ -54,7 +62,11 @@ export function timeseriesFetcher({ const params = { apm: { - events: [ProcessorEvent.transaction as const], + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], }, body: { size: 0, @@ -68,17 +80,31 @@ export function timeseriesFetcher({ extended_bounds: { min: start, max: end }, }, aggs: { - avg: { avg: { field: TRANSACTION_DURATION } }, + avg: { + avg: { + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), + }, + }, pct: { percentiles: { - field: TRANSACTION_DURATION, + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), percents: [95, 99], hdr: { number_of_significant_value_digits: 2 }, }, }, }, }, - overall_avg_duration: { avg: { field: TRANSACTION_DURATION } }, + overall_avg_duration: { + avg: { + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), + }, + }, transaction_results: { terms: { field: TRANSACTION_RESULT, missing: '' }, aggs: { @@ -89,6 +115,15 @@ export function timeseriesFetcher({ min_doc_count: 0, extended_bounds: { min: start, max: end }, }, + aggs: { + count: { + value_count: { + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), + }, + }, + }, }, }, }, diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts index ea06bd57bfff2..81dca447f16ca 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts @@ -18,6 +18,7 @@ export async function getApmTimeseriesData(options: { transactionType: string | undefined; transactionName: string | undefined; setup: Setup & SetupTimeRange & SetupUIFilters; + searchAggregatedTransactions: boolean; }) { const { start, end } = options.setup; const { bucketSize } = getBucketSize(start, end); diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/mock_responses/timeseries_response.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/mock_responses/timeseries_response.ts index cc30b256a4ffb..67084f8a42536 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/mock_responses/timeseries_response.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/mock_responses/timeseries_response.ts @@ -7,17 +7,20 @@ import { ESResponse } from '../fetcher'; export const timeseriesResponse = ({ - took: 368, + took: 206, timed_out: false, _shards: { - total: 90, - successful: 90, + total: 9, + successful: 9, skipped: 0, failed: 0, }, hits: { - total: 1297673, - max_score: 0, + total: { + value: 10000, + relation: 'gte', + }, + max_score: null, hits: [], }, aggregations: { @@ -32,2798 +35,6714 @@ export const timeseriesResponse = ({ }, { key: 'HTTP 2xx', - doc_count: 1127080, + doc_count: 12150, timeseries: { buckets: [ { - key_as_string: '2018-06-04T12:00:00.000Z', - key: 1528113600000, - doc_count: 16446, + key_as_string: '2020-07-04T08:40:00.000Z', + key: 1593852000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T08:50:00.000Z', + key: 1593852600000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T09:00:00.000Z', + key: 1593853200000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T09:10:00.000Z', + key: 1593853800000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T09:20:00.000Z', + key: 1593854400000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-04T15:00:00.000Z', - key: 1528124400000, - doc_count: 16292, + key_as_string: '2020-07-04T09:30:00.000Z', + key: 1593855000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-04T18:00:00.000Z', - key: 1528135200000, - doc_count: 16464, + key_as_string: '2020-07-04T09:40:00.000Z', + key: 1593855600000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-04T21:00:00.000Z', - key: 1528146000000, - doc_count: 16497, + key_as_string: '2020-07-04T09:50:00.000Z', + key: 1593856200000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-05T00:00:00.000Z', - key: 1528156800000, - doc_count: 16799, + key_as_string: '2020-07-04T10:00:00.000Z', + key: 1593856800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-05T03:00:00.000Z', - key: 1528167600000, - doc_count: 16561, + key_as_string: '2020-07-04T10:10:00.000Z', + key: 1593857400000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-05T06:00:00.000Z', - key: 1528178400000, - doc_count: 16431, + key_as_string: '2020-07-04T10:20:00.000Z', + key: 1593858000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-05T09:00:00.000Z', - key: 1528189200000, - doc_count: 16383, + key_as_string: '2020-07-04T10:30:00.000Z', + key: 1593858600000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-05T12:00:00.000Z', - key: 1528200000000, - doc_count: 16295, + key_as_string: '2020-07-04T10:40:00.000Z', + key: 1593859200000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-05T15:00:00.000Z', - key: 1528210800000, - doc_count: 16702, + key_as_string: '2020-07-04T10:50:00.000Z', + key: 1593859800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-05T18:00:00.000Z', - key: 1528221600000, - doc_count: 16469, + key_as_string: '2020-07-04T11:00:00.000Z', + key: 1593860400000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-05T21:00:00.000Z', - key: 1528232400000, - doc_count: 16466, + key_as_string: '2020-07-04T11:10:00.000Z', + key: 1593861000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-06T00:00:00.000Z', - key: 1528243200000, - doc_count: 16551, + key_as_string: '2020-07-04T11:20:00.000Z', + key: 1593861600000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-06T03:00:00.000Z', - key: 1528254000000, - doc_count: 16675, + key_as_string: '2020-07-04T11:30:00.000Z', + key: 1593862200000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-06T06:00:00.000Z', - key: 1528264800000, - doc_count: 16410, + key_as_string: '2020-07-04T11:40:00.000Z', + key: 1593862800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-06T09:00:00.000Z', - key: 1528275600000, - doc_count: 16247, + key_as_string: '2020-07-04T11:50:00.000Z', + key: 1593863400000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-06T12:00:00.000Z', - key: 1528286400000, - doc_count: 15145, + key_as_string: '2020-07-04T12:00:00.000Z', + key: 1593864000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-06T15:00:00.000Z', - key: 1528297200000, - doc_count: 16178, + key_as_string: '2020-07-04T12:10:00.000Z', + key: 1593864600000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-06T18:00:00.000Z', - key: 1528308000000, - doc_count: 16530, + key_as_string: '2020-07-04T12:20:00.000Z', + key: 1593865200000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-06T21:00:00.000Z', - key: 1528318800000, - doc_count: 16211, + key_as_string: '2020-07-04T12:30:00.000Z', + key: 1593865800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-07T00:00:00.000Z', - key: 1528329600000, - doc_count: 16453, + key_as_string: '2020-07-04T12:40:00.000Z', + key: 1593866400000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-07T03:00:00.000Z', - key: 1528340400000, - doc_count: 16503, + key_as_string: '2020-07-04T12:50:00.000Z', + key: 1593867000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-07T06:00:00.000Z', - key: 1528351200000, - doc_count: 16604, + key_as_string: '2020-07-04T13:00:00.000Z', + key: 1593867600000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-07T09:00:00.000Z', - key: 1528362000000, - doc_count: 16522, + key_as_string: '2020-07-04T13:10:00.000Z', + key: 1593868200000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-07T12:00:00.000Z', - key: 1528372800000, - doc_count: 16164, + key_as_string: '2020-07-04T13:20:00.000Z', + key: 1593868800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-07T15:00:00.000Z', - key: 1528383600000, - doc_count: 16520, + key_as_string: '2020-07-04T13:30:00.000Z', + key: 1593869400000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-07T18:00:00.000Z', - key: 1528394400000, - doc_count: 16534, + key_as_string: '2020-07-04T13:40:00.000Z', + key: 1593870000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-07T21:00:00.000Z', - key: 1528405200000, - doc_count: 16311, + key_as_string: '2020-07-04T13:50:00.000Z', + key: 1593870600000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-08T00:00:00.000Z', - key: 1528416000000, - doc_count: 16670, + key_as_string: '2020-07-04T14:00:00.000Z', + key: 1593871200000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-08T03:00:00.000Z', - key: 1528426800000, - doc_count: 16192, + key_as_string: '2020-07-04T14:10:00.000Z', + key: 1593871800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-08T06:00:00.000Z', - key: 1528437600000, - doc_count: 16579, + key_as_string: '2020-07-04T14:20:00.000Z', + key: 1593872400000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-08T09:00:00.000Z', - key: 1528448400000, - doc_count: 16330, + key_as_string: '2020-07-04T14:30:00.000Z', + key: 1593873000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-08T12:00:00.000Z', - key: 1528459200000, - doc_count: 16565, + key_as_string: '2020-07-04T14:40:00.000Z', + key: 1593873600000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-08T15:00:00.000Z', - key: 1528470000000, - doc_count: 16543, + key_as_string: '2020-07-04T14:50:00.000Z', + key: 1593874200000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-08T18:00:00.000Z', - key: 1528480800000, - doc_count: 16492, + key_as_string: '2020-07-04T15:00:00.000Z', + key: 1593874800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-08T21:00:00.000Z', - key: 1528491600000, - doc_count: 16404, + key_as_string: '2020-07-04T15:10:00.000Z', + key: 1593875400000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-09T00:00:00.000Z', - key: 1528502400000, - doc_count: 4528, + key_as_string: '2020-07-04T15:20:00.000Z', + key: 1593876000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-09T03:00:00.000Z', - key: 1528513200000, - doc_count: 4557, + key_as_string: '2020-07-04T15:30:00.000Z', + key: 1593876600000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-09T06:00:00.000Z', - key: 1528524000000, - doc_count: 4566, + key_as_string: '2020-07-04T15:40:00.000Z', + key: 1593877200000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-09T09:00:00.000Z', - key: 1528534800000, - doc_count: 4586, + key_as_string: '2020-07-04T15:50:00.000Z', + key: 1593877800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-09T12:00:00.000Z', - key: 1528545600000, - doc_count: 4672, + key_as_string: '2020-07-04T16:00:00.000Z', + key: 1593878400000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-09T15:00:00.000Z', - key: 1528556400000, - doc_count: 4685, + key_as_string: '2020-07-04T16:10:00.000Z', + key: 1593879000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-09T18:00:00.000Z', - key: 1528567200000, - doc_count: 4521, + key_as_string: '2020-07-04T16:20:00.000Z', + key: 1593879600000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-09T21:00:00.000Z', - key: 1528578000000, - doc_count: 4612, + key_as_string: '2020-07-04T16:30:00.000Z', + key: 1593880200000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-10T00:00:00.000Z', - key: 1528588800000, - doc_count: 4535, + key_as_string: '2020-07-04T16:40:00.000Z', + key: 1593880800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-10T03:00:00.000Z', - key: 1528599600000, - doc_count: 4606, + key_as_string: '2020-07-04T16:50:00.000Z', + key: 1593881400000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-10T06:00:00.000Z', - key: 1528610400000, - doc_count: 4614, + key_as_string: '2020-07-04T17:00:00.000Z', + key: 1593882000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-10T09:00:00.000Z', - key: 1528621200000, - doc_count: 4507, + key_as_string: '2020-07-04T17:10:00.000Z', + key: 1593882600000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-10T12:00:00.000Z', - key: 1528632000000, - doc_count: 4611, + key_as_string: '2020-07-04T17:20:00.000Z', + key: 1593883200000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-10T15:00:00.000Z', - key: 1528642800000, - doc_count: 4587, + key_as_string: '2020-07-04T17:30:00.000Z', + key: 1593883800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-10T18:00:00.000Z', - key: 1528653600000, - doc_count: 4582, + key_as_string: '2020-07-04T17:40:00.000Z', + key: 1593884400000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-10T21:00:00.000Z', - key: 1528664400000, - doc_count: 4615, + key_as_string: '2020-07-04T17:50:00.000Z', + key: 1593885000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-11T00:00:00.000Z', - key: 1528675200000, - doc_count: 16251, + key_as_string: '2020-07-04T18:00:00.000Z', + key: 1593885600000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-11T03:00:00.000Z', - key: 1528686000000, - doc_count: 16825, + key_as_string: '2020-07-04T18:10:00.000Z', + key: 1593886200000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-11T06:00:00.000Z', - key: 1528696800000, - doc_count: 16288, + key_as_string: '2020-07-04T18:20:00.000Z', + key: 1593886800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-11T09:00:00.000Z', - key: 1528707600000, - doc_count: 16492, + key_as_string: '2020-07-04T18:30:00.000Z', + key: 1593887400000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-11T12:00:00.000Z', - key: 1528718400000, - doc_count: 16434, + key_as_string: '2020-07-04T18:40:00.000Z', + key: 1593888000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-11T15:00:00.000Z', - key: 1528729200000, - doc_count: 17003, + key_as_string: '2020-07-04T18:50:00.000Z', + key: 1593888600000, + doc_count: 169, + count: { + value: 169, + }, }, { - key_as_string: '2018-06-11T18:00:00.000Z', - key: 1528740000000, - doc_count: 16364, + key_as_string: '2020-07-04T19:00:00.000Z', + key: 1593889200000, + doc_count: 444, + count: { + value: 444, + }, }, { - key_as_string: '2018-06-11T21:00:00.000Z', - key: 1528750800000, - doc_count: 16645, + key_as_string: '2020-07-04T19:10:00.000Z', + key: 1593889800000, + doc_count: 460, + count: { + value: 460, + }, }, { - key_as_string: '2018-06-12T00:00:00.000Z', - key: 1528761600000, - doc_count: 16695, + key_as_string: '2020-07-04T19:20:00.000Z', + key: 1593890400000, + doc_count: 506, + count: { + value: 506, + }, }, { - key_as_string: '2018-06-12T03:00:00.000Z', - key: 1528772400000, - doc_count: 16498, + key_as_string: '2020-07-04T19:30:00.000Z', + key: 1593891000000, + doc_count: 479, + count: { + value: 479, + }, }, { - key_as_string: '2018-06-12T06:00:00.000Z', - key: 1528783200000, - doc_count: 16588, + key_as_string: '2020-07-04T19:40:00.000Z', + key: 1593891600000, + doc_count: 457, + count: { + value: 457, + }, }, { - key_as_string: '2018-06-12T09:00:00.000Z', - key: 1528794000000, - doc_count: 16685, + key_as_string: '2020-07-04T19:50:00.000Z', + key: 1593892200000, + doc_count: 514, + count: { + value: 514, + }, }, { - key_as_string: '2018-06-12T12:00:00.000Z', - key: 1528804800000, - doc_count: 16361, + key_as_string: '2020-07-04T20:00:00.000Z', + key: 1593892800000, + doc_count: 482, + count: { + value: 482, + }, }, { - key_as_string: '2018-06-12T15:00:00.000Z', - key: 1528815600000, - doc_count: 16658, + key_as_string: '2020-07-04T20:10:00.000Z', + key: 1593893400000, + doc_count: 504, + count: { + value: 504, + }, }, { - key_as_string: '2018-06-12T18:00:00.000Z', - key: 1528826400000, - doc_count: 16507, + key_as_string: '2020-07-04T20:20:00.000Z', + key: 1593894000000, + doc_count: 532, + count: { + value: 532, + }, }, { - key_as_string: '2018-06-12T21:00:00.000Z', - key: 1528837200000, - doc_count: 16418, + key_as_string: '2020-07-04T20:30:00.000Z', + key: 1593894600000, + doc_count: 458, + count: { + value: 458, + }, }, { - key_as_string: '2018-06-13T00:00:00.000Z', - key: 1528848000000, - doc_count: 16477, + key_as_string: '2020-07-04T20:40:00.000Z', + key: 1593895200000, + doc_count: 448, + count: { + value: 448, + }, }, { - key_as_string: '2018-06-13T03:00:00.000Z', - key: 1528858800000, - doc_count: 16755, + key_as_string: '2020-07-04T20:50:00.000Z', + key: 1593895800000, + doc_count: 468, + count: { + value: 468, + }, }, { - key_as_string: '2018-06-13T06:00:00.000Z', - key: 1528869600000, - doc_count: 16594, + key_as_string: '2020-07-04T21:00:00.000Z', + key: 1593896400000, + doc_count: 526, + count: { + value: 526, + }, }, { - key_as_string: '2018-06-13T09:00:00.000Z', - key: 1528880400000, - doc_count: 16812, + key_as_string: '2020-07-04T21:10:00.000Z', + key: 1593897000000, + doc_count: 495, + count: { + value: 495, + }, }, { - key_as_string: '2018-06-13T12:00:00.000Z', - key: 1528891200000, - doc_count: 16863, + key_as_string: '2020-07-04T21:20:00.000Z', + key: 1593897600000, + doc_count: 492, + count: { + value: 492, + }, }, { - key_as_string: '2018-06-13T15:00:00.000Z', - key: 1528902000000, - doc_count: 16655, + key_as_string: '2020-07-04T21:30:00.000Z', + key: 1593898200000, + doc_count: 487, + count: { + value: 487, + }, }, { - key_as_string: '2018-06-13T18:00:00.000Z', - key: 1528912800000, - doc_count: 16723, + key_as_string: '2020-07-04T21:40:00.000Z', + key: 1593898800000, + doc_count: 491, + count: { + value: 491, + }, }, { - key_as_string: '2018-06-13T21:00:00.000Z', - key: 1528923600000, - doc_count: 16577, + key_as_string: '2020-07-04T21:50:00.000Z', + key: 1593899400000, + doc_count: 486, + count: { + value: 486, + }, }, { - key_as_string: '2018-06-14T00:00:00.000Z', - key: 1528934400000, - doc_count: 15125, + key_as_string: '2020-07-04T22:00:00.000Z', + key: 1593900000000, + doc_count: 458, + count: { + value: 458, + }, }, { - key_as_string: '2018-06-14T03:00:00.000Z', - key: 1528945200000, - doc_count: 16432, + key_as_string: '2020-07-04T22:10:00.000Z', + key: 1593900600000, + doc_count: 528, + count: { + value: 528, + }, }, { - key_as_string: '2018-06-14T06:00:00.000Z', - key: 1528956000000, - doc_count: 16464, + key_as_string: '2020-07-04T22:20:00.000Z', + key: 1593901200000, + doc_count: 467, + count: { + value: 467, + }, }, { - key_as_string: '2018-06-14T09:00:00.000Z', - key: 1528966800000, - doc_count: 16369, + key_as_string: '2020-07-04T22:30:00.000Z', + key: 1593901800000, + doc_count: 179, + count: { + value: 179, + }, }, { - key_as_string: '2018-06-14T12:00:00.000Z', - key: 1528977600000, + key_as_string: '2020-07-04T22:40:00.000Z', + key: 1593902400000, doc_count: 0, + count: { + value: 0, + }, }, - ], - }, - }, - { - key: 'HTTP 5xx', - doc_count: 82036, - timeseries: { - buckets: [ { - key_as_string: '2018-06-04T12:00:00.000Z', - key: 1528113600000, - doc_count: 1209, + key_as_string: '2020-07-04T22:50:00.000Z', + key: 1593903000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-04T15:00:00.000Z', - key: 1528124400000, - doc_count: 1203, + key_as_string: '2020-07-04T23:00:00.000Z', + key: 1593903600000, + doc_count: 39, + count: { + value: 39, + }, }, { - key_as_string: '2018-06-04T18:00:00.000Z', - key: 1528135200000, - doc_count: 1196, + key_as_string: '2020-07-04T23:10:00.000Z', + key: 1593904200000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-04T21:00:00.000Z', - key: 1528146000000, - doc_count: 1230, + key_as_string: '2020-07-04T23:20:00.000Z', + key: 1593904800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-05T00:00:00.000Z', - key: 1528156800000, - doc_count: 1233, + key_as_string: '2020-07-04T23:30:00.000Z', + key: 1593905400000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-05T03:00:00.000Z', - key: 1528167600000, - doc_count: 1272, + key_as_string: '2020-07-04T23:40:00.000Z', + key: 1593906000000, + doc_count: 36, + count: { + value: 36, + }, }, { - key_as_string: '2018-06-05T06:00:00.000Z', - key: 1528178400000, - doc_count: 1218, + key_as_string: '2020-07-04T23:50:00.000Z', + key: 1593906600000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-05T09:00:00.000Z', - key: 1528189200000, - doc_count: 1217, + key_as_string: '2020-07-05T00:00:00.000Z', + key: 1593907200000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-05T12:00:00.000Z', - key: 1528200000000, - doc_count: 1235, + key_as_string: '2020-07-05T00:10:00.000Z', + key: 1593907800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-05T15:00:00.000Z', - key: 1528210800000, - doc_count: 1249, + key_as_string: '2020-07-05T00:20:00.000Z', + key: 1593908400000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-05T18:00:00.000Z', - key: 1528221600000, - doc_count: 1158, + key_as_string: '2020-07-05T00:30:00.000Z', + key: 1593909000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-05T21:00:00.000Z', - key: 1528232400000, - doc_count: 1215, + key_as_string: '2020-07-05T00:40:00.000Z', + key: 1593909600000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-06T00:00:00.000Z', - key: 1528243200000, - doc_count: 1191, + key_as_string: '2020-07-05T00:50:00.000Z', + key: 1593910200000, + doc_count: 34, + count: { + value: 34, + }, }, { - key_as_string: '2018-06-06T03:00:00.000Z', - key: 1528254000000, - doc_count: 1235, + key_as_string: '2020-07-05T01:00:00.000Z', + key: 1593910800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-06T06:00:00.000Z', - key: 1528264800000, - doc_count: 1212, + key_as_string: '2020-07-05T01:10:00.000Z', + key: 1593911400000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-06T09:00:00.000Z', - key: 1528275600000, - doc_count: 1180, + key_as_string: '2020-07-05T01:20:00.000Z', + key: 1593912000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-06T12:00:00.000Z', - key: 1528286400000, - doc_count: 1091, + key_as_string: '2020-07-05T01:30:00.000Z', + key: 1593912600000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-06T15:00:00.000Z', - key: 1528297200000, - doc_count: 1176, + key_as_string: '2020-07-05T01:40:00.000Z', + key: 1593913200000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-06T18:00:00.000Z', - key: 1528308000000, - doc_count: 1243, + key_as_string: '2020-07-05T01:50:00.000Z', + key: 1593913800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-06T21:00:00.000Z', - key: 1528318800000, - doc_count: 1208, + key_as_string: '2020-07-05T02:00:00.000Z', + key: 1593914400000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-07T00:00:00.000Z', - key: 1528329600000, - doc_count: 1202, + key_as_string: '2020-07-05T02:10:00.000Z', + key: 1593915000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-07T03:00:00.000Z', - key: 1528340400000, - doc_count: 1288, + key_as_string: '2020-07-05T02:20:00.000Z', + key: 1593915600000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-07T06:00:00.000Z', - key: 1528351200000, - doc_count: 1241, + key_as_string: '2020-07-05T02:30:00.000Z', + key: 1593916200000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-07T09:00:00.000Z', - key: 1528362000000, - doc_count: 1215, + key_as_string: '2020-07-05T02:40:00.000Z', + key: 1593916800000, + doc_count: 31, + count: { + value: 31, + }, }, { - key_as_string: '2018-06-07T12:00:00.000Z', - key: 1528372800000, - doc_count: 1152, + key_as_string: '2020-07-05T02:50:00.000Z', + key: 1593917400000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-07T15:00:00.000Z', - key: 1528383600000, - doc_count: 1241, + key_as_string: '2020-07-05T03:00:00.000Z', + key: 1593918000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-07T18:00:00.000Z', - key: 1528394400000, - doc_count: 1177, + key_as_string: '2020-07-05T03:10:00.000Z', + key: 1593918600000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-07T21:00:00.000Z', - key: 1528405200000, - doc_count: 1243, + key_as_string: '2020-07-05T03:20:00.000Z', + key: 1593919200000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-08T00:00:00.000Z', - key: 1528416000000, - doc_count: 1255, + key_as_string: '2020-07-05T03:30:00.000Z', + key: 1593919800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-08T03:00:00.000Z', - key: 1528426800000, - doc_count: 1189, + key_as_string: '2020-07-05T03:40:00.000Z', + key: 1593920400000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-08T06:00:00.000Z', - key: 1528437600000, - doc_count: 1183, + key_as_string: '2020-07-05T03:50:00.000Z', + key: 1593921000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-08T09:00:00.000Z', - key: 1528448400000, - doc_count: 1215, + key_as_string: '2020-07-05T04:00:00.000Z', + key: 1593921600000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-08T12:00:00.000Z', - key: 1528459200000, - doc_count: 1282, + key_as_string: '2020-07-05T04:10:00.000Z', + key: 1593922200000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-08T15:00:00.000Z', - key: 1528470000000, - doc_count: 1177, + key_as_string: '2020-07-05T04:20:00.000Z', + key: 1593922800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-08T18:00:00.000Z', - key: 1528480800000, - doc_count: 1199, + key_as_string: '2020-07-05T04:30:00.000Z', + key: 1593923400000, + doc_count: 49, + count: { + value: 49, + }, }, { - key_as_string: '2018-06-08T21:00:00.000Z', - key: 1528491600000, - doc_count: 1234, + key_as_string: '2020-07-05T04:40:00.000Z', + key: 1593924000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-09T00:00:00.000Z', - key: 1528502400000, - doc_count: 284, + key_as_string: '2020-07-05T04:50:00.000Z', + key: 1593924600000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-09T03:00:00.000Z', - key: 1528513200000, - doc_count: 307, + key_as_string: '2020-07-05T05:00:00.000Z', + key: 1593925200000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-09T06:00:00.000Z', - key: 1528524000000, - doc_count: 283, + key_as_string: '2020-07-05T05:10:00.000Z', + key: 1593925800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-09T09:00:00.000Z', - key: 1528534800000, - doc_count: 303, + key_as_string: '2020-07-05T05:20:00.000Z', + key: 1593926400000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-09T12:00:00.000Z', - key: 1528545600000, - doc_count: 326, + key_as_string: '2020-07-05T05:30:00.000Z', + key: 1593927000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-09T15:00:00.000Z', - key: 1528556400000, - doc_count: 269, + key_as_string: '2020-07-05T05:40:00.000Z', + key: 1593927600000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-09T18:00:00.000Z', - key: 1528567200000, - doc_count: 297, + key_as_string: '2020-07-05T05:50:00.000Z', + key: 1593928200000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-09T21:00:00.000Z', - key: 1528578000000, - doc_count: 278, + key_as_string: '2020-07-05T06:00:00.000Z', + key: 1593928800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-10T00:00:00.000Z', - key: 1528588800000, - doc_count: 289, + key_as_string: '2020-07-05T06:10:00.000Z', + key: 1593929400000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-10T03:00:00.000Z', - key: 1528599600000, - doc_count: 272, + key_as_string: '2020-07-05T06:20:00.000Z', + key: 1593930000000, + doc_count: 50, + count: { + value: 50, + }, }, { - key_as_string: '2018-06-10T06:00:00.000Z', - key: 1528610400000, - doc_count: 279, + key_as_string: '2020-07-05T06:30:00.000Z', + key: 1593930600000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-10T09:00:00.000Z', - key: 1528621200000, - doc_count: 238, + key_as_string: '2020-07-05T06:40:00.000Z', + key: 1593931200000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-10T12:00:00.000Z', - key: 1528632000000, - doc_count: 288, + key_as_string: '2020-07-05T06:50:00.000Z', + key: 1593931800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-10T15:00:00.000Z', - key: 1528642800000, - doc_count: 258, + key_as_string: '2020-07-05T07:00:00.000Z', + key: 1593932400000, + doc_count: 37, + count: { + value: 37, + }, }, { - key_as_string: '2018-06-10T18:00:00.000Z', - key: 1528653600000, - doc_count: 264, + key_as_string: '2020-07-05T07:10:00.000Z', + key: 1593933000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-10T21:00:00.000Z', - key: 1528664400000, - doc_count: 296, + key_as_string: '2020-07-05T07:20:00.000Z', + key: 1593933600000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-11T00:00:00.000Z', - key: 1528675200000, - doc_count: 1213, + key_as_string: '2020-07-05T07:30:00.000Z', + key: 1593934200000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-11T03:00:00.000Z', - key: 1528686000000, - doc_count: 1254, + key_as_string: '2020-07-05T07:40:00.000Z', + key: 1593934800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-11T06:00:00.000Z', - key: 1528696800000, - doc_count: 1135, + key_as_string: '2020-07-05T07:50:00.000Z', + key: 1593935400000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-11T09:00:00.000Z', - key: 1528707600000, - doc_count: 1240, + key_as_string: '2020-07-05T08:00:00.000Z', + key: 1593936000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-11T12:00:00.000Z', - key: 1528718400000, - doc_count: 1215, + key_as_string: '2020-07-05T08:10:00.000Z', + key: 1593936600000, + doc_count: 194, + count: { + value: 194, + }, }, { - key_as_string: '2018-06-11T15:00:00.000Z', - key: 1528729200000, - doc_count: 1239, + key_as_string: '2020-07-05T08:20:00.000Z', + key: 1593937200000, + doc_count: 385, + count: { + value: 385, + }, }, { - key_as_string: '2018-06-11T18:00:00.000Z', - key: 1528740000000, - doc_count: 1209, + key_as_string: '2020-07-05T08:30:00.000Z', + key: 1593937800000, + doc_count: 421, + count: { + value: 421, + }, }, { - key_as_string: '2018-06-11T21:00:00.000Z', - key: 1528750800000, - doc_count: 1208, + key_as_string: '2020-07-05T08:40:00.000Z', + key: 1593938400000, + doc_count: 344, + count: { + value: 344, + }, }, + ], + }, + }, + { + key: 'HTTP 3xx', + doc_count: 3828, + timeseries: { + buckets: [ { - key_as_string: '2018-06-12T00:00:00.000Z', - key: 1528761600000, - doc_count: 1176, + key_as_string: '2020-07-04T08:40:00.000Z', + key: 1593852000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-12T03:00:00.000Z', - key: 1528772400000, - doc_count: 1207, + key_as_string: '2020-07-04T08:50:00.000Z', + key: 1593852600000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-12T06:00:00.000Z', - key: 1528783200000, - doc_count: 1198, + key_as_string: '2020-07-04T09:00:00.000Z', + key: 1593853200000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-12T09:00:00.000Z', - key: 1528794000000, - doc_count: 1165, + key_as_string: '2020-07-04T09:10:00.000Z', + key: 1593853800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-12T12:00:00.000Z', - key: 1528804800000, - doc_count: 1188, + key_as_string: '2020-07-04T09:20:00.000Z', + key: 1593854400000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-12T15:00:00.000Z', - key: 1528815600000, - doc_count: 1245, + key_as_string: '2020-07-04T09:30:00.000Z', + key: 1593855000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-12T18:00:00.000Z', - key: 1528826400000, - doc_count: 1238, + key_as_string: '2020-07-04T09:40:00.000Z', + key: 1593855600000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-12T21:00:00.000Z', - key: 1528837200000, - doc_count: 1283, + key_as_string: '2020-07-04T09:50:00.000Z', + key: 1593856200000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-13T00:00:00.000Z', - key: 1528848000000, - doc_count: 1198, + key_as_string: '2020-07-04T10:00:00.000Z', + key: 1593856800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-13T03:00:00.000Z', - key: 1528858800000, - doc_count: 1172, + key_as_string: '2020-07-04T10:10:00.000Z', + key: 1593857400000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-13T06:00:00.000Z', - key: 1528869600000, - doc_count: 1229, + key_as_string: '2020-07-04T10:20:00.000Z', + key: 1593858000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-13T09:00:00.000Z', - key: 1528880400000, - doc_count: 1239, + key_as_string: '2020-07-04T10:30:00.000Z', + key: 1593858600000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-13T12:00:00.000Z', - key: 1528891200000, - doc_count: 1231, + key_as_string: '2020-07-04T10:40:00.000Z', + key: 1593859200000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-13T15:00:00.000Z', - key: 1528902000000, - doc_count: 1248, + key_as_string: '2020-07-04T10:50:00.000Z', + key: 1593859800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-13T18:00:00.000Z', - key: 1528912800000, - doc_count: 1220, + key_as_string: '2020-07-04T11:00:00.000Z', + key: 1593860400000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-13T21:00:00.000Z', - key: 1528923600000, - doc_count: 1224, + key_as_string: '2020-07-04T11:10:00.000Z', + key: 1593861000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-14T00:00:00.000Z', - key: 1528934400000, - doc_count: 1088, + key_as_string: '2020-07-04T11:20:00.000Z', + key: 1593861600000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-14T03:00:00.000Z', - key: 1528945200000, - doc_count: 1235, + key_as_string: '2020-07-04T11:30:00.000Z', + key: 1593862200000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-14T06:00:00.000Z', - key: 1528956000000, - doc_count: 1161, + key_as_string: '2020-07-04T11:40:00.000Z', + key: 1593862800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-14T09:00:00.000Z', - key: 1528966800000, - doc_count: 1183, + key_as_string: '2020-07-04T11:50:00.000Z', + key: 1593863400000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-14T12:00:00.000Z', - key: 1528977600000, + key_as_string: '2020-07-04T12:00:00.000Z', + key: 1593864000000, doc_count: 0, + count: { + value: 0, + }, }, - ], - }, - }, - { - key: 'HTTP 4xx', - doc_count: 81907, - timeseries: { - buckets: [ { - key_as_string: '2018-06-04T12:00:00.000Z', - key: 1528113600000, - doc_count: 1186, + key_as_string: '2020-07-04T12:10:00.000Z', + key: 1593864600000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-04T15:00:00.000Z', - key: 1528124400000, - doc_count: 1213, + key_as_string: '2020-07-04T12:20:00.000Z', + key: 1593865200000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-04T18:00:00.000Z', - key: 1528135200000, - doc_count: 1205, + key_as_string: '2020-07-04T12:30:00.000Z', + key: 1593865800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-04T21:00:00.000Z', - key: 1528146000000, - doc_count: 1162, + key_as_string: '2020-07-04T12:40:00.000Z', + key: 1593866400000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-05T00:00:00.000Z', - key: 1528156800000, - doc_count: 1238, + key_as_string: '2020-07-04T12:50:00.000Z', + key: 1593867000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-05T03:00:00.000Z', - key: 1528167600000, - doc_count: 1191, + key_as_string: '2020-07-04T13:00:00.000Z', + key: 1593867600000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-05T06:00:00.000Z', - key: 1528178400000, - doc_count: 1274, + key_as_string: '2020-07-04T13:10:00.000Z', + key: 1593868200000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-05T09:00:00.000Z', - key: 1528189200000, - doc_count: 1234, + key_as_string: '2020-07-04T13:20:00.000Z', + key: 1593868800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-05T12:00:00.000Z', - key: 1528200000000, - doc_count: 1164, + key_as_string: '2020-07-04T13:30:00.000Z', + key: 1593869400000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-05T15:00:00.000Z', - key: 1528210800000, - doc_count: 1233, + key_as_string: '2020-07-04T13:40:00.000Z', + key: 1593870000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-05T18:00:00.000Z', - key: 1528221600000, - doc_count: 1223, + key_as_string: '2020-07-04T13:50:00.000Z', + key: 1593870600000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-05T21:00:00.000Z', - key: 1528232400000, - doc_count: 1216, + key_as_string: '2020-07-04T14:00:00.000Z', + key: 1593871200000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-06T00:00:00.000Z', - key: 1528243200000, - doc_count: 1200, + key_as_string: '2020-07-04T14:10:00.000Z', + key: 1593871800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-06T03:00:00.000Z', - key: 1528254000000, - doc_count: 1237, + key_as_string: '2020-07-04T14:20:00.000Z', + key: 1593872400000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-06T06:00:00.000Z', - key: 1528264800000, - doc_count: 1231, + key_as_string: '2020-07-04T14:30:00.000Z', + key: 1593873000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-06T09:00:00.000Z', - key: 1528275600000, - doc_count: 1182, + key_as_string: '2020-07-04T14:40:00.000Z', + key: 1593873600000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-06T12:00:00.000Z', - key: 1528286400000, - doc_count: 1125, + key_as_string: '2020-07-04T14:50:00.000Z', + key: 1593874200000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-06T15:00:00.000Z', - key: 1528297200000, - doc_count: 1243, + key_as_string: '2020-07-04T15:00:00.000Z', + key: 1593874800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-06T18:00:00.000Z', - key: 1528308000000, - doc_count: 1247, + key_as_string: '2020-07-04T15:10:00.000Z', + key: 1593875400000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-06T21:00:00.000Z', - key: 1528318800000, - doc_count: 1163, + key_as_string: '2020-07-04T15:20:00.000Z', + key: 1593876000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-07T00:00:00.000Z', - key: 1528329600000, - doc_count: 1220, + key_as_string: '2020-07-04T15:30:00.000Z', + key: 1593876600000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-07T03:00:00.000Z', - key: 1528340400000, - doc_count: 1202, + key_as_string: '2020-07-04T15:40:00.000Z', + key: 1593877200000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-07T06:00:00.000Z', - key: 1528351200000, - doc_count: 1192, + key_as_string: '2020-07-04T15:50:00.000Z', + key: 1593877800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-07T09:00:00.000Z', - key: 1528362000000, - doc_count: 1248, + key_as_string: '2020-07-04T16:00:00.000Z', + key: 1593878400000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-07T12:00:00.000Z', - key: 1528372800000, - doc_count: 1189, + key_as_string: '2020-07-04T16:10:00.000Z', + key: 1593879000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-07T15:00:00.000Z', - key: 1528383600000, - doc_count: 1230, + key_as_string: '2020-07-04T16:20:00.000Z', + key: 1593879600000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-07T18:00:00.000Z', - key: 1528394400000, - doc_count: 1206, + key_as_string: '2020-07-04T16:30:00.000Z', + key: 1593880200000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-07T21:00:00.000Z', - key: 1528405200000, - doc_count: 1190, + key_as_string: '2020-07-04T16:40:00.000Z', + key: 1593880800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-08T00:00:00.000Z', - key: 1528416000000, - doc_count: 1232, + key_as_string: '2020-07-04T16:50:00.000Z', + key: 1593881400000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-08T03:00:00.000Z', - key: 1528426800000, - doc_count: 1171, + key_as_string: '2020-07-04T17:00:00.000Z', + key: 1593882000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-08T06:00:00.000Z', - key: 1528437600000, - doc_count: 1232, + key_as_string: '2020-07-04T17:10:00.000Z', + key: 1593882600000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-08T09:00:00.000Z', - key: 1528448400000, - doc_count: 1253, + key_as_string: '2020-07-04T17:20:00.000Z', + key: 1593883200000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-08T12:00:00.000Z', - key: 1528459200000, - doc_count: 1250, + key_as_string: '2020-07-04T17:30:00.000Z', + key: 1593883800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-08T15:00:00.000Z', - key: 1528470000000, - doc_count: 1167, + key_as_string: '2020-07-04T17:40:00.000Z', + key: 1593884400000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-08T18:00:00.000Z', - key: 1528480800000, - doc_count: 1258, + key_as_string: '2020-07-04T17:50:00.000Z', + key: 1593885000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-08T21:00:00.000Z', - key: 1528491600000, - doc_count: 1148, + key_as_string: '2020-07-04T18:00:00.000Z', + key: 1593885600000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-09T00:00:00.000Z', - key: 1528502400000, - doc_count: 284, + key_as_string: '2020-07-04T18:10:00.000Z', + key: 1593886200000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-09T03:00:00.000Z', - key: 1528513200000, - doc_count: 240, + key_as_string: '2020-07-04T18:20:00.000Z', + key: 1593886800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-09T06:00:00.000Z', - key: 1528524000000, - doc_count: 273, + key_as_string: '2020-07-04T18:30:00.000Z', + key: 1593887400000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-09T09:00:00.000Z', - key: 1528534800000, - doc_count: 295, + key_as_string: '2020-07-04T18:40:00.000Z', + key: 1593888000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-09T12:00:00.000Z', - key: 1528545600000, - doc_count: 281, + key_as_string: '2020-07-04T18:50:00.000Z', + key: 1593888600000, + doc_count: 62, + count: { + value: 62, + }, }, { - key_as_string: '2018-06-09T15:00:00.000Z', - key: 1528556400000, - doc_count: 300, + key_as_string: '2020-07-04T19:00:00.000Z', + key: 1593889200000, + doc_count: 52, + count: { + value: 52, + }, }, { - key_as_string: '2018-06-09T18:00:00.000Z', - key: 1528567200000, - doc_count: 264, + key_as_string: '2020-07-04T19:10:00.000Z', + key: 1593889800000, + doc_count: 128, + count: { + value: 128, + }, }, { - key_as_string: '2018-06-09T21:00:00.000Z', - key: 1528578000000, - doc_count: 260, + key_as_string: '2020-07-04T19:20:00.000Z', + key: 1593890400000, + doc_count: 143, + count: { + value: 143, + }, }, { - key_as_string: '2018-06-10T00:00:00.000Z', - key: 1528588800000, - doc_count: 279, + key_as_string: '2020-07-04T19:30:00.000Z', + key: 1593891000000, + doc_count: 129, + count: { + value: 129, + }, }, { - key_as_string: '2018-06-10T03:00:00.000Z', - key: 1528599600000, - doc_count: 259, + key_as_string: '2020-07-04T19:40:00.000Z', + key: 1593891600000, + doc_count: 121, + count: { + value: 121, + }, }, { - key_as_string: '2018-06-10T06:00:00.000Z', - key: 1528610400000, - doc_count: 291, + key_as_string: '2020-07-04T19:50:00.000Z', + key: 1593892200000, + doc_count: 292, + count: { + value: 292, + }, }, { - key_as_string: '2018-06-10T09:00:00.000Z', - key: 1528621200000, - doc_count: 248, + key_as_string: '2020-07-04T20:00:00.000Z', + key: 1593892800000, + doc_count: 139, + count: { + value: 139, + }, }, { - key_as_string: '2018-06-10T12:00:00.000Z', - key: 1528632000000, - doc_count: 311, + key_as_string: '2020-07-04T20:10:00.000Z', + key: 1593893400000, + doc_count: 104, + count: { + value: 104, + }, }, { - key_as_string: '2018-06-10T15:00:00.000Z', - key: 1528642800000, - doc_count: 277, + key_as_string: '2020-07-04T20:20:00.000Z', + key: 1593894000000, + doc_count: 198, + count: { + value: 198, + }, }, { - key_as_string: '2018-06-10T18:00:00.000Z', - key: 1528653600000, - doc_count: 279, + key_as_string: '2020-07-04T20:30:00.000Z', + key: 1593894600000, + doc_count: 179, + count: { + value: 179, + }, }, { - key_as_string: '2018-06-10T21:00:00.000Z', - key: 1528664400000, - doc_count: 275, + key_as_string: '2020-07-04T20:40:00.000Z', + key: 1593895200000, + doc_count: 117, + count: { + value: 117, + }, }, { - key_as_string: '2018-06-11T00:00:00.000Z', - key: 1528675200000, - doc_count: 1167, + key_as_string: '2020-07-04T20:50:00.000Z', + key: 1593895800000, + doc_count: 183, + count: { + value: 183, + }, }, { - key_as_string: '2018-06-11T03:00:00.000Z', - key: 1528686000000, - doc_count: 1270, + key_as_string: '2020-07-04T21:00:00.000Z', + key: 1593896400000, + doc_count: 264, + count: { + value: 264, + }, }, { - key_as_string: '2018-06-11T06:00:00.000Z', - key: 1528696800000, - doc_count: 1163, + key_as_string: '2020-07-04T21:10:00.000Z', + key: 1593897000000, + doc_count: 180, + count: { + value: 180, + }, }, { - key_as_string: '2018-06-11T09:00:00.000Z', - key: 1528707600000, - doc_count: 1155, + key_as_string: '2020-07-04T21:20:00.000Z', + key: 1593897600000, + doc_count: 160, + count: { + value: 160, + }, }, { - key_as_string: '2018-06-11T12:00:00.000Z', - key: 1528718400000, - doc_count: 1217, + key_as_string: '2020-07-04T21:30:00.000Z', + key: 1593898200000, + doc_count: 208, + count: { + value: 208, + }, }, { - key_as_string: '2018-06-11T15:00:00.000Z', - key: 1528729200000, - doc_count: 1227, + key_as_string: '2020-07-04T21:40:00.000Z', + key: 1593898800000, + doc_count: 158, + count: { + value: 158, + }, }, { - key_as_string: '2018-06-11T18:00:00.000Z', - key: 1528740000000, - doc_count: 1194, + key_as_string: '2020-07-04T21:50:00.000Z', + key: 1593899400000, + doc_count: 176, + count: { + value: 176, + }, }, { - key_as_string: '2018-06-11T21:00:00.000Z', - key: 1528750800000, - doc_count: 1153, + key_as_string: '2020-07-04T22:00:00.000Z', + key: 1593900000000, + doc_count: 183, + count: { + value: 183, + }, }, { - key_as_string: '2018-06-12T00:00:00.000Z', - key: 1528761600000, - doc_count: 1211, + key_as_string: '2020-07-04T22:10:00.000Z', + key: 1593900600000, + doc_count: 234, + count: { + value: 234, + }, }, { - key_as_string: '2018-06-12T03:00:00.000Z', - key: 1528772400000, - doc_count: 1203, + key_as_string: '2020-07-04T22:20:00.000Z', + key: 1593901200000, + doc_count: 125, + count: { + value: 125, + }, }, { - key_as_string: '2018-06-12T06:00:00.000Z', - key: 1528783200000, - doc_count: 1269, + key_as_string: '2020-07-04T22:30:00.000Z', + key: 1593901800000, + doc_count: 48, + count: { + value: 48, + }, }, { - key_as_string: '2018-06-12T09:00:00.000Z', - key: 1528794000000, - doc_count: 1197, + key_as_string: '2020-07-04T22:40:00.000Z', + key: 1593902400000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-12T12:00:00.000Z', - key: 1528804800000, - doc_count: 1184, + key_as_string: '2020-07-04T22:50:00.000Z', + key: 1593903000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-12T15:00:00.000Z', - key: 1528815600000, - doc_count: 1176, + key_as_string: '2020-07-04T23:00:00.000Z', + key: 1593903600000, + doc_count: 9, + count: { + value: 9, + }, }, { - key_as_string: '2018-06-12T18:00:00.000Z', - key: 1528826400000, - doc_count: 1162, + key_as_string: '2020-07-04T23:10:00.000Z', + key: 1593904200000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-12T21:00:00.000Z', - key: 1528837200000, - doc_count: 1270, + key_as_string: '2020-07-04T23:20:00.000Z', + key: 1593904800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-13T00:00:00.000Z', - key: 1528848000000, - doc_count: 1224, + key_as_string: '2020-07-04T23:30:00.000Z', + key: 1593905400000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-13T03:00:00.000Z', - key: 1528858800000, - doc_count: 1255, + key_as_string: '2020-07-04T23:40:00.000Z', + key: 1593906000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-13T06:00:00.000Z', - key: 1528869600000, - doc_count: 1207, + key_as_string: '2020-07-04T23:50:00.000Z', + key: 1593906600000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-13T09:00:00.000Z', - key: 1528880400000, - doc_count: 1206, + key_as_string: '2020-07-05T00:00:00.000Z', + key: 1593907200000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-13T12:00:00.000Z', - key: 1528891200000, - doc_count: 1254, + key_as_string: '2020-07-05T00:10:00.000Z', + key: 1593907800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-13T15:00:00.000Z', - key: 1528902000000, - doc_count: 1216, + key_as_string: '2020-07-05T00:20:00.000Z', + key: 1593908400000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-13T18:00:00.000Z', - key: 1528912800000, - doc_count: 1263, + key_as_string: '2020-07-05T00:30:00.000Z', + key: 1593909000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-13T21:00:00.000Z', - key: 1528923600000, - doc_count: 1277, + key_as_string: '2020-07-05T00:40:00.000Z', + key: 1593909600000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-14T00:00:00.000Z', - key: 1528934400000, - doc_count: 1183, + key_as_string: '2020-07-05T00:50:00.000Z', + key: 1593910200000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-14T03:00:00.000Z', - key: 1528945200000, - doc_count: 1221, + key_as_string: '2020-07-05T01:00:00.000Z', + key: 1593910800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-14T06:00:00.000Z', - key: 1528956000000, - doc_count: 1198, + key_as_string: '2020-07-05T01:10:00.000Z', + key: 1593911400000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-14T09:00:00.000Z', - key: 1528966800000, - doc_count: 1214, + key_as_string: '2020-07-05T01:20:00.000Z', + key: 1593912000000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-14T12:00:00.000Z', - key: 1528977600000, + key_as_string: '2020-07-05T01:30:00.000Z', + key: 1593912600000, doc_count: 0, + count: { + value: 0, + }, }, - ], - }, - }, - { - key: 'HTTP 3xx', - doc_count: 6650, - timeseries: { - buckets: [ { - key_as_string: '2018-06-04T12:00:00.000Z', - key: 1528113600000, + key_as_string: '2020-07-05T01:40:00.000Z', + key: 1593913200000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-04T15:00:00.000Z', - key: 1528124400000, + key_as_string: '2020-07-05T01:50:00.000Z', + key: 1593913800000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-04T18:00:00.000Z', - key: 1528135200000, + key_as_string: '2020-07-05T02:00:00.000Z', + key: 1593914400000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-04T21:00:00.000Z', - key: 1528146000000, + key_as_string: '2020-07-05T02:10:00.000Z', + key: 1593915000000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-05T00:00:00.000Z', - key: 1528156800000, + key_as_string: '2020-07-05T02:20:00.000Z', + key: 1593915600000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-05T03:00:00.000Z', - key: 1528167600000, + key_as_string: '2020-07-05T02:30:00.000Z', + key: 1593916200000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-05T06:00:00.000Z', - key: 1528178400000, + key_as_string: '2020-07-05T02:40:00.000Z', + key: 1593916800000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-05T09:00:00.000Z', - key: 1528189200000, + key_as_string: '2020-07-05T02:50:00.000Z', + key: 1593917400000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-05T12:00:00.000Z', - key: 1528200000000, + key_as_string: '2020-07-05T03:00:00.000Z', + key: 1593918000000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-05T15:00:00.000Z', - key: 1528210800000, + key_as_string: '2020-07-05T03:10:00.000Z', + key: 1593918600000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-05T18:00:00.000Z', - key: 1528221600000, + key_as_string: '2020-07-05T03:20:00.000Z', + key: 1593919200000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-05T21:00:00.000Z', - key: 1528232400000, + key_as_string: '2020-07-05T03:30:00.000Z', + key: 1593919800000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-06T00:00:00.000Z', - key: 1528243200000, + key_as_string: '2020-07-05T03:40:00.000Z', + key: 1593920400000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-06T03:00:00.000Z', - key: 1528254000000, + key_as_string: '2020-07-05T03:50:00.000Z', + key: 1593921000000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-06T06:00:00.000Z', - key: 1528264800000, + key_as_string: '2020-07-05T04:00:00.000Z', + key: 1593921600000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-06T09:00:00.000Z', - key: 1528275600000, + key_as_string: '2020-07-05T04:10:00.000Z', + key: 1593922200000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-06T12:00:00.000Z', - key: 1528286400000, - doc_count: 4041, + key_as_string: '2020-07-05T04:20:00.000Z', + key: 1593922800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-06T15:00:00.000Z', - key: 1528297200000, - doc_count: 454, + key_as_string: '2020-07-05T04:30:00.000Z', + key: 1593923400000, + doc_count: 11, + count: { + value: 11, + }, }, { - key_as_string: '2018-06-06T18:00:00.000Z', - key: 1528308000000, + key_as_string: '2020-07-05T04:40:00.000Z', + key: 1593924000000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-06T21:00:00.000Z', - key: 1528318800000, + key_as_string: '2020-07-05T04:50:00.000Z', + key: 1593924600000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-07T00:00:00.000Z', - key: 1528329600000, + key_as_string: '2020-07-05T05:00:00.000Z', + key: 1593925200000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-07T03:00:00.000Z', - key: 1528340400000, + key_as_string: '2020-07-05T05:10:00.000Z', + key: 1593925800000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-07T06:00:00.000Z', - key: 1528351200000, + key_as_string: '2020-07-05T05:20:00.000Z', + key: 1593926400000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-07T09:00:00.000Z', - key: 1528362000000, + key_as_string: '2020-07-05T05:30:00.000Z', + key: 1593927000000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-07T12:00:00.000Z', - key: 1528372800000, + key_as_string: '2020-07-05T05:40:00.000Z', + key: 1593927600000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-07T15:00:00.000Z', - key: 1528383600000, + key_as_string: '2020-07-05T05:50:00.000Z', + key: 1593928200000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-07T18:00:00.000Z', - key: 1528394400000, + key_as_string: '2020-07-05T06:00:00.000Z', + key: 1593928800000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-07T21:00:00.000Z', - key: 1528405200000, + key_as_string: '2020-07-05T06:10:00.000Z', + key: 1593929400000, doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T06:20:00.000Z', + key: 1593930000000, + doc_count: 28, + count: { + value: 28, + }, }, { - key_as_string: '2018-06-08T00:00:00.000Z', - key: 1528416000000, + key_as_string: '2020-07-05T06:30:00.000Z', + key: 1593930600000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-08T03:00:00.000Z', - key: 1528426800000, + key_as_string: '2020-07-05T06:40:00.000Z', + key: 1593931200000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-08T06:00:00.000Z', - key: 1528437600000, + key_as_string: '2020-07-05T06:50:00.000Z', + key: 1593931800000, doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T07:00:00.000Z', + key: 1593932400000, + doc_count: 2, + count: { + value: 2, + }, }, { - key_as_string: '2018-06-08T09:00:00.000Z', - key: 1528448400000, + key_as_string: '2020-07-05T07:10:00.000Z', + key: 1593933000000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-08T12:00:00.000Z', - key: 1528459200000, + key_as_string: '2020-07-05T07:20:00.000Z', + key: 1593933600000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-08T15:00:00.000Z', - key: 1528470000000, + key_as_string: '2020-07-05T07:30:00.000Z', + key: 1593934200000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-08T18:00:00.000Z', - key: 1528480800000, + key_as_string: '2020-07-05T07:40:00.000Z', + key: 1593934800000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-08T21:00:00.000Z', - key: 1528491600000, + key_as_string: '2020-07-05T07:50:00.000Z', + key: 1593935400000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-09T00:00:00.000Z', - key: 1528502400000, + key_as_string: '2020-07-05T08:00:00.000Z', + key: 1593936000000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-09T03:00:00.000Z', - key: 1528513200000, + key_as_string: '2020-07-05T08:10:00.000Z', + key: 1593936600000, doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T08:20:00.000Z', + key: 1593937200000, + doc_count: 63, + count: { + value: 63, + }, + }, + { + key_as_string: '2020-07-05T08:30:00.000Z', + key: 1593937800000, + doc_count: 50, + count: { + value: 50, + }, + }, + { + key_as_string: '2020-07-05T08:40:00.000Z', + key: 1593938400000, + doc_count: 82, + count: { + value: 82, + }, }, + ], + }, + }, + { + key: 'HTTP 4xx', + doc_count: 683, + timeseries: { + buckets: [ { - key_as_string: '2018-06-09T06:00:00.000Z', - key: 1528524000000, + key_as_string: '2020-07-04T08:40:00.000Z', + key: 1593852000000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-09T09:00:00.000Z', - key: 1528534800000, + key_as_string: '2020-07-04T08:50:00.000Z', + key: 1593852600000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-09T12:00:00.000Z', - key: 1528545600000, + key_as_string: '2020-07-04T09:00:00.000Z', + key: 1593853200000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-09T15:00:00.000Z', - key: 1528556400000, + key_as_string: '2020-07-04T09:10:00.000Z', + key: 1593853800000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-09T18:00:00.000Z', - key: 1528567200000, + key_as_string: '2020-07-04T09:20:00.000Z', + key: 1593854400000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-09T21:00:00.000Z', - key: 1528578000000, + key_as_string: '2020-07-04T09:30:00.000Z', + key: 1593855000000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-10T00:00:00.000Z', - key: 1528588800000, + key_as_string: '2020-07-04T09:40:00.000Z', + key: 1593855600000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-10T03:00:00.000Z', - key: 1528599600000, + key_as_string: '2020-07-04T09:50:00.000Z', + key: 1593856200000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-10T06:00:00.000Z', - key: 1528610400000, + key_as_string: '2020-07-04T10:00:00.000Z', + key: 1593856800000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-10T09:00:00.000Z', - key: 1528621200000, + key_as_string: '2020-07-04T10:10:00.000Z', + key: 1593857400000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-10T12:00:00.000Z', - key: 1528632000000, + key_as_string: '2020-07-04T10:20:00.000Z', + key: 1593858000000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-10T15:00:00.000Z', - key: 1528642800000, + key_as_string: '2020-07-04T10:30:00.000Z', + key: 1593858600000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-10T18:00:00.000Z', - key: 1528653600000, + key_as_string: '2020-07-04T10:40:00.000Z', + key: 1593859200000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-10T21:00:00.000Z', - key: 1528664400000, + key_as_string: '2020-07-04T10:50:00.000Z', + key: 1593859800000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-11T00:00:00.000Z', - key: 1528675200000, + key_as_string: '2020-07-04T11:00:00.000Z', + key: 1593860400000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-11T03:00:00.000Z', - key: 1528686000000, + key_as_string: '2020-07-04T11:10:00.000Z', + key: 1593861000000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-11T06:00:00.000Z', - key: 1528696800000, + key_as_string: '2020-07-04T11:20:00.000Z', + key: 1593861600000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-11T09:00:00.000Z', - key: 1528707600000, + key_as_string: '2020-07-04T11:30:00.000Z', + key: 1593862200000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-11T12:00:00.000Z', - key: 1528718400000, + key_as_string: '2020-07-04T11:40:00.000Z', + key: 1593862800000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-11T15:00:00.000Z', - key: 1528729200000, + key_as_string: '2020-07-04T11:50:00.000Z', + key: 1593863400000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-11T18:00:00.000Z', - key: 1528740000000, + key_as_string: '2020-07-04T12:00:00.000Z', + key: 1593864000000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-11T21:00:00.000Z', - key: 1528750800000, + key_as_string: '2020-07-04T12:10:00.000Z', + key: 1593864600000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-12T00:00:00.000Z', - key: 1528761600000, + key_as_string: '2020-07-04T12:20:00.000Z', + key: 1593865200000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-12T03:00:00.000Z', - key: 1528772400000, + key_as_string: '2020-07-04T12:30:00.000Z', + key: 1593865800000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-12T06:00:00.000Z', - key: 1528783200000, + key_as_string: '2020-07-04T12:40:00.000Z', + key: 1593866400000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-12T09:00:00.000Z', - key: 1528794000000, + key_as_string: '2020-07-04T12:50:00.000Z', + key: 1593867000000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-12T12:00:00.000Z', - key: 1528804800000, + key_as_string: '2020-07-04T13:00:00.000Z', + key: 1593867600000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-12T15:00:00.000Z', - key: 1528815600000, + key_as_string: '2020-07-04T13:10:00.000Z', + key: 1593868200000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-12T18:00:00.000Z', - key: 1528826400000, + key_as_string: '2020-07-04T13:20:00.000Z', + key: 1593868800000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-12T21:00:00.000Z', - key: 1528837200000, + key_as_string: '2020-07-04T13:30:00.000Z', + key: 1593869400000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-13T00:00:00.000Z', - key: 1528848000000, + key_as_string: '2020-07-04T13:40:00.000Z', + key: 1593870000000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-13T03:00:00.000Z', - key: 1528858800000, + key_as_string: '2020-07-04T13:50:00.000Z', + key: 1593870600000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-13T06:00:00.000Z', - key: 1528869600000, + key_as_string: '2020-07-04T14:00:00.000Z', + key: 1593871200000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-13T09:00:00.000Z', - key: 1528880400000, + key_as_string: '2020-07-04T14:10:00.000Z', + key: 1593871800000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-13T12:00:00.000Z', - key: 1528891200000, + key_as_string: '2020-07-04T14:20:00.000Z', + key: 1593872400000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-13T15:00:00.000Z', - key: 1528902000000, + key_as_string: '2020-07-04T14:30:00.000Z', + key: 1593873000000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-13T18:00:00.000Z', - key: 1528912800000, + key_as_string: '2020-07-04T14:40:00.000Z', + key: 1593873600000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-13T21:00:00.000Z', - key: 1528923600000, + key_as_string: '2020-07-04T14:50:00.000Z', + key: 1593874200000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-14T00:00:00.000Z', - key: 1528934400000, - doc_count: 2155, + key_as_string: '2020-07-04T15:00:00.000Z', + key: 1593874800000, + doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-14T03:00:00.000Z', - key: 1528945200000, + key_as_string: '2020-07-04T15:10:00.000Z', + key: 1593875400000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-14T06:00:00.000Z', - key: 1528956000000, + key_as_string: '2020-07-04T15:20:00.000Z', + key: 1593876000000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-14T09:00:00.000Z', - key: 1528966800000, + key_as_string: '2020-07-04T15:30:00.000Z', + key: 1593876600000, doc_count: 0, + count: { + value: 0, + }, }, { - key_as_string: '2018-06-14T12:00:00.000Z', - key: 1528977600000, + key_as_string: '2020-07-04T15:40:00.000Z', + key: 1593877200000, doc_count: 0, + count: { + value: 0, + }, }, - ], - }, - }, - ], - }, - response_times: { - buckets: [ - { - key_as_string: '2018-06-04T12:00:00.000Z', - key: 1528113600000, - doc_count: 18841, - pct: { - values: { - '95.0': 82172.85648714812, - '99.0': 293866.3866666665, - }, + { + key_as_string: '2020-07-04T15:50:00.000Z', + key: 1593877800000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T16:00:00.000Z', + key: 1593878400000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T16:10:00.000Z', + key: 1593879000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T16:20:00.000Z', + key: 1593879600000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T16:30:00.000Z', + key: 1593880200000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T16:40:00.000Z', + key: 1593880800000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T16:50:00.000Z', + key: 1593881400000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T17:00:00.000Z', + key: 1593882000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T17:10:00.000Z', + key: 1593882600000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T17:20:00.000Z', + key: 1593883200000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T17:30:00.000Z', + key: 1593883800000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T17:40:00.000Z', + key: 1593884400000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T17:50:00.000Z', + key: 1593885000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T18:00:00.000Z', + key: 1593885600000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T18:10:00.000Z', + key: 1593886200000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T18:20:00.000Z', + key: 1593886800000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T18:30:00.000Z', + key: 1593887400000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T18:40:00.000Z', + key: 1593888000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T18:50:00.000Z', + key: 1593888600000, + doc_count: 11, + count: { + value: 11, + }, + }, + { + key_as_string: '2020-07-04T19:00:00.000Z', + key: 1593889200000, + doc_count: 31, + count: { + value: 31, + }, + }, + { + key_as_string: '2020-07-04T19:10:00.000Z', + key: 1593889800000, + doc_count: 19, + count: { + value: 19, + }, + }, + { + key_as_string: '2020-07-04T19:20:00.000Z', + key: 1593890400000, + doc_count: 23, + count: { + value: 23, + }, + }, + { + key_as_string: '2020-07-04T19:30:00.000Z', + key: 1593891000000, + doc_count: 26, + count: { + value: 26, + }, + }, + { + key_as_string: '2020-07-04T19:40:00.000Z', + key: 1593891600000, + doc_count: 27, + count: { + value: 27, + }, + }, + { + key_as_string: '2020-07-04T19:50:00.000Z', + key: 1593892200000, + doc_count: 27, + count: { + value: 27, + }, + }, + { + key_as_string: '2020-07-04T20:00:00.000Z', + key: 1593892800000, + doc_count: 30, + count: { + value: 30, + }, + }, + { + key_as_string: '2020-07-04T20:10:00.000Z', + key: 1593893400000, + doc_count: 28, + count: { + value: 28, + }, + }, + { + key_as_string: '2020-07-04T20:20:00.000Z', + key: 1593894000000, + doc_count: 33, + count: { + value: 33, + }, + }, + { + key_as_string: '2020-07-04T20:30:00.000Z', + key: 1593894600000, + doc_count: 23, + count: { + value: 23, + }, + }, + { + key_as_string: '2020-07-04T20:40:00.000Z', + key: 1593895200000, + doc_count: 35, + count: { + value: 35, + }, + }, + { + key_as_string: '2020-07-04T20:50:00.000Z', + key: 1593895800000, + doc_count: 26, + count: { + value: 26, + }, + }, + { + key_as_string: '2020-07-04T21:00:00.000Z', + key: 1593896400000, + doc_count: 35, + count: { + value: 35, + }, + }, + { + key_as_string: '2020-07-04T21:10:00.000Z', + key: 1593897000000, + doc_count: 25, + count: { + value: 25, + }, + }, + { + key_as_string: '2020-07-04T21:20:00.000Z', + key: 1593897600000, + doc_count: 26, + count: { + value: 26, + }, + }, + { + key_as_string: '2020-07-04T21:30:00.000Z', + key: 1593898200000, + doc_count: 25, + count: { + value: 25, + }, + }, + { + key_as_string: '2020-07-04T21:40:00.000Z', + key: 1593898800000, + doc_count: 17, + count: { + value: 17, + }, + }, + { + key_as_string: '2020-07-04T21:50:00.000Z', + key: 1593899400000, + doc_count: 19, + count: { + value: 19, + }, + }, + { + key_as_string: '2020-07-04T22:00:00.000Z', + key: 1593900000000, + doc_count: 28, + count: { + value: 28, + }, + }, + { + key_as_string: '2020-07-04T22:10:00.000Z', + key: 1593900600000, + doc_count: 24, + count: { + value: 24, + }, + }, + { + key_as_string: '2020-07-04T22:20:00.000Z', + key: 1593901200000, + doc_count: 30, + count: { + value: 30, + }, + }, + { + key_as_string: '2020-07-04T22:30:00.000Z', + key: 1593901800000, + doc_count: 6, + count: { + value: 6, + }, + }, + { + key_as_string: '2020-07-04T22:40:00.000Z', + key: 1593902400000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T22:50:00.000Z', + key: 1593903000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T23:00:00.000Z', + key: 1593903600000, + doc_count: 2, + count: { + value: 2, + }, + }, + { + key_as_string: '2020-07-04T23:10:00.000Z', + key: 1593904200000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T23:20:00.000Z', + key: 1593904800000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T23:30:00.000Z', + key: 1593905400000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T23:40:00.000Z', + key: 1593906000000, + doc_count: 4, + count: { + value: 4, + }, + }, + { + key_as_string: '2020-07-04T23:50:00.000Z', + key: 1593906600000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T00:00:00.000Z', + key: 1593907200000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T00:10:00.000Z', + key: 1593907800000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T00:20:00.000Z', + key: 1593908400000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T00:30:00.000Z', + key: 1593909000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T00:40:00.000Z', + key: 1593909600000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T00:50:00.000Z', + key: 1593910200000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T01:00:00.000Z', + key: 1593910800000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T01:10:00.000Z', + key: 1593911400000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T01:20:00.000Z', + key: 1593912000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T01:30:00.000Z', + key: 1593912600000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T01:40:00.000Z', + key: 1593913200000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T01:50:00.000Z', + key: 1593913800000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T02:00:00.000Z', + key: 1593914400000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T02:10:00.000Z', + key: 1593915000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T02:20:00.000Z', + key: 1593915600000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T02:30:00.000Z', + key: 1593916200000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T02:40:00.000Z', + key: 1593916800000, + doc_count: 3, + count: { + value: 3, + }, + }, + { + key_as_string: '2020-07-05T02:50:00.000Z', + key: 1593917400000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T03:00:00.000Z', + key: 1593918000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T03:10:00.000Z', + key: 1593918600000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T03:20:00.000Z', + key: 1593919200000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T03:30:00.000Z', + key: 1593919800000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T03:40:00.000Z', + key: 1593920400000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T03:50:00.000Z', + key: 1593921000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T04:00:00.000Z', + key: 1593921600000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T04:10:00.000Z', + key: 1593922200000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T04:20:00.000Z', + key: 1593922800000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T04:30:00.000Z', + key: 1593923400000, + doc_count: 2, + count: { + value: 2, + }, + }, + { + key_as_string: '2020-07-05T04:40:00.000Z', + key: 1593924000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T04:50:00.000Z', + key: 1593924600000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T05:00:00.000Z', + key: 1593925200000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T05:10:00.000Z', + key: 1593925800000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T05:20:00.000Z', + key: 1593926400000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T05:30:00.000Z', + key: 1593927000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T05:40:00.000Z', + key: 1593927600000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T05:50:00.000Z', + key: 1593928200000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T06:00:00.000Z', + key: 1593928800000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T06:10:00.000Z', + key: 1593929400000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T06:20:00.000Z', + key: 1593930000000, + doc_count: 3, + count: { + value: 3, + }, + }, + { + key_as_string: '2020-07-05T06:30:00.000Z', + key: 1593930600000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T06:40:00.000Z', + key: 1593931200000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T06:50:00.000Z', + key: 1593931800000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T07:00:00.000Z', + key: 1593932400000, + doc_count: 2, + count: { + value: 2, + }, + }, + { + key_as_string: '2020-07-05T07:10:00.000Z', + key: 1593933000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T07:20:00.000Z', + key: 1593933600000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T07:30:00.000Z', + key: 1593934200000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T07:40:00.000Z', + key: 1593934800000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T07:50:00.000Z', + key: 1593935400000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T08:00:00.000Z', + key: 1593936000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T08:10:00.000Z', + key: 1593936600000, + doc_count: 15, + count: { + value: 15, + }, + }, + { + key_as_string: '2020-07-05T08:20:00.000Z', + key: 1593937200000, + doc_count: 29, + count: { + value: 29, + }, + }, + { + key_as_string: '2020-07-05T08:30:00.000Z', + key: 1593937800000, + doc_count: 31, + count: { + value: 31, + }, + }, + { + key_as_string: '2020-07-05T08:40:00.000Z', + key: 1593938400000, + doc_count: 18, + count: { + value: 18, + }, + }, + ], + }, + }, + { + key: 'HTTP 5xx', + doc_count: 378, + timeseries: { + buckets: [ + { + key_as_string: '2020-07-04T08:40:00.000Z', + key: 1593852000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T08:50:00.000Z', + key: 1593852600000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T09:00:00.000Z', + key: 1593853200000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T09:10:00.000Z', + key: 1593853800000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T09:20:00.000Z', + key: 1593854400000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T09:30:00.000Z', + key: 1593855000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T09:40:00.000Z', + key: 1593855600000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T09:50:00.000Z', + key: 1593856200000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T10:00:00.000Z', + key: 1593856800000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T10:10:00.000Z', + key: 1593857400000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T10:20:00.000Z', + key: 1593858000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T10:30:00.000Z', + key: 1593858600000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T10:40:00.000Z', + key: 1593859200000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T10:50:00.000Z', + key: 1593859800000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T11:00:00.000Z', + key: 1593860400000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T11:10:00.000Z', + key: 1593861000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T11:20:00.000Z', + key: 1593861600000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T11:30:00.000Z', + key: 1593862200000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T11:40:00.000Z', + key: 1593862800000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T11:50:00.000Z', + key: 1593863400000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T12:00:00.000Z', + key: 1593864000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T12:10:00.000Z', + key: 1593864600000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T12:20:00.000Z', + key: 1593865200000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T12:30:00.000Z', + key: 1593865800000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T12:40:00.000Z', + key: 1593866400000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T12:50:00.000Z', + key: 1593867000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T13:00:00.000Z', + key: 1593867600000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T13:10:00.000Z', + key: 1593868200000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T13:20:00.000Z', + key: 1593868800000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T13:30:00.000Z', + key: 1593869400000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T13:40:00.000Z', + key: 1593870000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T13:50:00.000Z', + key: 1593870600000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T14:00:00.000Z', + key: 1593871200000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T14:10:00.000Z', + key: 1593871800000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T14:20:00.000Z', + key: 1593872400000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T14:30:00.000Z', + key: 1593873000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T14:40:00.000Z', + key: 1593873600000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T14:50:00.000Z', + key: 1593874200000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T15:00:00.000Z', + key: 1593874800000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T15:10:00.000Z', + key: 1593875400000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T15:20:00.000Z', + key: 1593876000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T15:30:00.000Z', + key: 1593876600000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T15:40:00.000Z', + key: 1593877200000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T15:50:00.000Z', + key: 1593877800000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T16:00:00.000Z', + key: 1593878400000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T16:10:00.000Z', + key: 1593879000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T16:20:00.000Z', + key: 1593879600000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T16:30:00.000Z', + key: 1593880200000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T16:40:00.000Z', + key: 1593880800000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T16:50:00.000Z', + key: 1593881400000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T17:00:00.000Z', + key: 1593882000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T17:10:00.000Z', + key: 1593882600000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T17:20:00.000Z', + key: 1593883200000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T17:30:00.000Z', + key: 1593883800000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T17:40:00.000Z', + key: 1593884400000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T17:50:00.000Z', + key: 1593885000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T18:00:00.000Z', + key: 1593885600000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T18:10:00.000Z', + key: 1593886200000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T18:20:00.000Z', + key: 1593886800000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T18:30:00.000Z', + key: 1593887400000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T18:40:00.000Z', + key: 1593888000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T18:50:00.000Z', + key: 1593888600000, + doc_count: 5, + count: { + value: 5, + }, + }, + { + key_as_string: '2020-07-04T19:00:00.000Z', + key: 1593889200000, + doc_count: 15, + count: { + value: 15, + }, + }, + { + key_as_string: '2020-07-04T19:10:00.000Z', + key: 1593889800000, + doc_count: 12, + count: { + value: 12, + }, + }, + { + key_as_string: '2020-07-04T19:20:00.000Z', + key: 1593890400000, + doc_count: 16, + count: { + value: 16, + }, + }, + { + key_as_string: '2020-07-04T19:30:00.000Z', + key: 1593891000000, + doc_count: 12, + count: { + value: 12, + }, + }, + { + key_as_string: '2020-07-04T19:40:00.000Z', + key: 1593891600000, + doc_count: 16, + count: { + value: 16, + }, + }, + { + key_as_string: '2020-07-04T19:50:00.000Z', + key: 1593892200000, + doc_count: 23, + count: { + value: 23, + }, + }, + { + key_as_string: '2020-07-04T20:00:00.000Z', + key: 1593892800000, + doc_count: 10, + count: { + value: 10, + }, + }, + { + key_as_string: '2020-07-04T20:10:00.000Z', + key: 1593893400000, + doc_count: 10, + count: { + value: 10, + }, + }, + { + key_as_string: '2020-07-04T20:20:00.000Z', + key: 1593894000000, + doc_count: 18, + count: { + value: 18, + }, + }, + { + key_as_string: '2020-07-04T20:30:00.000Z', + key: 1593894600000, + doc_count: 10, + count: { + value: 10, + }, + }, + { + key_as_string: '2020-07-04T20:40:00.000Z', + key: 1593895200000, + doc_count: 17, + count: { + value: 17, + }, + }, + { + key_as_string: '2020-07-04T20:50:00.000Z', + key: 1593895800000, + doc_count: 13, + count: { + value: 13, + }, + }, + { + key_as_string: '2020-07-04T21:00:00.000Z', + key: 1593896400000, + doc_count: 18, + count: { + value: 18, + }, + }, + { + key_as_string: '2020-07-04T21:10:00.000Z', + key: 1593897000000, + doc_count: 17, + count: { + value: 17, + }, + }, + { + key_as_string: '2020-07-04T21:20:00.000Z', + key: 1593897600000, + doc_count: 17, + count: { + value: 17, + }, + }, + { + key_as_string: '2020-07-04T21:30:00.000Z', + key: 1593898200000, + doc_count: 11, + count: { + value: 11, + }, + }, + { + key_as_string: '2020-07-04T21:40:00.000Z', + key: 1593898800000, + doc_count: 10, + count: { + value: 10, + }, + }, + { + key_as_string: '2020-07-04T21:50:00.000Z', + key: 1593899400000, + doc_count: 18, + count: { + value: 18, + }, + }, + { + key_as_string: '2020-07-04T22:00:00.000Z', + key: 1593900000000, + doc_count: 16, + count: { + value: 16, + }, + }, + { + key_as_string: '2020-07-04T22:10:00.000Z', + key: 1593900600000, + doc_count: 12, + count: { + value: 12, + }, + }, + { + key_as_string: '2020-07-04T22:20:00.000Z', + key: 1593901200000, + doc_count: 18, + count: { + value: 18, + }, + }, + { + key_as_string: '2020-07-04T22:30:00.000Z', + key: 1593901800000, + doc_count: 8, + count: { + value: 8, + }, + }, + { + key_as_string: '2020-07-04T22:40:00.000Z', + key: 1593902400000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T22:50:00.000Z', + key: 1593903000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T23:00:00.000Z', + key: 1593903600000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T23:10:00.000Z', + key: 1593904200000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T23:20:00.000Z', + key: 1593904800000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T23:30:00.000Z', + key: 1593905400000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-04T23:40:00.000Z', + key: 1593906000000, + doc_count: 1, + count: { + value: 1, + }, + }, + { + key_as_string: '2020-07-04T23:50:00.000Z', + key: 1593906600000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T00:00:00.000Z', + key: 1593907200000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T00:10:00.000Z', + key: 1593907800000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T00:20:00.000Z', + key: 1593908400000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T00:30:00.000Z', + key: 1593909000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T00:40:00.000Z', + key: 1593909600000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T00:50:00.000Z', + key: 1593910200000, + doc_count: 3, + count: { + value: 3, + }, + }, + { + key_as_string: '2020-07-05T01:00:00.000Z', + key: 1593910800000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T01:10:00.000Z', + key: 1593911400000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T01:20:00.000Z', + key: 1593912000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T01:30:00.000Z', + key: 1593912600000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T01:40:00.000Z', + key: 1593913200000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T01:50:00.000Z', + key: 1593913800000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T02:00:00.000Z', + key: 1593914400000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T02:10:00.000Z', + key: 1593915000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T02:20:00.000Z', + key: 1593915600000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T02:30:00.000Z', + key: 1593916200000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T02:40:00.000Z', + key: 1593916800000, + doc_count: 3, + count: { + value: 3, + }, + }, + { + key_as_string: '2020-07-05T02:50:00.000Z', + key: 1593917400000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T03:00:00.000Z', + key: 1593918000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T03:10:00.000Z', + key: 1593918600000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T03:20:00.000Z', + key: 1593919200000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T03:30:00.000Z', + key: 1593919800000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T03:40:00.000Z', + key: 1593920400000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T03:50:00.000Z', + key: 1593921000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T04:00:00.000Z', + key: 1593921600000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T04:10:00.000Z', + key: 1593922200000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T04:20:00.000Z', + key: 1593922800000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T04:30:00.000Z', + key: 1593923400000, + doc_count: 2, + count: { + value: 2, + }, + }, + { + key_as_string: '2020-07-05T04:40:00.000Z', + key: 1593924000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T04:50:00.000Z', + key: 1593924600000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T05:00:00.000Z', + key: 1593925200000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T05:10:00.000Z', + key: 1593925800000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T05:20:00.000Z', + key: 1593926400000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T05:30:00.000Z', + key: 1593927000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T05:40:00.000Z', + key: 1593927600000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T05:50:00.000Z', + key: 1593928200000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T06:00:00.000Z', + key: 1593928800000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T06:10:00.000Z', + key: 1593929400000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T06:20:00.000Z', + key: 1593930000000, + doc_count: 2, + count: { + value: 2, + }, + }, + { + key_as_string: '2020-07-05T06:30:00.000Z', + key: 1593930600000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T06:40:00.000Z', + key: 1593931200000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T06:50:00.000Z', + key: 1593931800000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T07:00:00.000Z', + key: 1593932400000, + doc_count: 1, + count: { + value: 1, + }, + }, + { + key_as_string: '2020-07-05T07:10:00.000Z', + key: 1593933000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T07:20:00.000Z', + key: 1593933600000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T07:30:00.000Z', + key: 1593934200000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T07:40:00.000Z', + key: 1593934800000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T07:50:00.000Z', + key: 1593935400000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T08:00:00.000Z', + key: 1593936000000, + doc_count: 0, + count: { + value: 0, + }, + }, + { + key_as_string: '2020-07-05T08:10:00.000Z', + key: 1593936600000, + doc_count: 6, + count: { + value: 6, + }, + }, + { + key_as_string: '2020-07-05T08:20:00.000Z', + key: 1593937200000, + doc_count: 17, + count: { + value: 17, + }, + }, + { + key_as_string: '2020-07-05T08:30:00.000Z', + key: 1593937800000, + doc_count: 16, + count: { + value: 16, + }, + }, + { + key_as_string: '2020-07-05T08:40:00.000Z', + key: 1593938400000, + doc_count: 5, + count: { + value: 5, + }, + }, + ], + }, + }, + ], + }, + response_times: { + buckets: [ + { + key_as_string: '2020-07-04T08:40:00.000Z', + key: 1593852000000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T08:50:00.000Z', + key: 1593852600000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T09:00:00.000Z', + key: 1593853200000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T09:10:00.000Z', + key: 1593853800000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T09:20:00.000Z', + key: 1593854400000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T09:30:00.000Z', + key: 1593855000000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T09:40:00.000Z', + key: 1593855600000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T09:50:00.000Z', + key: 1593856200000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T10:00:00.000Z', + key: 1593856800000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T10:10:00.000Z', + key: 1593857400000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T10:20:00.000Z', + key: 1593858000000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T10:30:00.000Z', + key: 1593858600000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T10:40:00.000Z', + key: 1593859200000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T10:50:00.000Z', + key: 1593859800000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T11:00:00.000Z', + key: 1593860400000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T11:10:00.000Z', + key: 1593861000000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T11:20:00.000Z', + key: 1593861600000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T11:30:00.000Z', + key: 1593862200000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T11:40:00.000Z', + key: 1593862800000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T11:50:00.000Z', + key: 1593863400000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T12:00:00.000Z', + key: 1593864000000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T12:10:00.000Z', + key: 1593864600000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T12:20:00.000Z', + key: 1593865200000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T12:30:00.000Z', + key: 1593865800000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T12:40:00.000Z', + key: 1593866400000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T12:50:00.000Z', + key: 1593867000000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T13:00:00.000Z', + key: 1593867600000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T13:10:00.000Z', + key: 1593868200000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T13:20:00.000Z', + key: 1593868800000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T13:30:00.000Z', + key: 1593869400000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T13:40:00.000Z', + key: 1593870000000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T13:50:00.000Z', + key: 1593870600000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T14:00:00.000Z', + key: 1593871200000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T14:10:00.000Z', + key: 1593871800000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T14:20:00.000Z', + key: 1593872400000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T14:30:00.000Z', + key: 1593873000000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T14:40:00.000Z', + key: 1593873600000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T14:50:00.000Z', + key: 1593874200000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T15:00:00.000Z', + key: 1593874800000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T15:10:00.000Z', + key: 1593875400000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T15:20:00.000Z', + key: 1593876000000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T15:30:00.000Z', + key: 1593876600000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T15:40:00.000Z', + key: 1593877200000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T15:50:00.000Z', + key: 1593877800000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T16:00:00.000Z', + key: 1593878400000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T16:10:00.000Z', + key: 1593879000000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T16:20:00.000Z', + key: 1593879600000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T16:30:00.000Z', + key: 1593880200000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T16:40:00.000Z', + key: 1593880800000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T16:50:00.000Z', + key: 1593881400000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T17:00:00.000Z', + key: 1593882000000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T17:10:00.000Z', + key: 1593882600000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T17:20:00.000Z', + key: 1593883200000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T17:30:00.000Z', + key: 1593883800000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T17:40:00.000Z', + key: 1593884400000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T17:50:00.000Z', + key: 1593885000000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T18:00:00.000Z', + key: 1593885600000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T18:10:00.000Z', + key: 1593886200000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T18:20:00.000Z', + key: 1593886800000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T18:30:00.000Z', + key: 1593887400000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, + }, + avg: { + value: null, + }, + }, + { + key_as_string: '2020-07-04T18:40:00.000Z', + key: 1593888000000, + doc_count: 0, + pct: { + values: { + '95.0': null, + '99.0': null, + }, }, avg: { - value: 26310.63483891513, + value: null, }, }, { - key_as_string: '2018-06-04T15:00:00.000Z', - key: 1528124400000, - doc_count: 18708, + key_as_string: '2020-07-04T18:50:00.000Z', + key: 1593888600000, + doc_count: 247, pct: { values: { - '95.0': 80738.78571428556, - '99.0': 293257.27333333343, + '95.0': 114680.0, + '99.0': 827384.0, }, }, avg: { - value: 26193.277795595466, + value: 43364.46153846154, }, }, { - key_as_string: '2018-06-04T18:00:00.000Z', - key: 1528135200000, - doc_count: 18865, + key_as_string: '2020-07-04T19:00:00.000Z', + key: 1593889200000, + doc_count: 542, pct: { values: { - '95.0': 77058.03529411761, - '99.0': 290195.8800000004, + '95.0': 659448.0, + '99.0': 2326520.0, }, }, avg: { - value: 25291.787065995228, + value: 147903.58671586716, }, }, { - key_as_string: '2018-06-04T21:00:00.000Z', - key: 1528146000000, - doc_count: 18889, + key_as_string: '2020-07-04T19:10:00.000Z', + key: 1593889800000, + doc_count: 619, pct: { values: { - '95.0': 77892.20721980717, - '99.0': 278548.1649999994, + '95.0': 122360.0, + '99.0': 1130488.0, }, }, avg: { - value: 24690.306474667796, + value: 57370.52342487884, }, }, { - key_as_string: '2018-06-05T00:00:00.000Z', - key: 1528156800000, - doc_count: 19270, + key_as_string: '2020-07-04T19:20:00.000Z', + key: 1593890400000, + doc_count: 688, pct: { values: { - '95.0': 77085.86687499998, - '99.0': 290701.8973333341, + '95.0': 121336.0, + '99.0': 1032184.0, }, }, avg: { - value: 24809.8953814219, + value: 59687.82558139535, }, }, { - key_as_string: '2018-06-05T03:00:00.000Z', - key: 1528167600000, - doc_count: 19024, + key_as_string: '2020-07-04T19:30:00.000Z', + key: 1593891000000, + doc_count: 646, pct: { values: { - '95.0': 80048.3462981744, - '99.0': 286839.5897777779, + '95.0': 120828.0, + '99.0': 770044.0, }, }, avg: { - value: 25460.0394764508, + value: 51810.68111455108, }, }, { - key_as_string: '2018-06-05T06:00:00.000Z', - key: 1528178400000, - doc_count: 18923, + key_as_string: '2020-07-04T19:40:00.000Z', + key: 1593891600000, + doc_count: 621, pct: { values: { - '95.0': 84089.21370223971, - '99.0': 287979.5149999999, + '95.0': 139256.0, + '99.0': 651256.0, }, }, avg: { - value: 26360.440733498916, + value: 51736.59420289855, }, }, { - key_as_string: '2018-06-05T09:00:00.000Z', - key: 1528189200000, - doc_count: 18834, + key_as_string: '2020-07-04T19:50:00.000Z', + key: 1593892200000, + doc_count: 856, pct: { values: { - '95.0': 84880.90143416924, - '99.0': 300107.5009999992, + '95.0': 76792.0, + '99.0': 667640.0, }, }, avg: { - value: 27050.95205479452, + value: 37241.293224299065, }, }, { - key_as_string: '2018-06-05T12:00:00.000Z', - key: 1528200000000, - doc_count: 18694, + key_as_string: '2020-07-04T20:00:00.000Z', + key: 1593892800000, + doc_count: 661, pct: { values: { - '95.0': 84554.8884781166, - '99.0': 294402.2179999999, + '95.0': 129528.0, + '99.0': 708600.0, }, }, avg: { - value: 26555.857333903925, + value: 49444.90771558245, }, }, { - key_as_string: '2018-06-05T15:00:00.000Z', - key: 1528210800000, - doc_count: 19184, + key_as_string: '2020-07-04T20:10:00.000Z', + key: 1593893400000, + doc_count: 646, pct: { values: { - '95.0': 81839.39583333326, - '99.0': 289849.459333332, + '95.0': 378872.0, + '99.0': 815096.0, }, }, avg: { - value: 26164.343359049206, + value: 56807.80495356037, }, }, { - key_as_string: '2018-06-05T18:00:00.000Z', - key: 1528221600000, - doc_count: 18850, + key_as_string: '2020-07-04T20:20:00.000Z', + key: 1593894000000, + doc_count: 781, pct: { values: { - '95.0': 85993.55410163336, - '99.0': 296942.86299999955, + '95.0': 97272.0, + '99.0': 688120.0, }, }, avg: { - value: 26989.84546419098, + value: 43238.74519846351, }, }, { - key_as_string: '2018-06-05T21:00:00.000Z', - key: 1528232400000, - doc_count: 18897, + key_as_string: '2020-07-04T20:30:00.000Z', + key: 1593894600000, + doc_count: 670, pct: { values: { - '95.0': 85001.44588628765, - '99.0': 292048.20571428596, + '95.0': 102904.0, + '99.0': 978936.0, }, }, avg: { - value: 26314.409430068266, + value: 51754.80149253731, }, }, { - key_as_string: '2018-06-06T00:00:00.000Z', - key: 1528243200000, - doc_count: 18942, + key_as_string: '2020-07-04T20:40:00.000Z', + key: 1593895200000, + doc_count: 617, pct: { values: { - '95.0': 86980.16445312503, - '99.0': 299308.7371666667, + '95.0': 100856.0, + '99.0': 839672.0, }, }, avg: { - value: 27460.774575018477, + value: 47166.5964343598, }, }, { - key_as_string: '2018-06-06T03:00:00.000Z', - key: 1528254000000, - doc_count: 19147, + key_as_string: '2020-07-04T20:50:00.000Z', + key: 1593895800000, + doc_count: 690, pct: { values: { - '95.0': 84961.8710743802, - '99.0': 292151.2377777781, + '95.0': 97784.0, + '99.0': 757752.0, }, }, avg: { - value: 26461.469107431974, + value: 41854.688405797104, }, }, { - key_as_string: '2018-06-06T06:00:00.000Z', - key: 1528264800000, - doc_count: 18853, + key_as_string: '2020-07-04T21:00:00.000Z', + key: 1593896400000, + doc_count: 843, pct: { values: { - '95.0': 88906.54601889332, - '99.0': 302274.4192592592, + '95.0': 72700.0, + '99.0': 577532.0, }, }, avg: { - value: 27657.584946692834, + value: 30464.317912218266, }, }, { - key_as_string: '2018-06-06T09:00:00.000Z', - key: 1528275600000, - doc_count: 18609, + key_as_string: '2020-07-04T21:10:00.000Z', + key: 1593897000000, + doc_count: 717, pct: { values: { - '95.0': 90198.34708994703, - '99.0': 299457.1612121209, + '95.0': 98296.0, + '99.0': 618488.0, }, }, avg: { - value: 27940.445967005213, + value: 41558.531380753135, }, }, { - key_as_string: '2018-06-06T12:00:00.000Z', - key: 1528286400000, - doc_count: 21402, + key_as_string: '2020-07-04T21:20:00.000Z', + key: 1593897600000, + doc_count: 695, pct: { values: { - '95.0': 135627.71242424246, - '99.0': 350398.59259259375, + '95.0': 112120.0, + '99.0': 565240.0, }, }, avg: { - value: 34454.377581534434, + value: 41159.68345323741, }, }, { - key_as_string: '2018-06-06T15:00:00.000Z', - key: 1528297200000, - doc_count: 19051, + key_as_string: '2020-07-04T21:30:00.000Z', + key: 1593898200000, + doc_count: 731, pct: { values: { - '95.0': 167037.1993837535, - '99.0': 421204.23333333334, + '95.0': 91640.0, + '99.0': 618488.0, }, }, avg: { - value: 44024.31809353839, + value: 34211.03967168263, }, }, { - key_as_string: '2018-06-06T18:00:00.000Z', - key: 1528308000000, - doc_count: 19020, + key_as_string: '2020-07-04T21:40:00.000Z', + key: 1593898800000, + doc_count: 676, pct: { values: { - '95.0': 128293.12184873945, - '99.0': 368166.68976190523, + '95.0': 83448.0, + '99.0': 655352.0, }, }, avg: { - value: 36374.53333333333, + value: 41322.30621301775, }, }, { - key_as_string: '2018-06-06T21:00:00.000Z', - key: 1528318800000, - doc_count: 18582, + key_as_string: '2020-07-04T21:50:00.000Z', + key: 1593899400000, + doc_count: 699, pct: { values: { - '95.0': 130653.54236263742, - '99.0': 367193.6128571426, + '95.0': 84476.0, + '99.0': 843772.0, }, }, avg: { - value: 36991.29442471209, + value: 42301.523605150214, }, }, { - key_as_string: '2018-06-07T00:00:00.000Z', - key: 1528329600000, - doc_count: 18875, + key_as_string: '2020-07-04T22:00:00.000Z', + key: 1593900000000, + doc_count: 685, pct: { values: { - '95.0': 131630.8902645502, - '99.0': 375658.10190476174, + '95.0': 117756.0, + '99.0': 831484.0, }, }, avg: { - value: 37178.002701986756, + value: 59615.69343065693, }, }, { - key_as_string: '2018-06-07T03:00:00.000Z', - key: 1528340400000, - doc_count: 18993, + key_as_string: '2020-07-04T22:10:00.000Z', + key: 1593900600000, + doc_count: 798, pct: { values: { - '95.0': 133581.33541666638, - '99.0': 368152.03822222137, + '95.0': 66556.0, + '99.0': 430076.0, }, }, avg: { - value: 37605.57078923814, + value: 29567.520050125313, }, }, { - key_as_string: '2018-06-07T06:00:00.000Z', - key: 1528351200000, - doc_count: 19037, + key_as_string: '2020-07-04T22:20:00.000Z', + key: 1593901200000, + doc_count: 640, pct: { values: { - '95.0': 132697.92762266204, - '99.0': 365705.8319999995, + '95.0': 130552.0, + '99.0': 864248.0, }, }, avg: { - value: 37319.89767295267, + value: 56104.7484375, }, }, { - key_as_string: '2018-06-07T09:00:00.000Z', - key: 1528362000000, - doc_count: 18985, + key_as_string: '2020-07-04T22:30:00.000Z', + key: 1593901800000, + doc_count: 241, pct: { values: { - '95.0': 140003.6918918918, - '99.0': 380075.48533333326, + '95.0': 111608.0, + '99.0': 655352.0, }, }, avg: { - value: 38709.5041348433, + value: 40900.70954356847, }, }, { - key_as_string: '2018-06-07T12:00:00.000Z', - key: 1528372800000, - doc_count: 18505, + key_as_string: '2020-07-04T22:40:00.000Z', + key: 1593902400000, + doc_count: 0, pct: { values: { - '95.0': 138149.5673529411, - '99.0': 375697.1923809518, + '95.0': null, + '99.0': null, }, }, avg: { - value: 38140.131856255066, + value: null, }, }, { - key_as_string: '2018-06-07T15:00:00.000Z', - key: 1528383600000, - doc_count: 18991, + key_as_string: '2020-07-04T22:50:00.000Z', + key: 1593903000000, + doc_count: 0, pct: { values: { - '95.0': 121872.37504835591, - '99.0': 351080.94111111073, + '95.0': null, + '99.0': null, }, }, avg: { - value: 34564.81091043125, + value: null, }, }, { - key_as_string: '2018-06-07T18:00:00.000Z', - key: 1528394400000, - doc_count: 18917, + key_as_string: '2020-07-04T23:00:00.000Z', + key: 1593903600000, + doc_count: 50, pct: { values: { - '95.0': 116378.03873517792, - '99.0': 339294.12799999997, + '95.0': 276448.0, + '99.0': 2883552.0, }, }, avg: { - value: 33256.37743828302, + value: 141618.04, }, }, { - key_as_string: '2018-06-07T21:00:00.000Z', - key: 1528405200000, - doc_count: 18744, + key_as_string: '2020-07-04T23:10:00.000Z', + key: 1593904200000, + doc_count: 0, pct: { values: { - '95.0': 131545.40999999995, - '99.0': 378902.90649999987, + '95.0': null, + '99.0': null, }, }, avg: { - value: 37251.5625266752, + value: null, }, }, { - key_as_string: '2018-06-08T00:00:00.000Z', - key: 1528416000000, - doc_count: 19157, + key_as_string: '2020-07-04T23:20:00.000Z', + key: 1593904800000, + doc_count: 0, pct: { values: { - '95.0': 133111.25804878055, - '99.0': 384483.3233333327, + '95.0': null, + '99.0': null, }, }, avg: { - value: 38681.89084929791, + value: null, }, }, { - key_as_string: '2018-06-08T03:00:00.000Z', - key: 1528426800000, - doc_count: 18552, + key_as_string: '2020-07-04T23:30:00.000Z', + key: 1593905400000, + doc_count: 0, pct: { values: { - '95.0': 144821.9855278593, - '99.0': 394692.25000000105, + '95.0': null, + '99.0': null, }, }, avg: { - value: 40677.801045709355, + value: null, }, }, { - key_as_string: '2018-06-08T06:00:00.000Z', - key: 1528437600000, - doc_count: 18994, + key_as_string: '2020-07-04T23:40:00.000Z', + key: 1593906000000, + doc_count: 41, pct: { values: { - '95.0': 134737.3997727272, - '99.0': 403362.50399999996, + '95.0': 1028088.0, + '99.0': 6094840.0, }, }, avg: { - value: 39987.86453616932, + value: 380742.48780487804, }, }, { - key_as_string: '2018-06-08T09:00:00.000Z', - key: 1528448400000, - doc_count: 18798, + key_as_string: '2020-07-04T23:50:00.000Z', + key: 1593906600000, + doc_count: 0, pct: { values: { - '95.0': 141206.57726666646, - '99.0': 396559.0274999993, + '95.0': null, + '99.0': null, }, }, avg: { - value: 41059.392914139804, + value: null, }, }, { - key_as_string: '2018-06-08T12:00:00.000Z', - key: 1528459200000, - doc_count: 19097, + key_as_string: '2020-07-05T00:00:00.000Z', + key: 1593907200000, + doc_count: 0, pct: { values: { - '95.0': 137731.8994082841, - '99.0': 371815.8320000008, + '95.0': null, + '99.0': null, }, }, avg: { - value: 39630.710111535845, + value: null, }, }, { - key_as_string: '2018-06-08T15:00:00.000Z', - key: 1528470000000, - doc_count: 18887, + key_as_string: '2020-07-05T00:10:00.000Z', + key: 1593907800000, + doc_count: 0, pct: { values: { - '95.0': 141476.23189033198, - '99.0': 405477.6133333326, + '95.0': null, + '99.0': null, }, }, avg: { - value: 41561.81331074284, + value: null, }, }, { - key_as_string: '2018-06-08T18:00:00.000Z', - key: 1528480800000, - doc_count: 18949, + key_as_string: '2020-07-05T00:20:00.000Z', + key: 1593908400000, + doc_count: 0, pct: { values: { - '95.0': 149636.31340909077, - '99.0': 413542.18133333366, + '95.0': null, + '99.0': null, }, }, avg: { - value: 43079.490738297536, + value: null, }, }, { - key_as_string: '2018-06-08T21:00:00.000Z', - key: 1528491600000, - doc_count: 18786, + key_as_string: '2020-07-05T00:30:00.000Z', + key: 1593909000000, + doc_count: 0, pct: { values: { - '95.0': 151934.55000000002, - '99.0': 424399.340000001, + '95.0': null, + '99.0': null, }, }, avg: { - value: 43925.39609283509, + value: null, }, }, { - key_as_string: '2018-06-09T00:00:00.000Z', - key: 1528502400000, - doc_count: 5096, + key_as_string: '2020-07-05T00:40:00.000Z', + key: 1593909600000, + doc_count: 0, pct: { values: { - '95.0': 82198.17857142858, - '99.0': 303815.9000000001, + '95.0': null, + '99.0': null, }, }, avg: { - value: 25821.91424646782, + value: null, }, }, { - key_as_string: '2018-06-09T03:00:00.000Z', - key: 1528513200000, - doc_count: 5104, + key_as_string: '2020-07-05T00:50:00.000Z', + key: 1593910200000, + doc_count: 37, pct: { values: { - '95.0': 85946.43199999983, - '99.0': 306305.0800000006, + '95.0': 352128.0, + '99.0': 446336.0, }, }, avg: { - value: 27343.60011755486, + value: 122524.7027027027, }, }, { - key_as_string: '2018-06-09T06:00:00.000Z', - key: 1528524000000, - doc_count: 5122, + key_as_string: '2020-07-05T01:00:00.000Z', + key: 1593910800000, + doc_count: 0, pct: { values: { - '95.0': 78617.66249999996, - '99.0': 297521.94999999984, + '95.0': null, + '99.0': null, }, }, avg: { - value: 25249.95060523233, + value: null, }, }, { - key_as_string: '2018-06-09T09:00:00.000Z', - key: 1528534800000, - doc_count: 5184, + key_as_string: '2020-07-05T01:10:00.000Z', + key: 1593911400000, + doc_count: 0, pct: { values: { - '95.0': 79606.48333333322, - '99.0': 317938.0900000003, + '95.0': null, + '99.0': null, }, }, avg: { - value: 25492.77199074074, + value: null, }, }, { - key_as_string: '2018-06-09T12:00:00.000Z', - key: 1528545600000, - doc_count: 5279, + key_as_string: '2020-07-05T01:20:00.000Z', + key: 1593912000000, + doc_count: 0, pct: { values: { - '95.0': 76297.93999999986, - '99.0': 312262.3000000003, + '95.0': null, + '99.0': null, }, }, avg: { - value: 25991.647281682137, + value: null, }, }, { - key_as_string: '2018-06-09T15:00:00.000Z', - key: 1528556400000, - doc_count: 5254, + key_as_string: '2020-07-05T01:30:00.000Z', + key: 1593912600000, + doc_count: 0, pct: { values: { - '95.0': 80742.63333333324, - '99.0': 318428.8700000002, + '95.0': null, + '99.0': null, }, }, avg: { - value: 26273.31290445375, + value: null, }, }, { - key_as_string: '2018-06-09T18:00:00.000Z', - key: 1528567200000, - doc_count: 5082, + key_as_string: '2020-07-05T01:40:00.000Z', + key: 1593913200000, + doc_count: 0, pct: { values: { - '95.0': 81291.45969696966, - '99.0': 295421.4099999999, + '95.0': null, + '99.0': null, }, }, avg: { - value: 26234.98976780795, + value: null, }, }, { - key_as_string: '2018-06-09T21:00:00.000Z', - key: 1528578000000, - doc_count: 5150, + key_as_string: '2020-07-05T01:50:00.000Z', + key: 1593913800000, + doc_count: 0, pct: { values: { - '95.0': 73467.02500000004, - '99.0': 293067.86000000004, + '95.0': null, + '99.0': null, }, }, avg: { - value: 23494.54873786408, + value: null, }, }, { - key_as_string: '2018-06-10T00:00:00.000Z', - key: 1528588800000, - doc_count: 5103, + key_as_string: '2020-07-05T02:00:00.000Z', + key: 1593914400000, + doc_count: 0, pct: { values: { - '95.0': 69177.66999999993, - '99.0': 264935.71999999933, + '95.0': null, + '99.0': null, }, }, avg: { - value: 22008.80482069371, + value: null, }, }, { - key_as_string: '2018-06-10T03:00:00.000Z', - key: 1528599600000, - doc_count: 5137, + key_as_string: '2020-07-05T02:10:00.000Z', + key: 1593915000000, + doc_count: 0, pct: { values: { - '95.0': 71956.06111111109, - '99.0': 282795.0400000003, + '95.0': null, + '99.0': null, }, }, avg: { - value: 22828.136655635586, + value: null, }, }, { - key_as_string: '2018-06-10T06:00:00.000Z', - key: 1528610400000, - doc_count: 5184, + key_as_string: '2020-07-05T02:20:00.000Z', + key: 1593915600000, + doc_count: 0, pct: { values: { - '95.0': 68480.91142857139, - '99.0': 285390.8400000001, + '95.0': null, + '99.0': null, }, }, avg: { - value: 22138.7081404321, + value: null, }, }, { - key_as_string: '2018-06-10T09:00:00.000Z', - key: 1528621200000, - doc_count: 4993, + key_as_string: '2020-07-05T02:30:00.000Z', + key: 1593916200000, + doc_count: 0, pct: { values: { - '95.0': 68957.0999999999, - '99.0': 290402.24, + '95.0': null, + '99.0': null, }, }, avg: { - value: 22634.985579811735, + value: null, }, }, { - key_as_string: '2018-06-10T12:00:00.000Z', - key: 1528632000000, - doc_count: 5210, + key_as_string: '2020-07-05T02:40:00.000Z', + key: 1593916800000, + doc_count: 37, pct: { values: { - '95.0': 67489.50416666668, - '99.0': 293655.53, + '95.0': 348144.0, + '99.0': 3293168.0, }, }, avg: { - value: 22202.780998080616, + value: 160060.1081081081, }, }, { - key_as_string: '2018-06-10T15:00:00.000Z', - key: 1528642800000, - doc_count: 5122, + key_as_string: '2020-07-05T02:50:00.000Z', + key: 1593917400000, + doc_count: 0, pct: { values: { - '95.0': 71556.91249999998, - '99.0': 292723.56999999995, + '95.0': null, + '99.0': null, }, }, avg: { - value: 23084.082780163997, + value: null, }, }, { - key_as_string: '2018-06-10T18:00:00.000Z', - key: 1528653600000, - doc_count: 5125, + key_as_string: '2020-07-05T03:00:00.000Z', + key: 1593918000000, + doc_count: 0, pct: { values: { - '95.0': 72157.65128205132, - '99.0': 301051.32000000105, + '95.0': null, + '99.0': null, }, }, avg: { - value: 23109.666146341464, + value: null, }, }, { - key_as_string: '2018-06-10T21:00:00.000Z', - key: 1528664400000, - doc_count: 5186, + key_as_string: '2020-07-05T03:10:00.000Z', + key: 1593918600000, + doc_count: 0, pct: { values: { - '95.0': 76124.5625, - '99.0': 291322.0499999998, + '95.0': null, + '99.0': null, }, }, avg: { - value: 23306.89028152719, + value: null, }, }, { - key_as_string: '2018-06-11T00:00:00.000Z', - key: 1528675200000, - doc_count: 18631, + key_as_string: '2020-07-05T03:20:00.000Z', + key: 1593919200000, + doc_count: 0, pct: { values: { - '95.0': 141709.34661835746, - '99.0': 379855.2444444447, + '95.0': null, + '99.0': null, }, }, avg: { - value: 39341.022704095325, + value: null, }, }, { - key_as_string: '2018-06-11T03:00:00.000Z', - key: 1528686000000, - doc_count: 19349, + key_as_string: '2020-07-05T03:30:00.000Z', + key: 1593919800000, + doc_count: 0, pct: { values: { - '95.0': 132371.48641975303, - '99.0': 371175.2592000001, + '95.0': null, + '99.0': null, }, }, avg: { - value: 37467.17153341258, + value: null, }, }, { - key_as_string: '2018-06-11T06:00:00.000Z', - key: 1528696800000, - doc_count: 18586, + key_as_string: '2020-07-05T03:40:00.000Z', + key: 1593920400000, + doc_count: 0, pct: { values: { - '95.0': 186783.51503759398, - '99.0': 498378.4238888898, + '95.0': null, + '99.0': null, }, }, avg: { - value: 52457.50554180566, + value: null, }, }, { - key_as_string: '2018-06-11T09:00:00.000Z', - key: 1528707600000, - doc_count: 18887, + key_as_string: '2020-07-05T03:50:00.000Z', + key: 1593921000000, + doc_count: 0, pct: { values: { - '95.0': 99540.17819499348, - '99.0': 331118.6599999997, + '95.0': null, + '99.0': null, }, }, avg: { - value: 31327.95780166252, + value: null, }, }, { - key_as_string: '2018-06-11T12:00:00.000Z', - key: 1528718400000, - doc_count: 18866, + key_as_string: '2020-07-05T04:00:00.000Z', + key: 1593921600000, + doc_count: 0, pct: { values: { - '95.0': 95982.62454212455, - '99.0': 328101.3999999988, + '95.0': null, + '99.0': null, }, }, avg: { - value: 30695.334941163997, + value: null, }, }, { - key_as_string: '2018-06-11T15:00:00.000Z', - key: 1528729200000, - doc_count: 19469, + key_as_string: '2020-07-05T04:10:00.000Z', + key: 1593922200000, + doc_count: 0, pct: { values: { - '95.0': 89559.3525925925, - '99.0': 313951.54249999986, + '95.0': null, + '99.0': null, }, }, avg: { - value: 28895.042785967435, + value: null, }, }, { - key_as_string: '2018-06-11T18:00:00.000Z', - key: 1528740000000, - doc_count: 18767, + key_as_string: '2020-07-05T04:20:00.000Z', + key: 1593922800000, + doc_count: 0, pct: { values: { - '95.0': 95769.83153735634, - '99.0': 323340.5274074075, + '95.0': null, + '99.0': null, }, }, avg: { - value: 30649.363989982416, + value: null, }, }, { - key_as_string: '2018-06-11T21:00:00.000Z', - key: 1528750800000, - doc_count: 19006, + key_as_string: '2020-07-05T04:30:00.000Z', + key: 1593923400000, + doc_count: 64, pct: { values: { - '95.0': 94063.90833755062, - '99.0': 315055.5047619052, + '95.0': 270328.0, + '99.0': 299000.0, }, }, avg: { - value: 29802.63622014101, + value: 70357.234375, }, }, { - key_as_string: '2018-06-12T00:00:00.000Z', - key: 1528761600000, - doc_count: 19082, + key_as_string: '2020-07-05T04:40:00.000Z', + key: 1593924000000, + doc_count: 0, pct: { values: { - '95.0': 96399.67269119772, - '99.0': 330070.03599999985, + '95.0': null, + '99.0': null, }, }, avg: { - value: 30759.03002829892, + value: null, }, }, { - key_as_string: '2018-06-12T03:00:00.000Z', - key: 1528772400000, - doc_count: 18908, + key_as_string: '2020-07-05T04:50:00.000Z', + key: 1593924600000, + doc_count: 0, pct: { values: { - '95.0': 96436.42520161276, - '99.0': 320531.54416666675, + '95.0': null, + '99.0': null, }, }, avg: { - value: 30399.76549608631, + value: null, }, }, { - key_as_string: '2018-06-12T06:00:00.000Z', - key: 1528783200000, - doc_count: 19055, + key_as_string: '2020-07-05T05:00:00.000Z', + key: 1593925200000, + doc_count: 0, pct: { values: { - '95.0': 91860.16988095238, - '99.0': 315137.16628571344, + '95.0': null, + '99.0': null, }, }, avg: { - value: 29421.610233534506, + value: null, }, }, { - key_as_string: '2018-06-12T09:00:00.000Z', - key: 1528794000000, - doc_count: 19047, + key_as_string: '2020-07-05T05:10:00.000Z', + key: 1593925800000, + doc_count: 0, pct: { values: { - '95.0': 105989.8333333334, - '99.0': 337251.4042424246, + '95.0': null, + '99.0': null, }, }, avg: { - value: 32641.679897096656, + value: null, }, }, { - key_as_string: '2018-06-12T12:00:00.000Z', - key: 1528804800000, - doc_count: 18733, + key_as_string: '2020-07-05T05:20:00.000Z', + key: 1593926400000, + doc_count: 0, pct: { values: { - '95.0': 97937.60342555979, - '99.0': 327054.9243636365, + '95.0': null, + '99.0': null, }, }, avg: { - value: 30621.65440666204, + value: null, }, }, { - key_as_string: '2018-06-12T15:00:00.000Z', - key: 1528815600000, - doc_count: 19079, + key_as_string: '2020-07-05T05:30:00.000Z', + key: 1593927000000, + doc_count: 0, pct: { values: { - '95.0': 98967.2249999999, - '99.0': 327653.0000000006, + '95.0': null, + '99.0': null, }, }, avg: { - value: 31039.60391005818, + value: null, }, }, { - key_as_string: '2018-06-12T18:00:00.000Z', - key: 1528826400000, - doc_count: 18907, + key_as_string: '2020-07-05T05:40:00.000Z', + key: 1593927600000, + doc_count: 0, pct: { values: { - '95.0': 97561.02469135808, - '99.0': 324505.1399999999, + '95.0': null, + '99.0': null, }, }, avg: { - value: 30954.760723541545, + value: null, }, }, { - key_as_string: '2018-06-12T21:00:00.000Z', - key: 1528837200000, - doc_count: 18971, + key_as_string: '2020-07-05T05:50:00.000Z', + key: 1593928200000, + doc_count: 0, pct: { values: { - '95.0': 102557.78813357186, - '99.0': 338040.3999999998, + '95.0': null, + '99.0': null, }, }, avg: { - value: 31902.050234568553, + value: null, }, }, { - key_as_string: '2018-06-13T00:00:00.000Z', - key: 1528848000000, - doc_count: 18899, + key_as_string: '2020-07-05T06:00:00.000Z', + key: 1593928800000, + doc_count: 0, pct: { values: { - '95.0': 100137.87578595306, - '99.0': 328600.5173333335, + '95.0': null, + '99.0': null, }, }, avg: { - value: 31594.350653473728, + value: null, }, }, { - key_as_string: '2018-06-13T03:00:00.000Z', - key: 1528858800000, - doc_count: 19182, + key_as_string: '2020-07-05T06:10:00.000Z', + key: 1593929400000, + doc_count: 0, pct: { values: { - '95.0': 98412.97120445351, - '99.0': 334060.93628571345, + '95.0': null, + '99.0': null, }, }, avg: { - value: 31343.87243248879, + value: null, }, }, { - key_as_string: '2018-06-13T06:00:00.000Z', - key: 1528869600000, - doc_count: 19030, + key_as_string: '2020-07-05T06:20:00.000Z', + key: 1593930000000, + doc_count: 83, pct: { values: { - '95.0': 101607.8328012912, - '99.0': 328569.4964999998, + '95.0': 1687544.0, + '99.0': 5046264.0, }, }, avg: { - value: 31200.14450867052, + value: 269745.9036144578, }, }, { - key_as_string: '2018-06-13T09:00:00.000Z', - key: 1528880400000, - doc_count: 19257, + key_as_string: '2020-07-05T06:30:00.000Z', + key: 1593930600000, + doc_count: 0, pct: { values: { - '95.0': 92000.51368421057, - '99.0': 320227.32399999973, + '95.0': null, + '99.0': null, }, }, avg: { - value: 28560.946668743833, + value: null, }, }, { - key_as_string: '2018-06-13T12:00:00.000Z', - key: 1528891200000, - doc_count: 19348, + key_as_string: '2020-07-05T06:40:00.000Z', + key: 1593931200000, + doc_count: 0, pct: { values: { - '95.0': 78027.29473684198, - '99.0': 292019.2899999998, + '95.0': null, + '99.0': null, }, }, avg: { - value: 24700.216146371717, + value: null, }, }, { - key_as_string: '2018-06-13T15:00:00.000Z', - key: 1528902000000, - doc_count: 19119, + key_as_string: '2020-07-05T06:50:00.000Z', + key: 1593931800000, + doc_count: 0, pct: { values: { - '95.0': 80762.078801789, - '99.0': 297757.72666666657, + '95.0': null, + '99.0': null, }, }, avg: { - value: 25261.025210523563, + value: null, }, }, { - key_as_string: '2018-06-13T18:00:00.000Z', - key: 1528912800000, - doc_count: 19206, + key_as_string: '2020-07-05T07:00:00.000Z', + key: 1593932400000, + doc_count: 42, pct: { values: { - '95.0': 81160.83425925927, - '99.0': 308034.4466666669, + '95.0': 798656.0, + '99.0': 4292544.0, }, }, avg: { - value: 26041.39789649068, + value: 313349.95238095237, }, }, { - key_as_string: '2018-06-13T21:00:00.000Z', - key: 1528923600000, - doc_count: 19078, + key_as_string: '2020-07-05T07:10:00.000Z', + key: 1593933000000, + doc_count: 0, pct: { values: { - '95.0': 84215.58945578222, - '99.0': 301128.4895238093, + '95.0': null, + '99.0': null, }, }, avg: { - value: 26123.556295209142, + value: null, }, }, { - key_as_string: '2018-06-14T00:00:00.000Z', - key: 1528934400000, - doc_count: 19551, + key_as_string: '2020-07-05T07:20:00.000Z', + key: 1593933600000, + doc_count: 0, pct: { values: { - '95.0': 194188.21428571426, - '99.0': 447266.9, + '95.0': null, + '99.0': null, }, }, avg: { - value: 46231.36177177638, + value: null, }, }, { - key_as_string: '2018-06-14T03:00:00.000Z', - key: 1528945200000, - doc_count: 18888, + key_as_string: '2020-07-05T07:30:00.000Z', + key: 1593934200000, + doc_count: 0, pct: { values: { - '95.0': 172616.2293896504, - '99.0': 409147.332500001, + '95.0': null, + '99.0': null, }, }, avg: { - value: 45350.42005506141, + value: null, }, }, { - key_as_string: '2018-06-14T06:00:00.000Z', - key: 1528956000000, - doc_count: 18823, + key_as_string: '2020-07-05T07:40:00.000Z', + key: 1593934800000, + doc_count: 0, pct: { values: { - '95.0': 182653.81858220184, - '99.0': 423121.9773333328, + '95.0': null, + '99.0': null, }, }, avg: { - value: 48256.049354513096, + value: null, }, }, { - key_as_string: '2018-06-14T09:00:00.000Z', - key: 1528966800000, - doc_count: 18766, + key_as_string: '2020-07-05T07:50:00.000Z', + key: 1593935400000, + doc_count: 0, pct: { values: { - '95.0': 194970.75667682925, - '99.0': 473485.4199999998, + '95.0': null, + '99.0': null, }, }, avg: { - value: 52360.30017052116, + value: null, }, }, { - key_as_string: '2018-06-14T12:00:00.000Z', - key: 1528977600000, + key_as_string: '2020-07-05T08:00:00.000Z', + key: 1593936000000, doc_count: 0, pct: { values: { - '95.0': 'NaN', - '99.0': 'NaN', + '95.0': null, + '99.0': null, }, }, avg: { value: null, }, }, + { + key_as_string: '2020-07-05T08:10:00.000Z', + key: 1593936600000, + doc_count: 215, + pct: { + values: { + '95.0': 3653624.0, + '99.0': 5046264.0, + }, + }, + avg: { + value: 397251.288372093, + }, + }, + { + key_as_string: '2020-07-05T08:20:00.000Z', + key: 1593937200000, + doc_count: 494, + pct: { + values: { + '95.0': 3276768.0, + '99.0': 4292576.0, + }, + }, + avg: { + value: 361953.5931174089, + }, + }, + { + key_as_string: '2020-07-05T08:30:00.000Z', + key: 1593937800000, + doc_count: 518, + pct: { + values: { + '95.0': 522208.0, + '99.0': 4128736.0, + }, + }, + avg: { + value: 259173.0694980695, + }, + }, + { + key_as_string: '2020-07-05T08:40:00.000Z', + key: 1593938400000, + doc_count: 449, + pct: { + values: { + '95.0': 372728.0, + '99.0': 843768.0, + }, + }, + avg: { + value: 79648.20935412026, + }, + }, ], }, overall_avg_duration: { - value: 32861.15660262639, + value: 73065.05176360115, }, }, } as unknown) as ESResponse; diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.test.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.test.ts index d0d0875be388f..bda3bfcdf769c 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.test.ts @@ -48,21 +48,33 @@ describe('getTpmBuckets', () => { key_as_string: '', key: 0, doc_count: 0, + count: { + value: 0, + }, }, { key_as_string: '', key: 1, doc_count: 200, + count: { + value: 200, + }, }, { key_as_string: '', key: 2, doc_count: 300, + count: { + value: 300, + }, }, { key_as_string: '', key: 3, doc_count: 400, + count: { + value: 400, + }, }, ], }, @@ -76,21 +88,33 @@ describe('getTpmBuckets', () => { key_as_string: '', key: 0, doc_count: 0, + count: { + value: 0, + }, }, { key_as_string: '', key: 1, doc_count: 100, + count: { + value: 100, + }, }, { key_as_string: '', key: 2, doc_count: 100, + count: { + value: 100, + }, }, { key_as_string: '', key: 3, doc_count: 300, + count: { + value: 300, + }, }, ], }, diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.ts index f68c069253b99..7afb7427c210f 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.ts @@ -60,7 +60,7 @@ export function getTpmBuckets({ const dataPoints = timeseries.buckets.map((bucket) => { // calculate request/minute. Avoid up-scaling numbers if bucketSize is below 60s (1 minute). // Eg. 1 request during a 10 second window should be displayed as "1 rpm" instead of "6 rpm". - const tmpValue = bucket.doc_count * (60 / Math.max(60, bucketSize)); + const tmpValue = bucket.count.value * (60 / Math.max(60, bucketSize)); return { x: bucket.key, y: tmpValue, @@ -72,7 +72,7 @@ export function getTpmBuckets({ resultKey === '' ? NOT_AVAILABLE_LABEL : (resultKey as string); const docCountTotal = timeseries.buckets - .map((bucket) => bucket.doc_count) + .map((bucket) => bucket.count.value) .reduce((a, b) => a + b, 0); // calculate request/minute diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/index.ts b/x-pack/plugins/apm/server/lib/transactions/charts/index.ts index e862982145f77..43abf0b1a1d33 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/index.ts @@ -28,6 +28,7 @@ export async function getTransactionCharts(options: { transactionType: string | undefined; transactionName: string | undefined; setup: Setup & SetupTimeRange & SetupUIFilters; + searchAggregatedTransactions: boolean; logger: Logger; uiFilters: UIFilters; }) { diff --git a/x-pack/plugins/apm/server/lib/transactions/queries.test.ts b/x-pack/plugins/apm/server/lib/transactions/queries.test.ts index 8c8dbe1a3460a..1c1d30c2d4d6d 100644 --- a/x-pack/plugins/apm/server/lib/transactions/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/queries.test.ts @@ -54,6 +54,7 @@ describe('transaction queries', () => { transactionName: undefined, transactionType: undefined, setup, + searchAggregatedTransactions: false, logger: loggerMock.create(), uiFilters: {}, }) @@ -68,6 +69,7 @@ describe('transaction queries', () => { transactionName: 'bar', transactionType: undefined, setup, + searchAggregatedTransactions: false, logger: loggerMock.create(), uiFilters: {}, }) @@ -82,6 +84,7 @@ describe('transaction queries', () => { transactionName: 'bar', transactionType: 'baz', setup, + searchAggregatedTransactions: false, logger: loggerMock.create(), uiFilters: {}, }) diff --git a/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts b/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts index 7d3af4caa2ca3..e72cc7e2483ad 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts @@ -13,11 +13,17 @@ import { rangeFilter } from '../../../common/utils/range_filter'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values'; import { ESFilter } from '../../../typings/elasticsearch'; +import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; -export async function getEnvironments( - setup: Setup & SetupTimeRange, - serviceName?: string -) { +export async function getEnvironments({ + setup, + serviceName, + searchAggregatedTransactions, +}: { + setup: Setup & SetupTimeRange; + serviceName?: string; + searchAggregatedTransactions: boolean; +}) { const { start, end, apmEventClient } = setup; const filter: ESFilter[] = [{ range: rangeFilter(start, end) }]; @@ -31,7 +37,9 @@ export async function getEnvironments( const params = { apm: { events: [ - ProcessorEvent.transaction, + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), ProcessorEvent.metric, ProcessorEvent.error, ], diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts index 4cbb9efe012e6..22fa20e255f6e 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts @@ -29,7 +29,10 @@ describe('local ui filter queries', () => { getLocalUIFilters({ setup, localFilterNames: ['transactionResult', 'host'], - projection: getServicesProjection({ setup }), + projection: getServicesProjection({ + setup, + searchAggregatedTransactions: false, + }), uiFilters: { transactionResult: ['2xx'], }, diff --git a/x-pack/plugins/apm/server/lib/ui_filters/queries.test.ts b/x-pack/plugins/apm/server/lib/ui_filters/queries.test.ts index 24e1c1a7f654c..18bdfe6e351d7 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/queries.test.ts @@ -18,13 +18,24 @@ describe('ui filter queries', () => { }); it('fetches environments', async () => { - mock = await inspectSearchParams((setup) => getEnvironments(setup, 'foo')); + mock = await inspectSearchParams((setup) => + getEnvironments({ + setup, + serviceName: 'foo', + searchAggregatedTransactions: false, + }) + ); expect(mock.params).toMatchSnapshot(); }); it('fetches environments without a service name', async () => { - mock = await inspectSearchParams((setup) => getEnvironments(setup)); + mock = await inspectSearchParams((setup) => + getEnvironments({ + setup, + searchAggregatedTransactions: false, + }) + ); expect(mock.params).toMatchSnapshot(); }); diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index f25e37927f094..b417f8689b229 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -26,11 +26,7 @@ import { MlPluginSetup } from '../../ml/server'; import { ObservabilityPluginSetup } from '../../observability/server'; import { SecurityPluginSetup } from '../../security/server'; import { TaskManagerSetupContract } from '../../task_manager/server'; -import { - APM_FEATURE, - APM_SERVICE_MAPS_FEATURE_NAME, - APM_SERVICE_MAPS_LICENSE_TYPE, -} from './feature'; +import { APM_FEATURE, registerFeaturesUsage } from './feature'; import { registerApmAlerts } from './lib/alerts/register_apm_alerts'; import { createApmTelemetry } from './lib/apm_telemetry'; import { getInternalSavedObjectsClient } from './lib/helpers/get_internal_saved_objects_client'; @@ -128,10 +124,8 @@ export class APMPlugin implements Plugin { }); plugins.features.registerKibanaFeature(APM_FEATURE); - plugins.licensing.featureUsage.register( - APM_SERVICE_MAPS_FEATURE_NAME, - APM_SERVICE_MAPS_LICENSE_TYPE - ); + + registerFeaturesUsage({ licensingPlugin: plugins.licensing }); createApmApi().init(core, { config$: mergedConfig$, diff --git a/x-pack/plugins/apm/server/projections/rum_overview.ts b/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts similarity index 86% rename from x-pack/plugins/apm/server/projections/rum_overview.ts rename to x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts index ac5a7df235267..27cd9b53f8349 100644 --- a/x-pack/plugins/apm/server/projections/rum_overview.ts +++ b/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts @@ -15,8 +15,9 @@ import { } from '../../common/elasticsearch_fieldnames'; import { rangeFilter } from '../../common/utils/range_filter'; import { ProcessorEvent } from '../../common/processor_event'; +import { TRANSACTION_PAGE_LOAD } from '../../common/transaction_types'; -export function getRumOverviewProjection({ +export function getRumPageLoadTransactionsProjection({ setup, }: { setup: Setup & SetupTimeRange & SetupUIFilters; @@ -26,9 +27,10 @@ export function getRumOverviewProjection({ const bool = { filter: [ { range: rangeFilter(start, end) }, - { term: { [TRANSACTION_TYPE]: 'page-load' } }, + { term: { [TRANSACTION_TYPE]: TRANSACTION_PAGE_LOAD } }, { // Adding this filter to cater for some inconsistent rum data + // not available on aggregated transactions exists: { field: 'transaction.marks.navigationTiming.fetchStart', }, diff --git a/x-pack/plugins/apm/server/projections/services.ts b/x-pack/plugins/apm/server/projections/services.ts index 18fa79f31d6f3..ba61f72519a23 100644 --- a/x-pack/plugins/apm/server/projections/services.ts +++ b/x-pack/plugins/apm/server/projections/services.ts @@ -12,20 +12,25 @@ import { import { SERVICE_NAME } from '../../common/elasticsearch_fieldnames'; import { rangeFilter } from '../../common/utils/range_filter'; import { ProcessorEvent } from '../../common/processor_event'; +import { getProcessorEventForAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; export function getServicesProjection({ setup, + searchAggregatedTransactions, }: { setup: Setup & SetupTimeRange & SetupUIFilters; + searchAggregatedTransactions: boolean; }) { const { start, end, uiFiltersES } = setup; return { apm: { events: [ - ProcessorEvent.transaction, - ProcessorEvent.metric, - ProcessorEvent.error, + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ProcessorEvent.metric as const, + ProcessorEvent.error as const, ], }, body: { diff --git a/x-pack/plugins/apm/server/projections/transaction_groups.ts b/x-pack/plugins/apm/server/projections/transaction_groups.ts index 8aa085cccf82a..0cc3a7a35d214 100644 --- a/x-pack/plugins/apm/server/projections/transaction_groups.ts +++ b/x-pack/plugins/apm/server/projections/transaction_groups.ts @@ -12,6 +12,7 @@ import { import { TRANSACTION_NAME, PARENT_ID, + TRANSACTION_ROOT, } from '../../common/elasticsearch_fieldnames'; import { Options } from '../../server/lib/transaction_groups/fetcher'; import { getTransactionsProjection } from './transactions'; @@ -29,18 +30,27 @@ export function getTransactionGroupsProjection({ ...(omit(options, 'type') as Omit), }); - const bool = - options.type === 'top_traces' - ? { - must_not: [{ exists: { field: PARENT_ID } }], - } - : {}; + if (options.type === 'top_traces') { + if (options.searchAggregatedTransactions) { + transactionsProjection.body.query.bool.filter.push({ + term: { + [TRANSACTION_ROOT]: true, + }, + }); + } else { + // @ts-expect-error: Property 'must_not' does not exist on type '{ filter: ESFilter[]; }'. + transactionsProjection.body.query.bool.must_not = [ + { + exists: { + field: PARENT_ID, + }, + }, + ]; + } + } return mergeProjection(transactionsProjection, { body: { - query: { - bool, - }, aggs: { transactions: { terms: { diff --git a/x-pack/plugins/apm/server/projections/transactions.ts b/x-pack/plugins/apm/server/projections/transactions.ts index f428a76a8b0cb..8e9bb3bf321f6 100644 --- a/x-pack/plugins/apm/server/projections/transactions.ts +++ b/x-pack/plugins/apm/server/projections/transactions.ts @@ -15,18 +15,23 @@ import { TRANSACTION_NAME, } from '../../common/elasticsearch_fieldnames'; import { rangeFilter } from '../../common/utils/range_filter'; -import { ProcessorEvent } from '../../common/processor_event'; +import { + getProcessorEventForAggregatedTransactions, + getDocumentTypeFilterForAggregatedTransactions, +} from '../lib/helpers/aggregated_transactions'; export function getTransactionsProjection({ setup, serviceName, transactionName, transactionType, + searchAggregatedTransactions, }: { setup: Setup & SetupTimeRange & SetupUIFilters; serviceName?: string; transactionName?: string; transactionType?: string; + searchAggregatedTransactions: boolean; }) { const { start, end, uiFiltersES } = setup; @@ -47,12 +52,19 @@ export function getTransactionsProjection({ ...transactionTypeFilter, ...serviceNameFilter, ...uiFiltersES, + ...getDocumentTypeFilterForAggregatedTransactions( + searchAggregatedTransactions + ), ], }; return { apm: { - events: [ProcessorEvent.transaction as const], + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], }, body: { query: { diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index a15b37945ce01..1230e8aa05c9f 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -48,6 +48,7 @@ import { transactionGroupsRoute, transactionGroupsAvgDurationByCountry, transactionGroupsAvgDurationByBrowser, + transactionSampleForGroupRoute, transactionGroupsErrorRateRoute, } from './transaction_groups'; import { @@ -140,6 +141,7 @@ const createApmApi = () => { .add(transactionGroupsRoute) .add(transactionGroupsAvgDurationByBrowser) .add(transactionGroupsAvgDurationByCountry) + .add(transactionSampleForGroupRoute) .add(transactionGroupsErrorRateRoute) // UI filters diff --git a/x-pack/plugins/apm/server/routes/observability_overview.ts b/x-pack/plugins/apm/server/routes/observability_overview.ts index d5bb3b49c2f4c..498e8b4792de1 100644 --- a/x-pack/plugins/apm/server/routes/observability_overview.ts +++ b/x-pack/plugins/apm/server/routes/observability_overview.ts @@ -10,6 +10,7 @@ import { getTransactionCoordinates } from '../lib/observability_overview/get_tra import { hasData } from '../lib/observability_overview/has_data'; import { createRoute } from './create_route'; import { rangeRt } from './default_api_types'; +import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; export const observabilityOverviewHasDataRoute = createRoute(() => ({ path: '/api/apm/observability_overview/has_data', @@ -27,11 +28,20 @@ export const observabilityOverviewRoute = createRoute(() => ({ handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { bucketSize } = context.params.query; - const serviceCountPromise = getServiceCount({ setup }); + const searchAggregatedTransactions = await getSearchAggregatedTransactions( + setup + ); + + const serviceCountPromise = getServiceCount({ + setup, + searchAggregatedTransactions, + }); const transactionCoordinatesPromise = getTransactionCoordinates({ setup, bucketSize, + searchAggregatedTransactions, }); + const [serviceCount, transactionCoordinates] = await Promise.all([ serviceCountPromise, transactionCoordinatesPromise, diff --git a/x-pack/plugins/apm/server/routes/rum_client.ts b/x-pack/plugins/apm/server/routes/rum_client.ts index d4adca6a6658f..179279b6f2d8a 100644 --- a/x-pack/plugins/apm/server/routes/rum_client.ts +++ b/x-pack/plugins/apm/server/routes/rum_client.ts @@ -67,12 +67,12 @@ export const rumPageLoadDistBreakdownRoute = createRoute(() => ({ query: { minPercentile, maxPercentile, breakdown }, } = context.params; - return getPageLoadDistBreakdown( + return getPageLoadDistBreakdown({ setup, - Number(minPercentile), - Number(maxPercentile), - breakdown - ); + minDuration: Number(minPercentile), + maxDuration: Number(maxPercentile), + breakdown, + }); }, })); diff --git a/x-pack/plugins/apm/server/routes/service_map.ts b/x-pack/plugins/apm/server/routes/service_map.ts index 8533d54ed6277..1996d4d4a262d 100644 --- a/x-pack/plugins/apm/server/routes/service_map.ts +++ b/x-pack/plugins/apm/server/routes/service_map.ts @@ -15,7 +15,8 @@ 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 { rangeRt, uiFiltersRt } from './default_api_types'; -import { APM_SERVICE_MAPS_FEATURE_NAME } from '../feature'; +import { notifyFeatureUsage } from '../feature'; +import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; import { getParsedUiFilters } from '../lib/helpers/convert_ui_filters/get_parsed_ui_filters'; export const serviceMapRoute = createRoute(() => ({ @@ -36,14 +37,28 @@ export const serviceMapRoute = createRoute(() => ({ if (!isActivePlatinumLicense(context.licensing.license)) { throw Boom.forbidden(invalidLicenseMessage); } - context.licensing.featureUsage.notifyUsage(APM_SERVICE_MAPS_FEATURE_NAME); + + notifyFeatureUsage({ + licensingPlugin: context.licensing, + featureName: 'serviceMaps', + }); const logger = context.logger; const setup = await setupRequest(context, request); const { query: { serviceName, environment }, } = context.params; - return getServiceMap({ setup, serviceName, environment, logger }); + + const searchAggregatedTransactions = await getSearchAggregatedTransactions( + setup + ); + return getServiceMap({ + setup, + serviceName, + environment, + searchAggregatedTransactions, + logger, + }); }, })); @@ -70,11 +85,15 @@ export const serviceMapServiceNodeRoute = createRoute(() => ({ path: { serviceName }, } = context.params; + const searchAggregatedTransactions = await getSearchAggregatedTransactions( + setup + ); const uiFilters = getParsedUiFilters({ uiFilters: uiFiltersJson, logger }); return getServiceMapServiceNodeInfo({ setup, serviceName, + searchAggregatedTransactions, uiFilters, }); }, diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index cc7f25867df2c..4bb10f31ba6a1 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -16,6 +16,7 @@ import { createRoute } from './create_route'; import { uiFiltersRt, rangeRt } from './default_api_types'; import { getServiceAnnotations } from '../lib/services/annotations'; import { dateAsStringRt } from '../../common/runtime_types/date_as_string_rt'; +import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; import { getParsedUiFilters } from '../lib/helpers/convert_ui_filters/get_parsed_ui_filters'; export const servicesRoute = createRoute(() => ({ @@ -31,8 +32,13 @@ export const servicesRoute = createRoute(() => ({ const setup = await setupRequest(context, request); + const searchAggregatedTransactions = await getSearchAggregatedTransactions( + setup + ); + const services = await getServices({ setup, + searchAggregatedTransactions, mlAnomaliesEnvironment: environment, }); @@ -51,7 +57,15 @@ export const serviceAgentNameRoute = createRoute(() => ({ handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { serviceName } = context.params.path; - return getServiceAgentName(serviceName, setup); + const searchAggregatedTransactions = await getSearchAggregatedTransactions( + setup + ); + + return getServiceAgentName({ + serviceName, + setup, + searchAggregatedTransactions, + }); }, })); @@ -66,7 +80,13 @@ export const serviceTransactionTypesRoute = createRoute(() => ({ handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { serviceName } = context.params.path; - return getServiceTransactionTypes(serviceName, setup); + return getServiceTransactionTypes({ + serviceName, + setup, + searchAggregatedTransactions: await getSearchAggregatedTransactions( + setup + ), + }); }, })); @@ -104,13 +124,20 @@ export const serviceAnnotationsRoute = createRoute(() => ({ const { serviceName } = context.params.path; const { environment } = context.params.query; - const annotationsClient = await context.plugins.observability?.getScopedAnnotationsClient( - context, - request - ); + const [ + annotationsClient, + searchAggregatedTransactions, + ] = await Promise.all([ + context.plugins.observability?.getScopedAnnotationsClient( + context, + request + ), + getSearchAggregatedTransactions(setup), + ]); return getServiceAnnotations({ setup, + searchAggregatedTransactions, serviceName, environment, annotationsClient, 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 0350ebfb9196c..beab6b6c850e3 100644 --- a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts +++ b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts @@ -22,6 +22,7 @@ import { agentConfigurationIntakeRt, } from '../../../common/agent_configuration/runtime_types/agent_configuration_intake_rt'; import { jsonRt } from '../../../common/runtime_types/json_rt'; +import { getSearchAggregatedTransactions } from '../../lib/helpers/aggregated_transactions'; // get list of configurations export const agentConfigurationRoute = createRoute(() => ({ @@ -199,8 +200,12 @@ export const listAgentConfigurationServicesRoute = createRoute(() => ({ path: '/api/apm/settings/agent-configuration/services', handler: async ({ context, request }) => { const setup = await setupRequest(context, request); + const searchAggregatedTransactions = await getSearchAggregatedTransactions( + setup + ); return await getServiceNames({ setup, + searchAggregatedTransactions, }); }, })); @@ -214,7 +219,15 @@ export const listAgentConfigurationEnvironmentsRoute = createRoute(() => ({ handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { serviceName } = context.params.query; - return await getEnvironments({ serviceName, setup }); + const searchAggregatedTransactions = await getSearchAggregatedTransactions( + setup + ); + + return await getEnvironments({ + serviceName, + setup, + searchAggregatedTransactions, + }); }, })); 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 290e81bd29973..f0a22356d074b 100644 --- a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts +++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts @@ -14,6 +14,8 @@ import { createAnomalyDetectionJobs } from '../../lib/anomaly_detection/create_a import { setupRequest } from '../../lib/helpers/setup_request'; import { getAllEnvironments } from '../../lib/environments/get_all_environments'; import { hasLegacyJobs } from '../../lib/anomaly_detection/has_legacy_jobs'; +import { getSearchAggregatedTransactions } from '../../lib/helpers/aggregated_transactions'; +import { notifyFeatureUsage } from '../../feature'; // get ML anomaly detection jobs for each environment export const anomalyDetectionJobsRoute = createRoute(() => ({ @@ -61,6 +63,10 @@ export const createAnomalyDetectionJobsRoute = createRoute(() => ({ } await createAnomalyDetectionJobs(setup, environments, context.logger); + notifyFeatureUsage({ + licensingPlugin: context.licensing, + featureName: 'ml', + }); }, })); @@ -70,6 +76,15 @@ export const anomalyDetectionEnvironmentsRoute = createRoute(() => ({ path: '/api/apm/settings/anomaly-detection/environments', handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - return await getAllEnvironments({ setup, includeMissing: true }); + + const searchAggregatedTransactions = await getSearchAggregatedTransactions( + setup + ); + + return await getAllEnvironments({ + setup, + searchAggregatedTransactions, + includeMissing: true, + }); }, })); 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 83c23a75e999d..7882383d78ab0 100644 --- a/x-pack/plugins/apm/server/routes/settings/custom_link.ts +++ b/x-pack/plugins/apm/server/routes/settings/custom_link.ts @@ -3,9 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +import Boom from 'boom'; import * as t from 'io-ts'; import { pick } from 'lodash'; +import { INVALID_LICENSE } from '../../../common/custom_link'; +import { ILicense } from '../../../../licensing/common/types'; import { FILTER_OPTIONS } from '../../../common/custom_link/custom_link_filter_options'; +import { notifyFeatureUsage } from '../../feature'; import { setupRequest } from '../../lib/helpers/setup_request'; import { createOrUpdateCustomLink } from '../../lib/settings/custom_link/create_or_update_custom_link'; import { @@ -17,6 +22,10 @@ import { getTransaction } from '../../lib/settings/custom_link/get_transaction'; import { listCustomLinks } from '../../lib/settings/custom_link/list_custom_links'; import { createRoute } from '../create_route'; +function isActiveGoldLicense(license: ILicense) { + return license.isActive && license.hasAtLeast('gold'); +} + export const customLinkTransactionRoute = createRoute(() => ({ path: '/api/apm/settings/custom_links/transaction', params: { @@ -37,6 +46,9 @@ export const listCustomLinksRoute = createRoute(() => ({ query: filterOptionsRt, }, handler: async ({ context, request }) => { + if (!isActiveGoldLicense(context.licensing.license)) { + throw Boom.forbidden(INVALID_LICENSE); + } const setup = await setupRequest(context, request); const { query } = context.params; // picks only the items listed in FILTER_OPTIONS @@ -55,9 +67,17 @@ export const createCustomLinkRoute = createRoute(() => ({ tags: ['access:apm', 'access:apm_write'], }, handler: async ({ context, request }) => { + 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 }); + + notifyFeatureUsage({ + licensingPlugin: context.licensing, + featureName: 'customLinks', + }); return res; }, })); @@ -75,6 +95,9 @@ export const updateCustomLinkRoute = createRoute(() => ({ tags: ['access:apm', 'access:apm_write'], }, handler: async ({ context, request }) => { + 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; @@ -99,6 +122,9 @@ export const deleteCustomLinkRoute = createRoute(() => ({ tags: ['access:apm', 'access:apm_write'], }, handler: async ({ context, request }) => { + if (!isActiveGoldLicense(context.licensing.license)) { + throw Boom.forbidden(INVALID_LICENSE); + } const setup = await setupRequest(context, request); const { id } = context.params.path; const res = await deleteCustomLink({ diff --git a/x-pack/plugins/apm/server/routes/traces.ts b/x-pack/plugins/apm/server/routes/traces.ts index 354dafe536c43..45b334a7f06d2 100644 --- a/x-pack/plugins/apm/server/routes/traces.ts +++ b/x-pack/plugins/apm/server/routes/traces.ts @@ -10,6 +10,7 @@ import { getTrace } from '../lib/traces/get_trace'; import { getTransactionGroupList } from '../lib/transaction_groups'; import { createRoute } from './create_route'; import { rangeRt, uiFiltersRt } from './default_api_types'; +import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; export const tracesRoute = createRoute(() => ({ path: '/api/apm/traces', @@ -18,7 +19,13 @@ export const tracesRoute = createRoute(() => ({ }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - return getTransactionGroupList({ type: 'top_traces' }, setup); + const searchAggregatedTransactions = await getSearchAggregatedTransactions( + setup + ); + return getTransactionGroupList( + { type: 'top_traces', searchAggregatedTransactions }, + setup + ); }, })); diff --git a/x-pack/plugins/apm/server/routes/transaction_groups.ts b/x-pack/plugins/apm/server/routes/transaction_groups.ts index c667ce4f07e93..3c512c1fe5278 100644 --- a/x-pack/plugins/apm/server/routes/transaction_groups.ts +++ b/x-pack/plugins/apm/server/routes/transaction_groups.ts @@ -14,6 +14,8 @@ import { createRoute } from './create_route'; import { uiFiltersRt, rangeRt } from './default_api_types'; import { getTransactionAvgDurationByBrowser } from '../lib/transactions/avg_duration_by_browser'; import { getTransactionAvgDurationByCountry } from '../lib/transactions/avg_duration_by_country'; +import { getTransactionSampleForGroup } from '../lib/transaction_groups/get_transaction_sample_for_group'; +import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; import { getErrorRate } from '../lib/transaction_groups/get_error_rate'; import { getParsedUiFilters } from '../lib/helpers/convert_ui_filters/get_parsed_ui_filters'; @@ -36,11 +38,16 @@ export const transactionGroupsRoute = createRoute(() => ({ const { serviceName } = context.params.path; const { transactionType } = context.params.query; + const searchAggregatedTransactions = await getSearchAggregatedTransactions( + setup + ); + return getTransactionGroupList( { type: 'top_transactions', serviceName, transactionType, + searchAggregatedTransactions, }, setup ); @@ -74,11 +81,16 @@ export const transactionGroupsChartsRoute = createRoute(() => ({ const uiFilters = getParsedUiFilters({ uiFilters: uiFiltersJson, logger }); + const searchAggregatedTransactions = await getSearchAggregatedTransactions( + setup + ); + return getTransactionCharts({ serviceName, transactionType, transactionName, setup, + searchAggregatedTransactions, logger, uiFilters, }); @@ -175,9 +187,14 @@ export const transactionGroupsAvgDurationByBrowser = createRoute(() => ({ const { serviceName } = context.params.path; const { transactionName } = context.params.query; + const searchAggregatedTransactions = await getSearchAggregatedTransactions( + setup + ); + return getTransactionAvgDurationByBrowser({ serviceName, setup, + searchAggregatedTransactions, transactionName, }); }, @@ -200,14 +217,43 @@ export const transactionGroupsAvgDurationByCountry = createRoute(() => ({ const { serviceName } = context.params.path; const { transactionName } = context.params.query; + const searchAggregatedTransactions = await getSearchAggregatedTransactions( + setup + ); + return getTransactionAvgDurationByCountry({ serviceName, transactionName, setup, + searchAggregatedTransactions, }); }, })); +export const transactionSampleForGroupRoute = createRoute(() => ({ + path: `/api/apm/transaction_sample`, + params: { + query: t.intersection([ + uiFiltersRt, + rangeRt, + t.type({ serviceName: t.string, transactionName: t.string }), + ]), + }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + + const { transactionName, serviceName } = context.params.query; + + return { + transaction: await getTransactionSampleForGroup({ + setup, + serviceName, + transactionName, + }), + }; + }, +})); + export const transactionGroupsErrorRateRoute = createRoute(() => ({ path: '/api/apm/services/{serviceName}/transaction_groups/error_rate', params: { diff --git a/x-pack/plugins/apm/server/routes/ui_filters.ts b/x-pack/plugins/apm/server/routes/ui_filters.ts index 864f5033c9d62..8bdd83a8ddda6 100644 --- a/x-pack/plugins/apm/server/routes/ui_filters.ts +++ b/x-pack/plugins/apm/server/routes/ui_filters.ts @@ -29,7 +29,9 @@ import { createRoute } from './create_route'; import { uiFiltersRt, rangeRt } from './default_api_types'; import { jsonRt } from '../../common/runtime_types/json_rt'; import { getServiceNodesProjection } from '../projections/service_nodes'; -import { getRumOverviewProjection } from '../projections/rum_overview'; +import { getRumPageLoadTransactionsProjection } from '../projections/rum_page_load_transactions'; +import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; +import { APMRequestHandlerContext } from './typings'; export const uiFiltersEnvironmentsRoute = createRoute(() => ({ path: '/api/apm/ui_filters/environments', @@ -44,7 +46,15 @@ export const uiFiltersEnvironmentsRoute = createRoute(() => ({ handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { serviceName } = context.params.query; - return getEnvironments(setup, serviceName); + const searchAggregatedTransactions = await getSearchAggregatedTransactions( + setup + ); + + return getEnvironments({ + setup, + serviceName, + searchAggregatedTransactions, + }); }, })); @@ -93,8 +103,9 @@ function createLocalFiltersRoute< const { uiFilters, filterNames } = query; const parsedUiFilters = JSON.parse(uiFilters); - const projection = getProjection({ + const projection = await getProjection({ query, + context, setup: { ...setup, uiFiltersES: getUiFiltersES(omit(parsedUiFilters, filterNames)), @@ -113,14 +124,25 @@ function createLocalFiltersRoute< export const servicesLocalFiltersRoute = createLocalFiltersRoute({ path: `/api/apm/ui_filters/local_filters/services`, - getProjection: ({ setup }) => getServicesProjection({ setup }), + getProjection: async ({ context, setup }) => { + const searchAggregatedTransactions = await getSearchAggregatedTransactions( + setup + ); + + return getServicesProjection({ setup, searchAggregatedTransactions }); + }, queryRt: t.type({}), }); export const transactionGroupsLocalFiltersRoute = createLocalFiltersRoute({ path: '/api/apm/ui_filters/local_filters/transactionGroups', - getProjection: ({ setup, query }) => { + getProjection: async ({ context, setup, query }) => { const { transactionType, serviceName, transactionName } = query; + + const searchAggregatedTransactions = await getSearchAggregatedTransactions( + setup + ); + return getTransactionGroupsProjection({ setup, options: { @@ -128,6 +150,7 @@ export const transactionGroupsLocalFiltersRoute = createLocalFiltersRoute({ transactionType, serviceName, transactionName, + searchAggregatedTransactions, }, }); }, @@ -144,10 +167,14 @@ export const transactionGroupsLocalFiltersRoute = createLocalFiltersRoute({ export const tracesLocalFiltersRoute = createLocalFiltersRoute({ path: '/api/apm/ui_filters/local_filters/traces', - getProjection: ({ setup }) => { + getProjection: async ({ setup, context }) => { + const searchAggregatedTransactions = await getSearchAggregatedTransactions( + setup + ); + return getTransactionGroupsProjection({ setup, - options: { type: 'top_traces' }, + options: { type: 'top_traces', searchAggregatedTransactions }, }); }, queryRt: t.type({}), @@ -155,13 +182,19 @@ export const tracesLocalFiltersRoute = createLocalFiltersRoute({ export const transactionsLocalFiltersRoute = createLocalFiltersRoute({ path: '/api/apm/ui_filters/local_filters/transactions', - getProjection: ({ setup, query }) => { + getProjection: async ({ context, setup, query }) => { const { transactionType, serviceName, transactionName } = query; + + const searchAggregatedTransactions = await getSearchAggregatedTransactions( + setup + ); + return getTransactionsProjection({ setup, transactionType, serviceName, transactionName, + searchAggregatedTransactions, }); }, queryRt: t.type({ @@ -221,8 +254,8 @@ export const serviceNodesLocalFiltersRoute = createLocalFiltersRoute({ export const rumOverviewLocalFiltersRoute = createLocalFiltersRoute({ path: '/api/apm/ui_filters/local_filters/rumOverview', - getProjection: ({ setup }) => { - return getRumOverviewProjection({ + getProjection: async ({ setup }) => { + return getRumPageLoadTransactionsProjection({ setup, }); }, @@ -237,7 +270,9 @@ type GetProjection< > = ({ query, setup, + context, }: { query: t.TypeOf; setup: Setup & SetupUIFilters & SetupTimeRange; -}) => TProjection; + context: APMRequestHandlerContext; +}) => Promise | TProjection; diff --git a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts index 44211601204cd..93f8b115256b4 100644 --- a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts +++ b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts @@ -176,7 +176,7 @@ export interface AggregationInputMap { [key: string]: AggregationOptionsMap; } -type BucketSubAggregationResponse< +type SubAggregationResponseOf< TAggregationInputMap extends AggregationInputMap | undefined, TDocument > = TAggregationInputMap extends AggregationInputMap @@ -192,10 +192,7 @@ interface AggregationResponsePart< { doc_count: number; key: string | number; - } & BucketSubAggregationResponse< - TAggregationOptionsMap['aggs'], - TDocument - > + } & SubAggregationResponseOf >; }; histogram: { @@ -203,23 +200,20 @@ interface AggregationResponsePart< { doc_count: number; key: number; - } & BucketSubAggregationResponse< - TAggregationOptionsMap['aggs'], - TDocument - > + } & SubAggregationResponseOf >; }; date_histogram: { buckets: Array< DateHistogramBucket & - BucketSubAggregationResponse + SubAggregationResponseOf >; }; avg: MetricsAggregationResponsePart; sum: MetricsAggregationResponsePart; max: MetricsAggregationResponsePart; min: MetricsAggregationResponsePart; - value_count: MetricsAggregationResponsePart; + value_count: { value: number }; cardinality: { value: number; }; @@ -261,7 +255,7 @@ interface AggregationResponsePart< }; filter: { doc_count: number; - } & AggregationResponseMap; + } & SubAggregationResponseOf; filters: TAggregationOptionsMap extends { filters: { filters: any[] } } ? Array< { doc_count: number } & AggregationResponseMap< @@ -278,13 +272,16 @@ interface AggregationResponsePart< buckets: { [key in keyof TAggregationOptionsMap['filters']['filters']]: { doc_count: number; - } & AggregationResponseMap; + } & SubAggregationResponseOf< + TAggregationOptionsMap['aggs'], + TDocument + >; }; } : never; sampler: { doc_count: number; - } & AggregationResponseMap; + } & SubAggregationResponseOf; derivative: | { value: number; @@ -303,10 +300,7 @@ interface AggregationResponsePart< { key: Record, string | number>; doc_count: number; - } & BucketSubAggregationResponse< - TAggregationOptionsMap['aggs'], - TDocument - > + } & SubAggregationResponseOf >; }; diversified_sampler: { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__stories__/__snapshots__/palette.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__stories__/__snapshots__/palette.stories.storyshot index 385b16d3d8e8e..c0d3256eb9eb7 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__stories__/__snapshots__/palette.stories.storyshot +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__stories__/__snapshots__/palette.stories.storyshot @@ -48,12 +48,10 @@ exports[`Storyshots arguments/Palette default 1`] = `