diff --git a/.eslintrc.js b/.eslintrc.js index af05af0f6e402..e66331594b4ae 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -491,6 +491,7 @@ module.exports = { 'x-pack/dev-tools/mocha/setup_mocha.js', 'x-pack/scripts/*.js', ], + excludedFiles: ['**/integration_tests/**/*'], rules: { 'import/no-commonjs': 'off', 'prefer-object-spread/prefer-object-spread': 'off', diff --git a/docs/management/alerting/alert-details.asciidoc b/docs/management/alerting/alert-details.asciidoc new file mode 100644 index 0000000000000..b108f79fe5bad --- /dev/null +++ b/docs/management/alerting/alert-details.asciidoc @@ -0,0 +1,34 @@ +[role="xpack"] +[[alert-details]] +=== Alert details + +beta[] + +The *Alert details* page tells you about the state of the alert and provides granular control over the actions it is taking. + +[role="screenshot"] +image::images/alerts-details-instances-active.png[Alert details page with three alert instances] + +In this example, alerts detect when a site serves more than a threshold number of bytes in a 24 hour period. Three sites are above the threshold. These are called alert instances - occurrences of the condition being detected - and the instance name, status, time of detection, and duration of the condition are shown in this view. + +Upon detection, each instance can trigger one or more actions. If the condition persists, the same actions will trigger either on the next scheduled alert check, or (if defined) after the re-notify period on the alert has passed. To prevent re-notification, you can suppress future actions by clicking on the eye icon to mute an individual alert instance. Muting means that the alert checks continue to run on a schedule, but that instance will not trigger any action. + +[role="screenshot"] +image::images/alerts-details-instance-muting.png[Muting an alert instance] + +Alert instances will come and go from the list depending on whether they meet the alert conditions or not - unless they are muted. If a muted instance no longer meets the alert conditions, it will appear as inactive in the list. This prevents an instance from triggering actions if it reappears in the future. + +[role="screenshot"] +image::images/alerts-details-instances-inactive.png[Alert details page with three inactive alert instances] + +If you want to suppress actions on all current and future instances, you can mute the entire alert. Alert checks continue to run and the instance list will update as instances activate or deactivate, but no actions will be triggered. + +[role="screenshot"] +image::images/alerts-details-muting.png[Use the mute toggle to suppress all action on current and future instances] + +You can also disable an alert altogether. When disabled, the alert stops running checks altogether and will clear any instances it is tracking. You may want to disable alerts that are not currently needed to reduce the load on {kib} and {es}. + +[role="screenshot"] +image::images/alerts-details-disabling.png[Use the disable toggle to turn off alert checks and clear instances tracked] + +* For further information on alerting concepts and examples, see <>. diff --git a/docs/management/alerting/alert-management.asciidoc b/docs/management/alerting/alert-management.asciidoc new file mode 100644 index 0000000000000..caf260937b7be --- /dev/null +++ b/docs/management/alerting/alert-management.asciidoc @@ -0,0 +1,59 @@ +[role="xpack"] +[[alert-management]] +=== Managing Alerts + +beta[] + +The *Alerts* tab provides a cross-app view of alerting. Different {kib} apps like <>, <>, <>, and <> can offer their own alerts, and the *Alerts* tab provides a central place to: + +* <> alerts +* <> including enabling/disabling, muting/unmuting, and deleting +* Drill-down to <> + +[role="screenshot"] +image:management/alerting/images/alerts-and-actions-ui.png[Example alert listing in the Alerts and Actions UI] + +For more information on alerting concepts and the types of alerts and actions available, see <>. + +[float] +==== Finding alerts + +The *Alerts* tab lists all alerts in the current space, including summary information about their execution frequency, tags, and type. + +The *search bar* can be used to quickly find alerts by name or tag. + +[role="screenshot"] +image::images/alerts-filter-by-search.png[Filtering the alerts list using the search bar] + +The *type* dropdown lets you filter to a subset of alert types. + +[role="screenshot"] +image::images/alerts-filter-by-type.png[Filtering the alerts list by types of alert] + +The *Action type* dropdown lets you filter by the type of action used in the alert. + +[role="screenshot"] +image::images/alerts-filter-by-action-type.png[Filtering the alert list by type of action] + +[float] +[[create-edit-alerts]] +==== Creating and editing alerts + +Many alerts must be created within the context of a {kib} app like <>, <>, or <>, but others are generic. Generic alert types can be created in the *Alerts* management UI by clicking the *Create* button. This will launch a flyout that guides you through selecting an alert type and configuring it's properties. Refer to <> for details on what types of alerts are available and how to configure them. + +After an alert is created, you can re-open the flyout and change an alerts properties by clicking the *Edit* button shown on each row of the alert listing. + + +[float] +[[controlling-alerts]] +==== Controlling alerts + +The alert listing allows you to quickly mute/unmute, disable/enable, and delete individual alerts by clicking the action button at the right of each row. + +[role="screenshot"] +image:management/alerting/images/individual-mute-disable.png[The actions button allows an individual alert to be muted, disabled, or deleted] + +These operations can also be performed in bulk by multi-selecting alerts and clicking the *Manage alerts* button: + +[role="screenshot"] +image:management/alerting/images/bulk-mute-disable.png[The Manage alerts button lets you mute/unmute, enable/disable, and delete in bulk] \ No newline at end of file diff --git a/docs/management/alerting/alerts-and-actions-intro.asciidoc b/docs/management/alerting/alerts-and-actions-intro.asciidoc new file mode 100644 index 0000000000000..4674c9ba902be --- /dev/null +++ b/docs/management/alerting/alerts-and-actions-intro.asciidoc @@ -0,0 +1,25 @@ +[role="xpack"] +[[managing-alerts-and-actions]] +== Alerts and Actions + +beta[] + +The *Alerts and Actions* UI lets you <> in a space, and provides tools to <> so that alerts can trigger actions like notification, indexing, and ticketing. + +To manage alerting and connectors, go to *Management > {kib} > Alerts and Actions*. + +[role="screenshot"] +image:management/alerting/images/alerts-and-actions-ui.png[Example alert listing in the Alerts and Actions UI] + +[NOTE] +============================================================================ +Similar to dashboards, alerts and connectors reside in a <>. +The *Alerts and Actions* UI only shows alerts and connectors for the current space. +============================================================================ + +[NOTE] +============================================================================ +{es} also offers alerting capabilities through Watcher, which +can be managed through the <>. See +<> for more information. +============================================================================ \ No newline at end of file diff --git a/docs/management/alerting/connector-management.asciidoc b/docs/management/alerting/connector-management.asciidoc new file mode 100644 index 0000000000000..1002a372f9460 --- /dev/null +++ b/docs/management/alerting/connector-management.asciidoc @@ -0,0 +1,47 @@ +[role="xpack"] +[[connector-management]] +=== Managing Connectors + +beta[] + +Alerts use *Connectors* to route actions to different destinations like log files, ticketing systems, and messaging tools. While each {kib} app can offer their own types of alerts, they typically share connectors. The *Connectors* tab offers a central place to view and manage all the connectors in the current space. + +For more information on connectors and the types of actions available see <>. + +[role="screenshot"] +image::images/connector-listing.png[Example connector listing in the Alerts and Actions UI] + + +[float] +==== Connector list + +The *Connectors* tab lists all connectors in the current space. The *search bar* can be used to find specific connectors by name and/or type. + +[role="screenshot"] +image::images/connector-filter-by-search.png[Filtering the connector list using the search bar] + + +The *type* dropdown also lets you filter to a subset of action types. + +[role="screenshot"] +image::images/connector-filter-by-type.png[Filtering the connector list by types of actions] + +The *Actions* column indicates the number of actions that reference the connector. This count helps you confirm a connector is unused before you delete it, and tells you how many actions will be affected when a connector is modified. + +[role="screenshot"] +image::images/connector-action-count.png[Filtering the connector list by types of actions] + +You can delete individual connectors using the trash icon on the right of each row. Connectors can also be deleted in bulk by multi-selecting them and clicking the *Delete* button to the left of the search box. + +[role="screenshot"] +image::images/connector-delete.png[Deleting connectors individually or in bulk] + +[NOTE] +============================================================================ +You can delete a connector even if there are still actions referencing it. +When this happens the action will fail to execute, and appear as errors in the {kib} logs. +============================================================================ + +==== Creating a new connector + +New connectors can be created by clicking the *Create connector* button, which will guide you to select the type of connector and configure it's properties. Refer to <> for the types of connectors available and how to configure them. Once you create a connector it will be made available to you anytime you set up an action in the current space. \ No newline at end of file diff --git a/docs/management/alerting/images/alerts-and-actions-ui.png b/docs/management/alerting/images/alerts-and-actions-ui.png new file mode 100644 index 0000000000000..acf3f3b1f0be9 Binary files /dev/null and b/docs/management/alerting/images/alerts-and-actions-ui.png differ diff --git a/docs/management/alerting/images/alerts-details-disabling.png b/docs/management/alerting/images/alerts-details-disabling.png new file mode 100644 index 0000000000000..7057afc8ad07d Binary files /dev/null and b/docs/management/alerting/images/alerts-details-disabling.png differ diff --git a/docs/management/alerting/images/alerts-details-instance-muting.png b/docs/management/alerting/images/alerts-details-instance-muting.png new file mode 100644 index 0000000000000..9d26fad419e4f Binary files /dev/null and b/docs/management/alerting/images/alerts-details-instance-muting.png differ diff --git a/docs/management/alerting/images/alerts-details-instances-active.png b/docs/management/alerting/images/alerts-details-instances-active.png new file mode 100644 index 0000000000000..d6895bd4952b8 Binary files /dev/null and b/docs/management/alerting/images/alerts-details-instances-active.png differ diff --git a/docs/management/alerting/images/alerts-details-instances-inactive.png b/docs/management/alerting/images/alerts-details-instances-inactive.png new file mode 100644 index 0000000000000..b049b4ba082f6 Binary files /dev/null and b/docs/management/alerting/images/alerts-details-instances-inactive.png differ diff --git a/docs/management/alerting/images/alerts-details-muting.png b/docs/management/alerting/images/alerts-details-muting.png new file mode 100644 index 0000000000000..9b47d82a74639 Binary files /dev/null and b/docs/management/alerting/images/alerts-details-muting.png differ diff --git a/docs/management/alerting/images/alerts-filter-by-action-type.png b/docs/management/alerting/images/alerts-filter-by-action-type.png new file mode 100644 index 0000000000000..94336a20e1d6c Binary files /dev/null and b/docs/management/alerting/images/alerts-filter-by-action-type.png differ diff --git a/docs/management/alerting/images/alerts-filter-by-search.png b/docs/management/alerting/images/alerts-filter-by-search.png new file mode 100644 index 0000000000000..df4b6d7d9d9aa Binary files /dev/null and b/docs/management/alerting/images/alerts-filter-by-search.png differ diff --git a/docs/management/alerting/images/alerts-filter-by-type.png b/docs/management/alerting/images/alerts-filter-by-type.png new file mode 100644 index 0000000000000..75ffb3ff69bab Binary files /dev/null and b/docs/management/alerting/images/alerts-filter-by-type.png differ diff --git a/docs/management/alerting/images/bulk-mute-disable.png b/docs/management/alerting/images/bulk-mute-disable.png new file mode 100644 index 0000000000000..04cdc3f03f34d Binary files /dev/null and b/docs/management/alerting/images/bulk-mute-disable.png differ diff --git a/docs/management/alerting/images/connector-action-count.png b/docs/management/alerting/images/connector-action-count.png new file mode 100644 index 0000000000000..b792ae2cc11c8 Binary files /dev/null and b/docs/management/alerting/images/connector-action-count.png differ diff --git a/docs/management/alerting/images/connector-delete.png b/docs/management/alerting/images/connector-delete.png new file mode 100644 index 0000000000000..ccb6bcea4bade Binary files /dev/null and b/docs/management/alerting/images/connector-delete.png differ diff --git a/docs/management/alerting/images/connector-filter-by-search.png b/docs/management/alerting/images/connector-filter-by-search.png new file mode 100644 index 0000000000000..1f052ea23a577 Binary files /dev/null and b/docs/management/alerting/images/connector-filter-by-search.png differ diff --git a/docs/management/alerting/images/connector-filter-by-type.png b/docs/management/alerting/images/connector-filter-by-type.png new file mode 100644 index 0000000000000..06c7d6f4dad47 Binary files /dev/null and b/docs/management/alerting/images/connector-filter-by-type.png differ diff --git a/docs/management/alerting/images/connector-listing.png b/docs/management/alerting/images/connector-listing.png new file mode 100644 index 0000000000000..43014ff88057f Binary files /dev/null and b/docs/management/alerting/images/connector-listing.png differ diff --git a/docs/management/alerting/images/individual-mute-disable.png b/docs/management/alerting/images/individual-mute-disable.png new file mode 100644 index 0000000000000..ca00240a4af61 Binary files /dev/null and b/docs/management/alerting/images/individual-mute-disable.png differ diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index e3b9e61667bdf..d7f1ec637d1df 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -27,7 +27,7 @@ If not set, {kib} will generate a random key on startup, but all alert and actio Although the key can be specified in clear text in `kibana.yml`, it's recommended to store this key securely in the <>. [float] -[[alert-settings]] +[[action-settings]] ==== Action settings `xpack.actions.whitelistedHosts`:: @@ -41,7 +41,7 @@ A list of action types that are enabled. It defaults to `[*]`, enabling all type Disabled action types will not appear as an option when creating new connectors, but existing connectors and actions of that type will remain in {kib} and will not function. [float] -[[action-settings]] +[[alert-settings]] ==== Alert settings You do not need to configure any additional settings to use alerting in {kib}. diff --git a/docs/user/alerting/action-types.asciidoc b/docs/user/alerting/action-types.asciidoc new file mode 100644 index 0000000000000..02c09736e1fa0 --- /dev/null +++ b/docs/user/alerting/action-types.asciidoc @@ -0,0 +1,182 @@ +[role="xpack"] +[[action-types]] +== Action and connector types + +{kib} provides the following types of actions: + +* <> +* <> +* <> +* <> +* <> +* <> + +This section describes how to configure connectors and actions for each type. + +[NOTE] +============================================== +Some action types are paid commercial features, while others are free. +For a comparison of the Elastic license levels, +see https://www.elastic.co/subscriptions[the subscription page]. +============================================== + +[float] +[[email-action-type]] +=== Email + +The email action type uses the SMTP protocol to send mail message, using an integration of https://nodemailer.com/[Nodemailer]. Email message text is sent as both plain text and html text. + +[float] +[[email-connector-configuration]] +==== Connector configuration + +Email connectors have the following configuration properties: + +Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. +Sender:: The from address for all emails sent with this connector, specified in `user@host-name` format. +Host:: Host name of the service provider. If you are using the <> setting, make sure this hostname is whitelisted. +Port:: The port to connect to on the service provider. +Secure:: If true the connection will use TLS when connecting to the service provider. See https://nodemailer.com/smtp/#tls-options[nodemailer TLS documentation] for more information. +Username:: username for 'login' type authentication. +Password:: password for 'login' type authentication. + +[float] +[[email-action-configuration]] +==== Action configuration + +Email actions have the following configuration properties: + +To, CC, BCC:: Each is a list of addresses. Addresses can be specified in `user@host-name` format, or in `name ` format. One of To, CC, or BCC must contain an entry. +Subject:: The subject line of the email. +Message:: The message text of the email. Markdown format is supported. + +[float] +[[index-action-type]] +=== Index + +The index action type will index a document into {es}. + +[float] +[[index-connector-configuration]] +==== Connector configuration + +Index connectors have the following configuration properties: + +Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. +Index:: The {es} index to be written to. +Refresh:: Setting for the {ref}/docs-refresh.html[refresh] policy for the write request. +Execution time field:: This field will be automatically set to the time the alert condition was detected. + +[float] +[[index-action-configuration]] +==== Action configuration + +Index actions have the following properties: + +Document:: The document to index in json format. + +[float] +[[pagerduty-action-type]] +=== PagerDuty + +The PagerDuty action type uses the https://v2.developer.pagerduty.com/docs/events-api-v2[v2 Events API] to trigger, acknowledge, and resolve PagerDuty alerts. + +[float] +[[pagerduty-connector-configuration]] +==== Connector configuration + +PagerDuty connectors have the following configuration properties: + +Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. +API URL:: An optional PagerDuty event URL. Defaults to `https://events.pagerduty.com/v2/enqueue`. If you are using the <> setting, make sure the hostname is whitelisted. +Routing Key:: A 32 character PagerDuty Integration Key for an integration on a service or on a global ruleset. + +[float] +[[pagerduty-action-configuration]] +==== Action configuration + +PagerDuty actions have the following properties: + +Severity:: The perceived severity of on the affected system. This can be one of `Critical`, `Error`, `Warning` or `Info`(default). +Event action:: One of `Trigger` (default), `Resolve`, or `Acknowledge`. See https://v2.developer.pagerduty.com/docs/events-api-v2#event-action[event action] for more details. +Dedup Key:: All actions sharing this key will be associated with the same PagerDuty alert. This value is used to correlate trigger and resolution. This value is *optional*, and if unset defaults to `action:`. The maximum length is *255* characters. See https://v2.developer.pagerduty.com/docs/events-api-v2#alert-de-duplication[alert deduplication] for details. +Timestamp:: An *optional* https://v2.developer.pagerduty.com/v2/docs/types#datetime[ISO-8601 format date-time], indicating the time the event was detected or generated. +Component:: An *optional* value indicating the component of the source machine that is responsible for the event, for example `mysql` or `eth0`. +Group:: An *optional* value indicating the logical grouping of components of a service, for example `app-stack`. +Source:: An *optional* value indicating the affected system, preferably a hostname or fully qualified domain name. Defaults to the {kib} saved object id of the action. +Summary:: An *optional* text summary of the event, defaults to `No summary provided`. The maximum length is 1024 characters. +Class:: An *optional* value indicating the class/type of the event, for example `ping failure` or `cpu load`. + +For more details on these properties, see https://v2.developer.pagerduty.com/v2/docs/send-an-event-events-api-v2[PagerDuty v2 event parameters]. + +[float] +[[server-log-action-type]] +=== Server log + +This action type writes and entry to the {kib} server log. + +[float] +[[server-log-connector-configuration]] +==== Connector configuration + +Server log connectors have the following configuration properties: + +Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. + +[float] +[[server-log-action-configuration]] +==== Action configuration + +Server log actions have the following properties: + +Message:: The message to log. + +[float] +[[slack-action-type]] +=== Slack + +The Slack action type uses https://api.slack.com/incoming-webhooks[Slack Incoming Webhooks]. + +[float] +[[slack-connector-configuration]] +==== Connector configuration + +Slack connectors have the following configuration properties: + +Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. +Webhook URL:: The URL of the incoming webhook. See https://api.slack.com/messaging/webhooks#getting_started[Slack Incoming Webhooks] for instructions on generating this URL. If you are using the <> setting, make sure the hostname is whitelisted. + +[float] +[[slack-action-configuration]] +==== Action configuration + +Slack actions have the following properties: + +Message:: The message text, converted to the `text` field in the Webhook JSON payload. Currently only the text field is supported. Markdown, images, and other advanced formatting are not yet supported. + +[float] +[[webhook-action-type]] +=== Webhook + +The Webhook action type uses https://github.com/axios/axios[axios] to send a POST or PUT request to a web service. + +[float] +[[webhook-connector-configuration]] +==== Connector configuration + +Webhook connectors have the following configuration properties: + +Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. +URL:: The request URL. If you are using the <> setting, make sure the hostname is whitelisted. +Method:: HTTP request method, either `post`(default) or `put`. +Headers:: A set of key-value pairs sent as headers with the request +User:: An optional username. If set, HTTP basic authentication is used. Currently only basic authentication is supported. +Password:: An optional password. If set, HTTP basic authentication is used. Currently only basic authentication is supported. + +[float] +[[webhook-action-configuration]] +==== Action configuration + +Webhook actions have the following properties: + +Body:: A json payload sent to the request URL. \ No newline at end of file diff --git a/docs/user/alerting/alert-types.asciidoc b/docs/user/alerting/alert-types.asciidoc new file mode 100644 index 0000000000000..4a99c70f9d961 --- /dev/null +++ b/docs/user/alerting/alert-types.asciidoc @@ -0,0 +1,115 @@ +[role="xpack"] +[[alert-types]] +== Alert types + +{kib} supplies alerts types in two ways: some are built into {kib}, while domain-specific alert types are registered by {kib} apps such as <>, <>, and <>. + +This section covers built-in alert types. For domain-specific alert types, refer to the documentation for that app. + +Currently {kib} provides one built-in alert type: the <> type. + +[float] +[[alert-type-index-threshold]] +=== Index threshold + +The index threshold alert type is designed to run an {es} query over indices, aggregating field values from documents, comparing them to threshold values, and scheduling actions to run when the thresholds are met. + +[float] +==== Creating the alert + +An index threshold alert can be created from the *Create* button in the <>. Fill in the <>, then select *Index Threshold*. + +[role="screenshot"] +image::images/alert-types-index-threshold-select.png[Choosing an index threshold alert type] + +[float] +==== Defining the conditions + +The index threshold has 5 clauses that define the condition to detect. + +[role="screenshot"] +image::images/alert-types-index-threshold-conditions.png[Five clauses define the condition to detect] + +Index:: This clause requires an *index or index pattern* and a *time field* that will be used for the *time window*. +When:: This clause specifies how the value to be compared to the threshold is calculated. The value is calculated by aggregating a numeric field a the *time window*. The aggregation options are: `count`, `average`, `sum`, `min`, and `max`. When using `count` the document count is used, and an aggregation field is not necessary. +Over/Grouped Over:: This clause lets you configure whether the aggregation is applied over all documents, or should be split into groups using a grouping field. If grouping is used, an <> will be created for each group when it exceeds the threshold. To limit the number of instances on high cardinality fields, you must specify the number of groups to check against the threshold. Only the *top* groups are checked. +Threshold:: This clause defines a threshold value and a comparison operator (one of `is above`, `is above or equals`, `is below`, `is below or equals`, or `is between`). The result of the aggregation is compared to this threshold. +Time window:: This clause determines how far back to search for documents, using the *time field* set in the *index* clause. Generally this value should be to a value higher than the *check every* value in the <>, to avoid gaps in detection. + +If data is available and all clauses have been defined, a preview chart will render the threshold value and display a line chart showing the value for the last 30 intervals. This can provide an indication of recent values and their proximity to the threshold, and help you tune the clauses. + +[role="screenshot"] +image::images/alert-types-index-threshold-preview.png[Five clauses define the condition to detect] + +[float] +=== Example + +In this section, you will use the {kib} <> to setup and tune the conditions on an index threshold alert. For this example, we want to detect when any of our top three sites have served more than 420,000 bytes over a 24 hour period. + +From the <>, create a new alert, and fill in the <>. This alert will be checked every 4 hours, and will not execute actions more than once per day. Choose the index threshold alert type. + +[role="screenshot"] +image::images/alert-types-index-threshold-select.png[Choosing an index threshold alert type] + +Click on each clause to open a control that helps you set the value: + +[float] +==== Index clause +The index clause control will list and allow you to search for available indices. Choose *kibana_sample_data_logs* + +[role="screenshot"] +image::images/alert-types-index-threshold-example-index.png[Choosing an index] + +Once an index is selected, the list of time fields for that index will be available to select. Choose *@timestamp*. + +[role="screenshot"] +image::images/alert-types-index-threshold-example-timefield.png[Choosing a time field] + +[float] +==== When clause + +We want to detect the number of bytes served during the time window, so we select `sum` as the aggregation, and `bytes` as the field to aggregate. + +[role="screenshot"] +image::images/alert-types-index-threshold-example-aggregation.png[Choosing the aggregation] + +[float] +==== Over/Grouped over clause + +We want to alert on the three sites that have the most traffic, so we'll group the sum of bytes by the `host.keyword` field and take the top 3 values. + +[role="screenshot"] +image::images/alert-types-index-threshold-example-grouping.png[Choosing the groups] + +[float] +==== Threshold clause + +We want to alert when any site exceeds 420,000 bytes over a 24 hour period, so we'll set the threshold to 420,000 and use the `is above` comparison. + +[role="screenshot"] +image::images/alert-types-index-threshold-example-threshold.png[Setting the threshold] + +[float] +==== Time window clause + +Finally, set the time window to 24 hours to complete the alert configuration. + +[role="screenshot"] +image::images/alert-types-index-threshold-example-window.png[Setting the time window] + +The preview chart will render showing the 24 hour sum of bytes at 4 hours intervals (the *check every* interval) for the past 120 hours (the last 30 intervals). + +[role="screenshot"] +image::images/alert-types-index-threshold-example-preview.png[Setting the time window] + +[float] +==== Comparing time windows + +You can interactively change the time window and observe the effect it has on the chart. Compare a 24 window to a 12 hour window. Notice the variability in the sum of bytes, due to different traffic levels during the day compared to at night. This variability would result in noisy alerts, so the 24 hour window is better. The preview chart can help you find the right values for your alert. + +[role="screenshot"] +image::images/alert-types-index-threshold-example-comparison.png[Comparing two time windows] + + + + diff --git a/docs/user/alerting/alerting-scale-performance.asciidoc b/docs/user/alerting/alerting-scale-performance.asciidoc new file mode 100644 index 0000000000000..644a7143f8278 --- /dev/null +++ b/docs/user/alerting/alerting-scale-performance.asciidoc @@ -0,0 +1,28 @@ +[role="xpack"] +[[alerting-scale-performance]] +== Scale and performance + +{kib} alerting run both alert checks and actions as persistent background tasks. This has two major benefits: + +* *Persistence*: all task state and scheduling is stored in {es}, so if {kib} is restarted, alerts and actions will pick up where they left off. +* *Scaling*: multiple {kib} instances can read from and update the same task queue in {es}, allowing the alerting and action load to be distributed across instances. In cases where a {kib} instance no longer has capacity to run alert checks or actions, capacity can be increased by adding additional {kib} instances. + +[float] +=== Running background alert checks and actions + +{kib} background tasks are managed by: + +* Polling an {es} task index for overdue tasks at 3 second intervals. +* Tasks are then claiming them by updating them in the {es} index, using optimistic concurrency control to prevent conflicts. Each {kib} instance can run a maximum of 10 concurrent tasks, so a maximum of 10 tasks are claimed each interval. +* Tasks are run on the {kib} server. +* In the case of alerts which are recurring background checks, upon completion the task is scheduled again according to the <>. + +[IMPORTANT] +============================================== +Because tasks are polled at 3 second intervals and only 10 tasks can run concurrently per {kib} instance, it is possible for alert and action tasks to be run late. This can happen if: + +* Alerts use a small *check interval*. The lowest interval possible is 3 seconds, though intervals of 30 seconds or higher are recommended. +* Many alerts or actions must be *run at once*. In this case pending tasks will queue in {es}, and be pulled 10 at a time from the queue at 3 second intervals. +* *Long running tasks* occupy slots for an extended time, leaving fewer slots for other tasks. + +============================================== \ No newline at end of file diff --git a/docs/user/alerting/defining-alerts.asciidoc b/docs/user/alerting/defining-alerts.asciidoc new file mode 100644 index 0000000000000..89c4c88708d58 --- /dev/null +++ b/docs/user/alerting/defining-alerts.asciidoc @@ -0,0 +1,80 @@ +[role="xpack"] +[[defining-alerts]] +== Defining alerts + +{kib} alerts can be created in a variety of apps including <>, <>, <>, <> and from <> UI. While alerting details may differ from app to app, they share a common interface for defining and configuring alerts that this section describes in more detail. + +[float] +=== Alert flyout + +When an alert is created in an app, the app will display a flyout panel with three main sections to configure: + +. <> +. <> +. <> + +image::images/alert-flyout-sections.png[The three sections of an alert definition] + +[float] +[[defining-alerts-general-details]] +=== General alert details + +All alert share the following four properties in common: + +[role="screenshot"] +image::images/alert-flyout-general-details.png[All alerts have name, tags, check every, and re-notify every properties in common] + +Name:: The name of the alert. While this name does not have to be unique, the name can be referenced in actions and also appears in the searchable alert listing in the management UI. A distinctive name can help identify and find an alert. +Tags:: A list of tag names that can be applied to an alert. Tags can help you organize and find alerts, because tags appear in the alert listing in the management UI which is searchable by tag. +Check every:: This value determines how frequently the alert conditions below are checked. Note that the timing of background alert checks are not guaranteed, particularly for intervals of less than 10 seconds. See <> for more information. +Re-notify every:: This value limits how often actions are repeated when an alert instance remains active across alert checks. See <> for more information. + +[float] +[[defining-alerts-type-conditions]] +=== Alert type and conditions + +Depending upon the {kib} app and context, you may be prompted to choose the type of alert you wish to create. Some apps will pre-select the type of alert for you. + +[role="screenshot"] +image::images/alert-flyout-alert-type-selection.png[Choosing the type of alert to create] + +Each alert type provides its own way of defining the conditions to detect, but an expression formed by a series of clauses is a common pattern. Each clause has a UI control that allows you to define the clause. For example, in an index threshold alert the `WHEN` clause allows you to select an aggregation operation to apply to a numeric field. + +[role="screenshot"] +image::images/alert-flyout-alert-conditions.png[UI for defining alert conditions on an index threshold alert] + +[float] +[[defining-alerts-actions-details]] +=== Action type and action details + +To add an action to an alert, you first select the type of action: + +[role="screenshot"] +image::images/alert-flyout-action-type-selection.png[UI for selecting an action type] + +Each action must specify a <> instance. If no connectors exist for that action type, click "Add new" to create one. + +Each action type exposes different properties. For example an email action allows you to set the recipients, the subject, and a message body in markdown format. See <> for details on the types of actions provided by {kib} and their properties. + +[role="screenshot"] +image::images/alert-flyout-action-details.png[UI for defining an email action] + +Using the https://mustache.github.io/[Mustache] template syntax `{{variable name}}`, you can pass alert values at the time a condition is detected to an action. Available variables differ by alert type, and a list can be accessed using the "add variable" button at the right of the text box. + +[role="screenshot"] +image::images/alert-flyout-action-variables.png[Passing alert values to an action] + +You can attach more than one action. Clicking the "Add action" button will prompt you to select another alert type and repeat the above steps again. + +[role="screenshot"] +image::images/alert-flyout-add-action.png[You can add multiple actions on an alert] + +[NOTE] +============================================== +Actions are not required on alerts. In some cases you may want to run an alert without actions first to understand its behavior, and configure actions later. +============================================== + +[float] +=== Managing alerts + +To modify an alert after it was created, including muting or disabling it, use the <>. \ No newline at end of file diff --git a/docs/user/alerting/images/alert-concepts-connectors.svg b/docs/user/alerting/images/alert-concepts-connectors.svg new file mode 100644 index 0000000000000..9ae4c21ee5870 --- /dev/null +++ b/docs/user/alerting/images/alert-concepts-connectors.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/user/alerting/images/alert-concepts-summary.svg b/docs/user/alerting/images/alert-concepts-summary.svg new file mode 100644 index 0000000000000..d11023b706418 --- /dev/null +++ b/docs/user/alerting/images/alert-concepts-summary.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/user/alerting/images/alert-flyout-action-details.png b/docs/user/alerting/images/alert-flyout-action-details.png new file mode 100644 index 0000000000000..06287ed1837d2 Binary files /dev/null and b/docs/user/alerting/images/alert-flyout-action-details.png differ diff --git a/docs/user/alerting/images/alert-flyout-action-type-selection.png b/docs/user/alerting/images/alert-flyout-action-type-selection.png new file mode 100644 index 0000000000000..e4448ca5f3fcd Binary files /dev/null and b/docs/user/alerting/images/alert-flyout-action-type-selection.png differ diff --git a/docs/user/alerting/images/alert-flyout-action-variables.png b/docs/user/alerting/images/alert-flyout-action-variables.png new file mode 100644 index 0000000000000..73fc1fe9dea63 Binary files /dev/null and b/docs/user/alerting/images/alert-flyout-action-variables.png differ diff --git a/docs/user/alerting/images/alert-flyout-add-action.png b/docs/user/alerting/images/alert-flyout-add-action.png new file mode 100644 index 0000000000000..159dbd5c1e449 Binary files /dev/null and b/docs/user/alerting/images/alert-flyout-add-action.png differ diff --git a/docs/user/alerting/images/alert-flyout-alert-conditions.png b/docs/user/alerting/images/alert-flyout-alert-conditions.png new file mode 100644 index 0000000000000..f3e8f42ff0f37 Binary files /dev/null and b/docs/user/alerting/images/alert-flyout-alert-conditions.png differ diff --git a/docs/user/alerting/images/alert-flyout-alert-type-selection.png b/docs/user/alerting/images/alert-flyout-alert-type-selection.png new file mode 100644 index 0000000000000..a0a25dc5f1bbc Binary files /dev/null and b/docs/user/alerting/images/alert-flyout-alert-type-selection.png differ diff --git a/docs/user/alerting/images/alert-flyout-general-details.png b/docs/user/alerting/images/alert-flyout-general-details.png new file mode 100644 index 0000000000000..db56c16c1c308 Binary files /dev/null and b/docs/user/alerting/images/alert-flyout-general-details.png differ diff --git a/docs/user/alerting/images/alert-flyout-sections.png b/docs/user/alerting/images/alert-flyout-sections.png new file mode 100644 index 0000000000000..8fa4bfffe39a3 Binary files /dev/null and b/docs/user/alerting/images/alert-flyout-sections.png differ diff --git a/docs/user/alerting/images/alert-instances.svg b/docs/user/alerting/images/alert-instances.svg new file mode 100644 index 0000000000000..b7b0bd4996053 --- /dev/null +++ b/docs/user/alerting/images/alert-instances.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/user/alerting/images/alert-types-index-threshold-conditions.png b/docs/user/alerting/images/alert-types-index-threshold-conditions.png new file mode 100644 index 0000000000000..356732dfb9777 Binary files /dev/null and b/docs/user/alerting/images/alert-types-index-threshold-conditions.png differ diff --git a/docs/user/alerting/images/alert-types-index-threshold-example-aggregation.png b/docs/user/alerting/images/alert-types-index-threshold-example-aggregation.png new file mode 100644 index 0000000000000..fc40da7436547 Binary files /dev/null and b/docs/user/alerting/images/alert-types-index-threshold-example-aggregation.png differ diff --git a/docs/user/alerting/images/alert-types-index-threshold-example-comparison.png b/docs/user/alerting/images/alert-types-index-threshold-example-comparison.png new file mode 100644 index 0000000000000..5e7c65e1247d8 Binary files /dev/null and b/docs/user/alerting/images/alert-types-index-threshold-example-comparison.png differ diff --git a/docs/user/alerting/images/alert-types-index-threshold-example-grouping.png b/docs/user/alerting/images/alert-types-index-threshold-example-grouping.png new file mode 100644 index 0000000000000..ea3a3849c8927 Binary files /dev/null and b/docs/user/alerting/images/alert-types-index-threshold-example-grouping.png differ diff --git a/docs/user/alerting/images/alert-types-index-threshold-example-index.png b/docs/user/alerting/images/alert-types-index-threshold-example-index.png new file mode 100644 index 0000000000000..8f818f7001278 Binary files /dev/null and b/docs/user/alerting/images/alert-types-index-threshold-example-index.png differ diff --git a/docs/user/alerting/images/alert-types-index-threshold-example-preview.png b/docs/user/alerting/images/alert-types-index-threshold-example-preview.png new file mode 100644 index 0000000000000..b5d9c38d99810 Binary files /dev/null and b/docs/user/alerting/images/alert-types-index-threshold-example-preview.png differ diff --git a/docs/user/alerting/images/alert-types-index-threshold-example-threshold.png b/docs/user/alerting/images/alert-types-index-threshold-example-threshold.png new file mode 100644 index 0000000000000..9c51807b8d219 Binary files /dev/null and b/docs/user/alerting/images/alert-types-index-threshold-example-threshold.png differ diff --git a/docs/user/alerting/images/alert-types-index-threshold-example-timefield.png b/docs/user/alerting/images/alert-types-index-threshold-example-timefield.png new file mode 100644 index 0000000000000..24e4e03f829ce Binary files /dev/null and b/docs/user/alerting/images/alert-types-index-threshold-example-timefield.png differ diff --git a/docs/user/alerting/images/alert-types-index-threshold-example-window.png b/docs/user/alerting/images/alert-types-index-threshold-example-window.png new file mode 100644 index 0000000000000..5405415958485 Binary files /dev/null and b/docs/user/alerting/images/alert-types-index-threshold-example-window.png differ diff --git a/docs/user/alerting/images/alert-types-index-threshold-preview.png b/docs/user/alerting/images/alert-types-index-threshold-preview.png new file mode 100644 index 0000000000000..3709f162b612b Binary files /dev/null and b/docs/user/alerting/images/alert-types-index-threshold-preview.png differ diff --git a/docs/user/alerting/images/alert-types-index-threshold-select.png b/docs/user/alerting/images/alert-types-index-threshold-select.png new file mode 100644 index 0000000000000..0c2776e01b962 Binary files /dev/null and b/docs/user/alerting/images/alert-types-index-threshold-select.png differ diff --git a/docs/user/alerting/images/alerting-overview.png b/docs/user/alerting/images/alerting-overview.png new file mode 100644 index 0000000000000..383bc8c2ce015 Binary files /dev/null and b/docs/user/alerting/images/alerting-overview.png differ diff --git a/docs/user/alerting/images/what-is-an-action.svg b/docs/user/alerting/images/what-is-an-action.svg new file mode 100644 index 0000000000000..7675f717a6c84 --- /dev/null +++ b/docs/user/alerting/images/what-is-an-action.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/user/alerting/images/what-is-an-alert.svg b/docs/user/alerting/images/what-is-an-alert.svg new file mode 100644 index 0000000000000..b4dc02fae39a5 --- /dev/null +++ b/docs/user/alerting/images/what-is-an-alert.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/user/alerting/index.asciidoc b/docs/user/alerting/index.asciidoc new file mode 100644 index 0000000000000..b4f7e6af3d61c --- /dev/null +++ b/docs/user/alerting/index.asciidoc @@ -0,0 +1,202 @@ +[role="xpack"] +[[alerting-getting-started]] += Alerting and Actions + +beta[] + +-- + +Alerting allows you to detect complex conditions within different {kib} apps and trigger actions when those conditions are met. Alerting is integrated with <>, <>, <>, <>, can be centrally managed from the <> UI, and provides a set of built-in <> and <> for you to use. + +image::images/alerting-overview.png[Alerts and actions UI] + +[IMPORTANT] +============================================== +To make sure you can access alerting and actions, see the <> section. +============================================== + +[float] +== Concepts and terminology + +*Alerts* work by running checks on a schedule to detect conditions. When a condition is met, the alert tracks it as an *alert instance* and responds by triggering one or more *actions*. +Actions typically involve interaction with {kib} services or third party integrations. *Connectors* allow actions to talk to these services and integrations. +This section describes all of these elements and how they operate together. + +[float] +=== What is an alert? + +An alert specifies a background task that runs on the {kib} server to check for specific conditions. It consists of three main parts: + +* *Conditions*: what needs to be detected? +* *Schedule*: when/how often should detection checks run? +* *Actions*: what happens when a condition is detected? + +For example, when monitoring a set of servers, an alert might check for average CPU usage > 0.9 on each server for the two minutes (condition), checked every minute (schedule), sending a warning email message via SMTP with subject `CPU on {{server}} is high` (action). + +image::images/what-is-an-alert.svg[Three components of an alert] + +The following sections each part of the alert is described in more detail. + +[float] +[[alerting-concepts-conditions]] +==== Conditions + +Under the hood, {kib} alerts detect conditions by running javascript function on the {kib} server, which gives it flexibility to support a wide range of detections, anything from the results of a simple {es} query to heavy computations involving data from multiple sources or external systems. + +These detections are packaged and exposed as *alert types*. An alert type hides the underlying details of the detection, and exposes a set of parameters +to control the details of the conditions to detect. + +For example, an <> lets you specify the index to query, an aggregation field, and a time window, but the details of the underlying {es} query are hidden. + +See <> for the types of alerts provided by {kib} and how they express their conditions. + +[float] +[[alerting-concepts-scheduling]] +==== Schedule + +Alert schedules are defined as an interval between subsequent checks, and can range from a few seconds to months. + +[IMPORTANT] +============================================== +The intervals of alert checks in {kib} are approximate, their timing of their execution is affected by factors such as the frequency at which tasks are claimed and the task load on the system. See <> for more information. +============================================== + +[float] +[[alerting-concepts-actions]] +==== Actions + +Actions are invocations of {kib} services or integrations with third-party systems, that run as background tasks on the {kib} server when alert conditions are met. + +When defining actions in an alert, you specify +* the *action type*: the type of service or integration to use> +* the connection for that type by referencing a <>. +* a mapping of alert values to properties exposed for that type of action. + +The result is a template: all the parameters needed to invoke a service are supplied except for specific values that are only known at the time the alert condition is detected. + +In the server monitoring example, the `email` action type is used, and `server` is mapped to the body of the email, using the template string `CPU on {{server}} is high`. + +When the alert detects the condition, it creates an <> containing the details of the condition, renders the template with these details such as server name, and executes the action on the {kib} server by invoking the `email` action type. + +image::images/what-is-an-action.svg[Actions are like templates that are rendered when an alert detects a condition] + +See <> for details on the types of actions provided by {kib}. + +[float] +[[alerting-concepts-alert-instances]] +=== Alert instances + +When checking for a condition, an alert might identify multiple occurrences of the condition. {kib} tracks each of these *alert instances* separately and takes action per instance. + +Using the server monitoring example, each server with average CPU > 0.9 is tracked as an alert instance. This means a separate email is sent for each server that exceeds the threshold. + +image::images/alert-instances.svg[{kib} tracks each detected condition as an alert instance and takes action on each instance] + +[float] +[[alerting-concepts-suppressing-duplicate-notifications]] +=== Suppressing duplicate notifications + +Since actions are taken per instance, alerts can end up generating a large number of actions. Take the following example where an alert is monitoring three servers every minute for CPU usage > 0.9: + +* Minute 1: server X123 > 0.9. *One email* is sent for server X123. +* Minute 2: X123 and Y456 > 0.9. *Two emails* are sent, on for X123 and one for Y456. +* Minute 3: X123, Y456, Z789 > 0.9. *Three emails* are sent, one for each of X123, Y456, Z789. + +In the above example, three emails are sent for server X123 in the span of 3 minutes for the same condition. Often it's desirable to suppress frequent re-notification. Operations like muting and re-notification throttling can be applied at the instance level. If we set the alert re-notify interval to 5 minutes, we reduce noise by only getting emails for new servers that exceed the threshold: + +* Minute 1: server X123 > 0.9. *One email* is sent for server X123. +* Minute 2: X123 and Y456 > 0.9. *One email* is sent for Y456 +* Minute 3: X123, Y456, Z789 > 0.9. *One email* is sent for Z789. + +[float] +[[alerting-concepts-connectors]] +=== Connectors + +Actions often involve connecting with services inside {kib} or integrations with third-party systems. +Rather than repeatedly entering connection information and credentials for each action, {kib} simplifies action setup using *connectors*. + +*Connectors* provide a central place to store connection information for services and integrations. For example if four alerts send email notifications via the same SMTP service, +they all reference the same SMTP connector. When the SMTP settings change they are updated once in the connector, instead of having to update four alerts. + +image::images/alert-concepts-connectors.svg[Connectors provide a central place to store service connection settings] + +[float] +=== Summary + +An _alert_ consists of conditions, _actions_, and a schedule. When conditions are met, _alert instances_ are created that render _actions_ and invoke them. To make action setup and update easier, actions refer to _connectors_ that centralize the information used to connect with {kib} services and third-party integrations. + +image::images/alert-concepts-summary.svg[Alerts, actions, alert instances and connectors work together to convert detection into action] + +* *Alert*: a specification of the conditions to be detected, the schedule for detection, and the response when detection occurs. +* *Action*: the response to a detected condition defined in the alert. Typically actions specify a service or third party integration along with alert details that will be sent to it. +* *Alert instance*: state tracked by {kib} for every occurrence of a detected condition. Actions as well as controls like muting and re-notification are controlled at the instance level. +* *Connector*: centralized configurations for services and third party integration that are referenced by actions. + +[float] +[[alerting-concepts-differences]] +== Differences from Watcher + +{kib} alerting and <> are both used to detect conditions and can trigger actions in response, but they are completely independent alerting systems. + +This section will clarify some of the important differences in the function and intent of the two systems. + +Functionally, {kib} alerting differs in that: + +* Scheduled checks are run on {kib} instead of {es} +* {kib} <> through *alert types*, whereas watches provide low-level control over inputs, conditions, and transformations. +* {kib} alerts tracks and persists the state of each detected condition through *alert instances*. This makes it possible to mute and throttle individual instances, and detect changes in state such as resolution. +* Actions are linked to *alert instances* in {kib} alerting. Actions are fired for each occurrence of a detected condition, rather than for the entire alert. + +At a higher level, {kib} alerts allow rich integrations across use cases like <>, <>, <>, and <>. +Pre-packaged *alert types* simplify setup, hide the details complex domain-specific detections, while providing a consistent interface across {kib}. + +[float] +[[alerting-setup-prerequisites]] +== Setup and prerequisites + +If you are using an *on-premises* Elastic Stack deployment with <>: + +* TLS must be configured for communication <>. {kib} alerting uses <> to secure background alert checks and actions, and API keys require {ref}/configuring-tls.html#tls-http[TLS on the HTTP interface]. +* In the kibana.yml configuration file, add the <> + +[float] +[[alerting-security]] +== Security + +To access alerting in a space, a user must have access to one of the following features: +* <> +* <> +* <> +* <> + +See <> for more information on configuring roles that provide access to these features. + +[float] +[[alerting-spaces]] +=== Space isolation + +Alerts and connectors are isolated to the {kib} space in which they were created. An alert or connector created in one space will not be visible in another. + +[float] +[[alerting-authorization]] +=== Authorization + +Alerts, including all background detection and the actions they generate are authorized using an <> associated with the last user to edit the alert. Upon creating or modifying an alert, an API key is generated for that user, capturing a snapshot of their privileges at that moment in time. The API key is then used to run all background tasks associated with the alert including detection checks and executing actions. + +[IMPORTANT] +============================================== +If an alert requires certain privileges to run such as index privileges, keep in mind that if a user without those privileges updates the alert, the alert will no longer function. +============================================== + +[float] +[[alerting-restricting-actions]] +=== Restricting actions + +For security reasons you may wish to limit the extent to which {kib} can connect to external services. <> allows you to disable certain <> and whitelist the hostnames that {kib} can connect with. + +-- + +include::defining-alerts.asciidoc[] +include::action-types.asciidoc[] +include::alert-types.asciidoc[] +include::alerting-scale-performance.asciidoc[] diff --git a/docs/user/index.asciidoc b/docs/user/index.asciidoc index ff100d0763368..0f53497716552 100644 --- a/docs/user/index.asciidoc +++ b/docs/user/index.asciidoc @@ -40,6 +40,8 @@ include::management.asciidoc[] include::reporting/index.asciidoc[] +include::alerting/index.asciidoc[] + include::api.asciidoc[] include::plugins.asciidoc[] diff --git a/docs/user/management.asciidoc b/docs/user/management.asciidoc index 34a3790529ca3..fa34802abe2a9 100644 --- a/docs/user/management.asciidoc +++ b/docs/user/management.asciidoc @@ -83,6 +83,10 @@ a| <> Customize {kib} to suit your needs. Change the format for displaying dates, turn on dark mode, set the timespan for notification messages, and much more. +| <> + +Centrally manage your alerts from across {kib}. Create and manage re-usable connectors for triggering actions. + | <> Create and manage the index patterns that help you retrieve your data from {es}. @@ -111,6 +115,14 @@ so you can tailor it to your needs without impacting others. include::{kib-repo-dir}/management/advanced-options.asciidoc[] +include::{kib-repo-dir}/management/alerting/alerts-and-actions-intro.asciidoc[] + +include::{kib-repo-dir}/management/alerting/alert-management.asciidoc[] + +include::{kib-repo-dir}/management/alerting/alert-details.asciidoc[] + +include::{kib-repo-dir}/management/alerting/connector-management.asciidoc[] + include::{kib-repo-dir}/management/managing-beats.asciidoc[] include::{kib-repo-dir}/management/index-lifecycle-policies/intro-to-lifecycle-policies.asciidoc[] diff --git a/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js b/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js index d7d4dc14519c3..f434d09c6bf81 100644 --- a/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js +++ b/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js @@ -17,163 +17,46 @@ * under the License. */ -/* eslint-disable no-restricted-syntax */ import { spawn } from 'child_process'; +import Fs from 'fs'; import { resolve } from 'path'; -import util from 'util'; -import { stat, readFileSync } from 'fs'; -import { snakeCase } from 'lodash'; +import { promisify } from 'util'; + import del from 'del'; -import { ProcRunner, ToolingLog } from '@kbn/dev-utils'; -import { createLegacyEsTestCluster } from '@kbn/test'; -import execa from 'execa'; +import { snakeCase } from 'lodash'; -const statP = util.promisify(stat); +const statAsync = promisify(Fs.stat); const ROOT_DIR = resolve(__dirname, '../../../'); -const oneMinute = 60000; - -describe(`running the plugin-generator via 'node scripts/generate_plugin.js plugin-name' with default config`, () => { - const pluginName = 'ispec-plugin'; - const snakeCased = snakeCase(pluginName); - const generatedPath = resolve(ROOT_DIR, `plugins/${snakeCased}`); - const collect = xs => data => xs.push(data + ''); // Coerce from Buffer to String - - beforeAll(() => { - jest.setTimeout(oneMinute * 10); - }); - - beforeAll(done => { - const create = spawn(process.execPath, ['scripts/generate_plugin.js', pluginName], { - cwd: ROOT_DIR, - }); - create.stdout.on('data', function selectDefaults() { - create.stdin.write('\n'); // Generate a plugin with default options. - }); - create.on('close', done); - }); - - afterAll(() => { - del.sync(generatedPath, { force: true }); - }); - - it(`should succeed on creating a plugin in a directory named 'plugins/${snakeCased}`, async () => { - const stats = await statP(generatedPath); - expect(stats.isDirectory()).toBe(true); - }); - // skipped until internationalization is re-introduced - it.skip(`should create an internationalization config file with a blank line appended to satisfy the parser`, async () => { - // Link to the error that happens when the blank line is not there: - // https://github.com/elastic/kibana/pull/45044#issuecomment-530092627 - const intlFile = `${generatedPath}/.i18nrc.json`; - expect(readFileSync(intlFile, 'utf8').endsWith('\n\n')).toBe(true); - }); - - describe(`then running`, () => { - it(`'yarn test:karma' should exit 0`, async () => { - await execa('yarn', ['test:karma'], { - cwd: generatedPath, - env: { - DISABLE_JUNIT_REPORTER: '1', - }, - }); - }); - - it.skip(`'yarn build' should exit 0`, async () => { - await execa('yarn', ['build'], { cwd: generatedPath }); - }); +const pluginName = 'ispec-plugin'; +const snakeCased = snakeCase(pluginName); +const generatedPath = resolve(ROOT_DIR, `plugins/${snakeCased}`); - describe('with es instance', () => { - const log = new ToolingLog({ - level: 'verbose', - writeTo: process.stdout, - }); - const pr = new ProcRunner(log); - - const es = createLegacyEsTestCluster({ license: 'basic', log }); - beforeAll(es.start); - afterAll(es.stop); - afterAll(() => pr.teardown()); +beforeAll(async () => { + await del(generatedPath, { force: true }); +}); - it(`'yarn start' should result in the spec plugin being initialized on kibana's stdout`, async () => { - await pr.run('kibana', { - cmd: 'yarn', - args: [ - 'start', - '--optimize.enabled=false', - '--logging.json=false', - '--logging.verbose=true', - '--migrations.skip=true', - ], - cwd: generatedPath, - wait: new RegExp('\\[ispecPlugin\\]\\[plugins\\] Setting up plugin'), - }); - await pr.stop('kibana'); - }); - }); +afterAll(async () => { + await del(generatedPath, { force: true }); +}); - it(`'yarn preinstall' should exit 0`, async () => { - await execa('yarn', ['preinstall'], { cwd: generatedPath }); +it('generates a plugin', async () => { + await new Promise((resolve, reject) => { + const proc = spawn(process.execPath, ['scripts/generate_plugin.js', pluginName], { + cwd: ROOT_DIR, + stdio: 'pipe', }); - it.skip(`'yarn lint' should exit 0`, async () => { - await execa('yarn', ['lint'], { cwd: generatedPath }); + proc.stdout.on('data', function selectDefaults() { + proc.stdin.write('\n'); // Generate a plugin with default options. }); - it(`'yarn kbn --help' should print out the kbn help msg`, done => { - const helpMsg = ` -usage: kbn [] - -By default commands are run for Kibana itself, all packages in the 'packages/' -folder and for all plugins in './plugins' and '../kibana-extra'. - -Available commands: - - bootstrap - Install dependencies and crosslink projects - clean - Remove the node_modules and target directories from all projects. - run - Run script defined in package.json in each package that contains that script. - watch - Runs \`kbn:watch\` script for every project. - -Global options: - - -e, --exclude Exclude specified project. Can be specified multiple times to exclude multiple projects, e.g. '-e kibana -e @kbn/pm'. - -i, --include Include only specified projects. If left unspecified, it defaults to including all projects. - --oss Do not include the x-pack when running command. - --skip-kibana-plugins Filter all plugins in ./plugins and ../kibana-extra when running command. -`; - const outData = []; - const kbnHelp = spawn('yarn', ['kbn', '--help'], { cwd: generatedPath }); - kbnHelp.stdout.on('data', collect(outData)); - kbnHelp.on('close', () => { - expect(outData.join('\n')).toContain(helpMsg); - done(); - }); - }); - - it(`'yarn es --help' should print out the es help msg`, done => { - const helpMsg = ` -usage: es [] - -Assists with running Elasticsearch for Kibana development - -Available commands: - - snapshot - Downloads and run from a nightly snapshot - source - Build and run from source - archive - Install and run from an Elasticsearch tar - build_snapshots - Build and collect ES snapshots - -Global options: - - --help -`; - const outData = []; - const kbnHelp = spawn('yarn', ['es', '--help'], { cwd: generatedPath }); - kbnHelp.stdout.on('data', collect(outData)); - kbnHelp.on('close', () => { - expect(outData.join('\n')).toContain(helpMsg); - done(); - }); - }); + proc.on('close', resolve); + proc.on('error', reject); }); + + const stats = await statAsync(generatedPath); + if (!stats.isDirectory()) { + throw new Error(`Expected [${generatedPath}] to be a directory`); + } }); diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/NoServicesMessage.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/NoServicesMessage.tsx index de058d6ef973a..c1afa433cb614 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/NoServicesMessage.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/NoServicesMessage.tsx @@ -58,7 +58,7 @@ export function NoServicesMessage({ historicalDataFound, status }: Props) {

{i18n.translate('xpack.apm.servicesTable.7xUpgradeServerMessage', { defaultMessage: `Upgrading from a pre-7.x version? Make sure you've also upgraded - your APM server instance(s) to at least 7.0.` + your APM Server instance(s) to at least 7.0.` })}

diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/NoServicesMessage.test.tsx.snap b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/NoServicesMessage.test.tsx.snap index 209b88f73b9e2..227becb9a9c4f 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/NoServicesMessage.test.tsx.snap +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/NoServicesMessage.test.tsx.snap @@ -19,7 +19,7 @@ exports[`NoServicesMessage status: pending and historicalDataFound: false 1`] =

Upgrading from a pre-7.x version? Make sure you've also upgraded - your APM server instance(s) to at least 7.0. + your APM Server instance(s) to at least 7.0.

You may also have old data that needs to be migrated. @@ -64,7 +64,7 @@ exports[`NoServicesMessage status: success and historicalDataFound: false 1`] =

Upgrading from a pre-7.x version? Make sure you've also upgraded - your APM server instance(s) to at least 7.0. + your APM Server instance(s) to at least 7.0.

You may also have old data that needs to be migrated. diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap index d85c83661ea49..dd0c50af2b03f 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap @@ -72,7 +72,7 @@ NodeList [ >

Upgrading from a pre-7.x version? Make sure you've also upgraded - your APM server instance(s) to at least 7.0. + your APM Server instance(s) to at least 7.0.

You may also have old data that needs to be migrated. diff --git a/x-pack/legacy/plugins/maps/public/angular/map_controller.js b/x-pack/legacy/plugins/maps/public/angular/map_controller.js index a19f8cdf5f7fc..519ba0b1e3d96 100644 --- a/x-pack/legacy/plugins/maps/public/angular/map_controller.js +++ b/x-pack/legacy/plugins/maps/public/angular/map_controller.js @@ -312,12 +312,7 @@ app.controller( const savedLayerList = savedMap.getLayerList(); const oldConfig = savedLayerList ? savedLayerList : initialLayerListConfig; - // savedMap stores layerList as a JSON string using JSON.stringify. - // JSON.stringify removes undefined properties from objects. - // savedMap.getLayerList converts the JSON string back into Javascript array of objects. - // Need to perform the same process for layerListConfigOnly to compare apples to apples - // and avoid undefined properties in layerListConfigOnly triggering unsaved changes. - return !_.isEqual(JSON.parse(JSON.stringify(layerListConfigOnly)), oldConfig); + return !_.isEqual(layerListConfigOnly, oldConfig); } function isOnMapNow() { diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js index 5e8f720fcc5e3..a1c15e27c9eb3 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js @@ -28,20 +28,12 @@ export function DynamicColorForm({ }; if (type === COLOR_MAP_TYPE.ORDINAL) { newColorOptions.useCustomColorRamp = useCustomColorMap; - if (customColorMap) { - newColorOptions.customColorRamp = customColorMap; - } - if (color) { - newColorOptions.color = color; - } + newColorOptions.customColorRamp = customColorMap; + newColorOptions.color = color; } else { newColorOptions.useCustomColorPalette = useCustomColorMap; - if (customColorMap) { - newColorOptions.customColorPalette = customColorMap; - } - if (color) { - newColorOptions.colorCategory = color; - } + newColorOptions.customColorPalette = customColorMap; + newColorOptions.colorCategory = color; } onDynamicStyleChange(styleProperty.getStyleName(), newColorOptions); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts index 2712b8f6ea9b5..aa9b1dcfcf239 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts @@ -226,15 +226,19 @@ describe('handleUpdateIncident', () => { const res = await handleUpdateIncident({ incidentId: '123', serviceNow, - params, + params: { + ...params, + updatedAt: '2020-03-15T08:34:53.450Z', + updatedBy: { fullName: 'Another User', username: 'anotherUser' }, + }, comments: [], mapping: finalMapping, }); expect(serviceNow.updateIncident).toHaveBeenCalled(); expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { - short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - description: 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + short_description: 'a title (updated at 2020-03-15T08:34:53.450Z by Another User)', + description: 'a description (updated at 2020-03-15T08:34:53.450Z by Another User)', }); expect(serviceNow.updateIncident).toHaveReturned(); expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); @@ -256,7 +260,11 @@ describe('handleUpdateIncident', () => { const res = await handleUpdateIncident({ incidentId: '123', serviceNow, - params, + params: { + ...params, + updatedAt: '2020-03-15T08:34:53.450Z', + updatedBy: { fullName: 'Another User', username: 'anotherUser' }, + }, comments: [ { comment: 'first comment', @@ -278,10 +286,10 @@ describe('handleUpdateIncident', () => { fullName: 'Elastic User', username: 'elastic', }, - updatedAt: '2020-03-13T08:34:53.450Z', + updatedAt: '2020-03-16T08:34:53.450Z', updatedBy: { - fullName: 'Elastic User', - username: 'elastic', + fullName: 'Another User', + username: 'anotherUser', }, version: 'WzU3LDFd', }, @@ -291,8 +299,8 @@ describe('handleUpdateIncident', () => { expect(serviceNow.updateIncident).toHaveBeenCalled(); expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { - description: 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + description: 'a description (updated at 2020-03-15T08:34:53.450Z by Another User)', + short_description: 'a title (updated at 2020-03-15T08:34:53.450Z by Another User)', }); expect(serviceNow.updateIncident).toHaveReturned(); expect(serviceNow.batchCreateComments).toHaveBeenCalled(); @@ -312,17 +320,17 @@ describe('handleUpdateIncident', () => { version: 'WzU3LDFd', }, { - comment: 'second comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', + comment: 'second comment (added at 2020-03-16T08:34:53.450Z by Another User)', commentId: '789', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', username: 'elastic', }, - updatedAt: '2020-03-13T08:34:53.450Z', + updatedAt: '2020-03-16T08:34:53.450Z', updatedBy: { - fullName: 'Elastic User', - username: 'elastic', + fullName: 'Another User', + username: 'anotherUser', }, version: 'WzU3LDFd', }, diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts index ce8c3542ab69f..cbcefe6364e8f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts @@ -191,13 +191,21 @@ describe('transformFields', () => { test('transform fields for update correctly', () => { const fields = prepareFieldsForTransformation({ - params: fullParams, + params: { + ...fullParams, + updatedAt: '2020-03-15T08:34:53.450Z', + updatedBy: { username: 'anotherUser', fullName: 'Another User' }, + }, mapping: finalMapping, defaultPipes: ['informationUpdated'], }); const res = transformFields({ - params: fullParams, + params: { + ...fullParams, + updatedAt: '2020-03-15T08:34:53.450Z', + updatedBy: { username: 'anotherUser', fullName: 'Another User' }, + }, fields, currentIncident: { short_description: 'first title (created at 2020-03-13T08:34:53.450Z by Elastic User)', @@ -205,9 +213,9 @@ describe('transformFields', () => { }, }); expect(res).toEqual({ - short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + short_description: 'a title (updated at 2020-03-15T08:34:53.450Z by Another User)', description: - 'first description (created at 2020-03-13T08:34:53.450Z by Elastic User) \r\na description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + 'first description (created at 2020-03-13T08:34:53.450Z by Elastic User) \r\na description (updated at 2020-03-15T08:34:53.450Z by Another User)', }); }); @@ -229,7 +237,7 @@ describe('transformFields', () => { expect(res.description?.includes('\r\n')).toBe(true); }); - test('append username if fullname is undefined', () => { + test('append username if fullname is undefined when create', () => { const fields = prepareFieldsForTransformation({ params: fullParams, mapping: finalMapping, @@ -245,6 +253,32 @@ describe('transformFields', () => { description: 'a description (created at 2020-03-13T08:34:53.450Z by elastic)', }); }); + + test('append username if fullname is undefined when update', () => { + const fields = prepareFieldsForTransformation({ + params: { + ...fullParams, + updatedAt: '2020-03-15T08:34:53.450Z', + updatedBy: { username: 'anotherUser', fullName: 'Another User' }, + }, + mapping: finalMapping, + defaultPipes: ['informationUpdated'], + }); + + const res = transformFields({ + params: { + ...fullParams, + updatedAt: '2020-03-15T08:34:53.450Z', + updatedBy: { username: 'anotherUser', fullName: null }, + }, + fields, + }); + + expect(res).toEqual({ + short_description: 'a title (updated at 2020-03-15T08:34:53.450Z by anotherUser)', + description: 'a description (updated at 2020-03-15T08:34:53.450Z by anotherUser)', + }); + }); }); describe('appendField', () => { @@ -330,8 +364,8 @@ describe('transformComments', () => { comment: 'first comment', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, + updatedAt: '2020-03-15T08:34:53.450Z', + updatedBy: { fullName: 'Another User', username: 'anotherUser' }, }, ]; const res = transformComments(comments, fullParams, ['informationUpdated']); @@ -339,11 +373,11 @@ describe('transformComments', () => { { commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', version: 'WzU3LDFd', - comment: 'first comment (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + comment: 'first comment (updated at 2020-03-15T08:34:53.450Z by Another User)', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, + updatedAt: '2020-03-15T08:34:53.450Z', + updatedBy: { fullName: 'Another User', username: 'anotherUser' }, }, ]); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts index 6dd3cc7baa760..750fda93b60d6 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts @@ -83,8 +83,11 @@ export const transformFields = ({ const transform = flow(...cur.pipes.map(p => t[p])); prev[cur.key] = transform({ value: cur.value, - date: params.createdAt, - user: params.createdBy.fullName ?? params.createdBy.username, + date: params.updatedAt ?? params.createdAt, + user: + params.updatedBy != null + ? params.updatedBy.fullName ?? params.updatedBy.username + : params.createdBy.fullName ?? params.createdBy.username, previousValue: currentIncident ? currentIncident[cur.key] : '', }).value; return prev; @@ -112,8 +115,11 @@ export const transformComments = ( ...c, comment: flow(...pipes.map(p => t[p]))({ value: c.comment, - date: c.createdAt, - user: c.createdBy.fullName ?? '', + date: c.updatedAt ?? c.createdAt, + user: + c.updatedBy != null + ? c.updatedBy.fullName ?? c.updatedBy.username + : c.createdBy.fullName ?? c.createdBy.username, }).value, })); }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts index 7eda7060df846..1a23354e6490d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts @@ -226,7 +226,11 @@ describe('execute()', () => { const executorOptions: ActionTypeExecutorOptions = { actionId, config: mockOptions.config, - params: { ...mockOptions.params, executorAction: 'updateIncident' }, + params: { + ...mockOptions.params, + updatedAt: '2020-03-15T08:34:53.450Z', + updatedBy: { fullName: 'Another User', username: 'anotherUser' }, + }, secrets: mockOptions.secrets, services, }; @@ -244,7 +248,11 @@ describe('execute()', () => { const executorOptions: ActionTypeExecutorOptions = { actionId, config: mockOptions.config, - params: { ...mockOptions.params, executorAction: 'updateIncident' }, + params: { + ...mockOptions.params, + updatedAt: '2020-03-15T08:34:53.450Z', + updatedBy: { fullName: 'Another User', username: 'anotherUser' }, + }, secrets: mockOptions.secrets, services, }; diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap index 365d8838a24a6..0c585bec22f6c 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap @@ -2,11 +2,6 @@ exports[`settingDefinitions should have correct default values 1`] = ` Array [ - Object { - "key": "active", - "type": "boolean", - "validationName": "(\\"true\\" | \\"false\\")", - }, Object { "key": "api_request_size", "type": "bytes", @@ -105,6 +100,11 @@ Array [ "validationError": "Please specify an integer and a unit", "validationName": "durationRt", }, + Object { + "key": "recording", + "type": "boolean", + "validationName": "(\\"true\\" | \\"false\\")", + }, Object { "key": "server_timeout", "type": "duration", diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts index b83c03c543295..6a0e2d65d1949 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts @@ -13,21 +13,6 @@ import { RawSettingDefinition } from './types'; * Settings added here will show up in the UI and will be validated on the client and server */ export const generalSettings: RawSettingDefinition[] = [ - // Active - { - key: 'active', - type: 'boolean', - defaultValue: 'true', - label: i18n.translate('xpack.apm.agentConfig.active.label', { - defaultMessage: 'Active' - }), - description: i18n.translate('xpack.apm.agentConfig.active.description', { - defaultMessage: - 'A boolean specifying if the agent should be active or not.\nWhen active, the agent instruments incoming HTTP requests, tracks errors and collects and sends metrics.\nWhen inactive, the agent works as a noop, not collecting data and not communicating with the APM Server.\nAs this is a reversible switch, agent threads are not being killed when inactivated, but they will be \nmostly idle in this state, so the overhead should be negligible.\n\nYou can use this setting to dynamically disable Elastic APM at runtime.' - }), - excludeAgents: ['js-base', 'rum-js', 'python', 'dotnet'] - }, - // API Request Size { key: 'api_request_size', @@ -40,7 +25,7 @@ export const generalSettings: RawSettingDefinition[] = [ 'xpack.apm.agentConfig.apiRequestSize.description', { defaultMessage: - 'The maximum total compressed size of the request body which is sent to the APM server intake api via a chunked encoding (HTTP streaming).\nNote that a small overshoot is possible.\n\nAllowed byte units are `b`, `kb` and `mb`. `1kb` is equal to `1024b`.' + 'The maximum total compressed size of the request body which is sent to the APM Server intake api via a chunked encoding (HTTP streaming).\nNote that a small overshoot is possible.\n\nAllowed byte units are `b`, `kb` and `mb`. `1kb` is equal to `1024b`.' } ), excludeAgents: ['js-base', 'rum-js', 'dotnet'] @@ -121,6 +106,20 @@ export const generalSettings: RawSettingDefinition[] = [ excludeAgents: ['js-base', 'rum-js', 'python'] }, + // Recording + { + key: 'recording', + type: 'boolean', + defaultValue: 'true', + label: i18n.translate('xpack.apm.agentConfig.recording.label', { + defaultMessage: 'Recording' + }), + description: i18n.translate('xpack.apm.agentConfig.recording.description', { + defaultMessage: + 'When recording, the agent instruments incoming HTTP requests, tracks errors, and collects and sends metrics. When inactive, the agent works as a noop, not collecting data and not communicating with the APM Server except for polling for updated configuration. As this is a reversible switch, agent threads are not being killed when inactivated, but they will be mostly idle in this state, so the overhead should be negligible. You can use this setting to dynamically control whether Elastic APM is enabled or disabled.' + }) + }, + // SERVER_TIMEOUT { key: 'server_timeout', @@ -133,7 +132,7 @@ export const generalSettings: RawSettingDefinition[] = [ 'xpack.apm.agentConfig.serverTimeout.description', { defaultMessage: - 'If a request to the APM server takes longer than the configured timeout,\nthe request is cancelled and the event (exception or transaction) is discarded.\nSet to 0 to disable timeouts.\n\nWARNING: If timeouts are disabled or set to a high value, your app could experience memory issues if the APM server times out.' + 'If a request to the APM Server takes longer than the configured timeout,\nthe request is cancelled and the event (exception or transaction) is discarded.\nSet to 0 to disable timeouts.\n\nWARNING: If timeouts are disabled or set to a high value, your app could experience memory issues if the APM Server times out.' } ), includeAgents: ['nodejs', 'java', 'go'] diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts index fe55442324c92..b0255d2d828bb 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts @@ -43,12 +43,12 @@ describe('filterByAgent', () => { describe('options per agent', () => { it('go', () => { expect(getSettingKeysForAgent('go')).toEqual([ - 'active', 'api_request_size', 'api_request_time', 'capture_body', 'capture_headers', 'log_level', + 'recording', 'server_timeout', 'span_frames_min_duration', 'stack_trace_limit', @@ -59,7 +59,6 @@ describe('filterByAgent', () => { it('java', () => { expect(getSettingKeysForAgent('java')).toEqual([ - 'active', 'api_request_size', 'api_request_time', 'capture_body', @@ -72,6 +71,7 @@ describe('filterByAgent', () => { 'profiling_inferred_spans_included_classes', 'profiling_inferred_spans_min_duration', 'profiling_inferred_spans_sampling_interval', + 'recording', 'server_timeout', 'span_frames_min_duration', 'stack_trace_limit', @@ -88,24 +88,26 @@ describe('filterByAgent', () => { it('js-base', () => { expect(getSettingKeysForAgent('js-base')).toEqual([ + 'recording', 'transaction_sample_rate' ]); }); it('rum-js', () => { expect(getSettingKeysForAgent('rum-js')).toEqual([ + 'recording', 'transaction_sample_rate' ]); }); it('nodejs', () => { expect(getSettingKeysForAgent('nodejs')).toEqual([ - 'active', 'api_request_size', 'api_request_time', 'capture_body', 'capture_headers', 'log_level', + 'recording', 'server_timeout', 'stack_trace_limit', 'transaction_max_spans', @@ -119,6 +121,7 @@ describe('filterByAgent', () => { 'api_request_time', 'capture_body', 'capture_headers', + 'recording', 'span_frames_min_duration', 'transaction_max_spans', 'transaction_sample_rate' @@ -130,6 +133,7 @@ describe('filterByAgent', () => { 'capture_body', 'capture_headers', 'log_level', + 'recording', 'span_frames_min_duration', 'stack_trace_limit', 'transaction_max_spans', @@ -139,12 +143,12 @@ describe('filterByAgent', () => { it('ruby', () => { expect(getSettingKeysForAgent('ruby')).toEqual([ - 'active', 'api_request_size', 'api_request_time', 'capture_body', 'capture_headers', 'log_level', + 'recording', 'span_frames_min_duration', 'transaction_max_spans', 'transaction_sample_rate' @@ -155,6 +159,7 @@ describe('filterByAgent', () => { expect(getSettingKeysForAgent(undefined)).toEqual([ 'capture_body', 'capture_headers', + 'recording', 'transaction_max_spans', 'transaction_sample_rate' ]); diff --git a/x-pack/plugins/apm/server/tutorial/instructions/apm_agent_instructions.ts b/x-pack/plugins/apm/server/tutorial/instructions/apm_agent_instructions.ts index d076008da9d8e..9695c331e0504 100644 --- a/x-pack/plugins/apm/server/tutorial/instructions/apm_agent_instructions.ts +++ b/x-pack/plugins/apm/server/tutorial/instructions/apm_agent_instructions.ts @@ -437,7 +437,7 @@ export const createJsAgentInstructions = (apmServerUrl = '') => [ title: i18n.translate( 'xpack.apm.tutorial.jsClient.enableRealUserMonitoring.title', { - defaultMessage: 'Enable Real User Monitoring support in APM server' + defaultMessage: 'Enable Real User Monitoring support in APM Server' } ), textPre: i18n.translate( diff --git a/x-pack/plugins/event_log/generated/mappings.json b/x-pack/plugins/event_log/generated/mappings.json index fc1fdb71b0c37..50e7fdd5a9048 100644 --- a/x-pack/plugins/event_log/generated/mappings.json +++ b/x-pack/plugins/event_log/generated/mappings.json @@ -1,12 +1,15 @@ { - "dynamic": "strict", + "dynamic": "false", "properties": { "@timestamp": { "type": "date" }, "tags": { "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "meta": { + "isArray": true + } }, "message": { "norms": false, @@ -18,8 +21,7 @@ "ignore_above": 1024, "type": "keyword" } - }, - "dynamic": "strict" + } }, "event": { "properties": { @@ -40,8 +42,7 @@ "end": { "type": "date" } - }, - "dynamic": "strict" + } }, "error": { "properties": { @@ -49,8 +50,7 @@ "norms": false, "type": "text" } - }, - "dynamic": "strict" + } }, "user": { "properties": { @@ -58,8 +58,7 @@ "ignore_above": 1024, "type": "keyword" } - }, - "dynamic": "strict" + } }, "kibana": { "properties": { @@ -86,11 +85,9 @@ "ignore_above": 1024 } }, - "type": "nested", - "dynamic": "strict" + "type": "nested" } - }, - "dynamic": "strict" + } } } } \ No newline at end of file diff --git a/x-pack/plugins/event_log/scripts/create_schemas.js b/x-pack/plugins/event_log/scripts/create_schemas.js index 6e9ab00d04d1f..b46f7f295ddc7 100755 --- a/x-pack/plugins/event_log/scripts/create_schemas.js +++ b/x-pack/plugins/event_log/scripts/create_schemas.js @@ -30,11 +30,13 @@ function main() { const exportedProperties = mappings.EcsEventLogProperties; const multiValuedProperties = new Set(mappings.EcsEventLogMultiValuedProperties); + augmentMappings(ecsMappings.mappings, multiValuedProperties); + const elMappings = getEventLogMappings(ecsMappings, exportedProperties); console.log(`generating files in ${PLUGIN_DIR}`); writeEventLogMappings(elMappings); - writeEventLogConfigSchema(elMappings, ecsVersion, multiValuedProperties); + writeEventLogConfigSchema(elMappings, ecsVersion); } // return a stripped down version of the ecs schema, with only exportedProperties @@ -57,7 +59,6 @@ function getEventLogMappings(ecsSchema, exportedProperties) { const elValue = lodash.get(result.mappings.properties, prop); elValue.type = ecsValue.type; - elValue.dynamic = 'strict'; } return result; @@ -86,7 +87,7 @@ function writeEventLogMappings(elSchema) { // fixObjectTypes(elSchema.mappings); const mappings = { - dynamic: 'strict', + dynamic: 'false', properties: elSchema.mappings.properties, }; @@ -94,11 +95,10 @@ function writeEventLogMappings(elSchema) { console.log('generated:', EVENT_LOG_MAPPINGS_FILE); } -function writeEventLogConfigSchema(elSchema, ecsVersion, multiValuedProperties) { +function writeEventLogConfigSchema(elSchema, ecsVersion) { const lineWriter = LineWriter.createLineWriter(); - const elSchemaMappings = augmentMappings(elSchema.mappings, multiValuedProperties); - generateSchemaLines(lineWriter, null, elSchemaMappings); + generateSchemaLines(lineWriter, null, elSchema.mappings); // last line will have an extraneous comma const schemaLines = lineWriter.getContent().replace(/,$/, ''); @@ -113,22 +113,21 @@ const StringTypes = new Set(['string', 'keyword', 'text', 'ip']); const NumberTypes = new Set(['long', 'integer', 'float']); function augmentMappings(mappings, multiValuedProperties) { - // clone the mappings, as we're adding some additional properties - mappings = JSON.parse(JSON.stringify(mappings)); - for (const prop of multiValuedProperties) { const fullProp = replaceDotWithProperties(prop); - lodash.set(mappings.properties, `${fullProp}.multiValued`, true); + const metaPropName = `${fullProp}.meta`; + const meta = lodash.get(mappings.properties, metaPropName) || {}; + meta.isArray = true; + lodash.set(mappings.properties, metaPropName, meta); } - - return mappings; } function generateSchemaLines(lineWriter, prop, mappings) { const propKey = legalPropertyName(prop); + if (mappings == null) return; if (StringTypes.has(mappings.type)) { - if (mappings.multiValued) { + if (mappings.meta && mappings.meta.isArray) { lineWriter.addLine(`${propKey}: ecsStringMulti(),`); } else { lineWriter.addLine(`${propKey}: ecsString(),`); @@ -169,6 +168,7 @@ function generateSchemaLines(lineWriter, prop, mappings) { // write the object properties lineWriter.indent(); for (const prop of Object.keys(mappings.properties)) { + if (prop === 'meta') continue; generateSchemaLines(lineWriter, prop, mappings.properties[prop]); } lineWriter.dedent(); diff --git a/x-pack/plugins/infra/common/log_analysis/log_analysis.ts b/x-pack/plugins/infra/common/log_analysis/log_analysis.ts index 9b2f1a55eb8c1..f0aa2067a24c2 100644 --- a/x-pack/plugins/infra/common/log_analysis/log_analysis.ts +++ b/x-pack/plugins/infra/common/log_analysis/log_analysis.ts @@ -14,19 +14,28 @@ export type JobStatus = | 'finished' | 'failed'; +export type SetupStatusRequiredReason = + | 'missing' // jobs are missing + | 'reconfiguration' // the configurations don't match the source configurations + | 'update'; // the definitions don't match the module definitions + export type SetupStatus = - | 'initializing' // acquiring job statuses to determine setup status - | 'unknown' // job status could not be acquired (failed request etc) - | 'required' // jobs are missing - | 'requiredForReconfiguration' // the configurations don't match the source configurations - | 'requiredForUpdate' // the definitions don't match the module definitions - | 'pending' // In the process of setting up the module for the first time or retrying, waiting for response - | 'succeeded' // setup succeeded, notifying user - | 'failed' // setup failed, notifying user - | 'hiddenAfterSuccess' // hide the setup screen and we show the results for the first time - | 'skipped' // setup hidden because the module is in a correct state already - | 'skippedButReconfigurable' // setup hidden even though the job configurations are outdated - | 'skippedButUpdatable'; // setup hidden even though the job definitions are outdated + | { type: 'initializing' } // acquiring job statuses to determine setup status + | { type: 'unknown' } // job status could not be acquired (failed request etc) + | { + type: 'required'; + reason: SetupStatusRequiredReason; + } // setup required + | { type: 'pending' } // In the process of setting up the module for the first time or retrying, waiting for response + | { type: 'succeeded' } // setup succeeded, notifying user + | { + type: 'failed'; + reasons: string[]; + } // setup failed, notifying user + | { + type: 'skipped'; + newlyCreated?: boolean; + }; // setup is hidden /** * Maps a job status to the possibility that results have already been produced @@ -43,9 +52,7 @@ export const isHealthyJobStatus = (jobStatus: JobStatus) => * produced before this state was reached. */ export const isSetupStatusWithResults = (setupStatus: SetupStatus) => - ['skipped', 'hiddenAfterSuccess', 'skippedButReconfigurable', 'skippedButUpdatable'].includes( - setupStatus - ); + setupStatus.type === 'skipped'; const KIBANA_SAMPLE_DATA_INDICES = ['kibana_sample_data_logs*']; diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_job_status/log_analysis_job_problem_indicator.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_job_status/log_analysis_job_problem_indicator.tsx index 8a16d819e12c2..e7e89bb365e4f 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_job_status/log_analysis_job_problem_indicator.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_job_status/log_analysis_job_problem_indicator.tsx @@ -6,33 +6,36 @@ import React from 'react'; -import { JobStatus, SetupStatus } from '../../../../common/log_analysis'; import { JobConfigurationOutdatedCallout } from './job_configuration_outdated_callout'; import { JobDefinitionOutdatedCallout } from './job_definition_outdated_callout'; import { JobStoppedCallout } from './job_stopped_callout'; +import { FirstUseCallout } from '../log_analysis_results'; export const LogAnalysisJobProblemIndicator: React.FC<{ - jobStatus: JobStatus; - setupStatus: SetupStatus; + hasOutdatedJobConfigurations: boolean; + hasOutdatedJobDefinitions: boolean; + hasStoppedJobs: boolean; + isFirstUse: boolean; onRecreateMlJobForReconfiguration: () => void; onRecreateMlJobForUpdate: () => void; -}> = ({ jobStatus, setupStatus, onRecreateMlJobForReconfiguration, onRecreateMlJobForUpdate }) => { - if (isStopped(jobStatus)) { - return ; - } else if (isUpdatable(setupStatus)) { - return ; - } else if (isReconfigurable(setupStatus)) { - return ; - } - - return null; // no problem to indicate +}> = ({ + hasOutdatedJobConfigurations, + hasOutdatedJobDefinitions, + hasStoppedJobs, + isFirstUse, + onRecreateMlJobForReconfiguration, + onRecreateMlJobForUpdate, +}) => { + return ( + <> + {hasOutdatedJobDefinitions ? ( + + ) : null} + {hasOutdatedJobConfigurations ? ( + + ) : null} + {hasStoppedJobs ? : null} + {isFirstUse ? : null} + + ); }; - -const isStopped = (jobStatus: JobStatus) => jobStatus === 'stopped'; - -const isUpdatable = (setupStatus: SetupStatus) => setupStatus === 'skippedButUpdatable'; - -const isReconfigurable = (setupStatus: SetupStatus) => setupStatus === 'skippedButReconfigurable'; - -export const jobHasProblem = (jobStatus: JobStatus, setupStatus: SetupStatus) => - isStopped(jobStatus) || isUpdatable(setupStatus) || isReconfigurable(setupStatus); diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/initial_configuration_step.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/initial_configuration_step.tsx index de20dd12c17bd..4ec895dfed4bc 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/initial_configuration_step.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/initial_configuration_step.tsx @@ -45,7 +45,7 @@ export const InitialConfigurationStep: React.FunctionComponent { - const disabled = useMemo(() => !editableFormStatus.includes(setupStatus), [setupStatus]); + const disabled = useMemo(() => !editableFormStatus.includes(setupStatus.type), [setupStatus]); return ( <> @@ -72,12 +72,7 @@ export const InitialConfigurationStep: React.FunctionComponent, status: - props.setupStatus === 'pending' + props.setupStatus.type === 'pending' ? 'incomplete' - : props.setupStatus === 'failed' + : props.setupStatus.type === 'failed' ? 'danger' - : props.setupStatus === 'succeeded' + : props.setupStatus.type === 'succeeded' ? 'complete' : undefined, }); @@ -55,7 +55,7 @@ export const ProcessStep: React.FunctionComponent = ({ }) => { return ( - {setupStatus === 'pending' ? ( + {setupStatus.type === 'pending' ? ( @@ -67,7 +67,7 @@ export const ProcessStep: React.FunctionComponent = ({ /> - ) : setupStatus === 'failed' ? ( + ) : setupStatus.type === 'failed' ? ( <> = ({ /> - ) : setupStatus === 'succeeded' ? ( + ) : setupStatus.type === 'succeeded' ? ( <> = ({ /> - ) : setupStatus === 'requiredForUpdate' || setupStatus === 'requiredForReconfiguration' ? ( + ) : setupStatus.type === 'required' && + (setupStatus.reason === 'update' || setupStatus.reason === 'reconfiguration') ? ( ) : ( diff --git a/x-pack/plugins/infra/public/containers/logs/log_analysis/api/ml_get_jobs_summary_api.ts b/x-pack/plugins/infra/public/containers/logs/log_analysis/api/ml_get_jobs_summary_api.ts index 49112dd8ec8d8..c3c119f51ff7d 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_analysis/api/ml_get_jobs_summary_api.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_analysis/api/ml_get_jobs_summary_api.ts @@ -54,6 +54,22 @@ const jobStateRT = rt.keyof({ opening: null, }); +const jobCategorizationStatusRT = rt.keyof({ + ok: null, + warn: null, +}); + +const jobModelSizeStatsRT = rt.type({ + categorization_status: jobCategorizationStatusRT, + categorized_doc_count: rt.number, + dead_category_count: rt.number, + frequent_category_count: rt.number, + rare_category_count: rt.number, + total_category_count: rt.number, +}); + +export type JobModelSizeStats = rt.TypeOf; + export const jobSummaryRT = rt.intersection([ rt.type({ id: rt.string, @@ -65,6 +81,7 @@ export const jobSummaryRT = rt.intersection([ fullJob: rt.partial({ custom_settings: jobCustomSettingsRT, finished_time: rt.number, + model_size_stats: jobModelSizeStatsRT, }), }), ]); diff --git a/x-pack/plugins/infra/public/containers/logs/log_analysis/index.ts b/x-pack/plugins/infra/public/containers/logs/log_analysis/index.ts index 185f6936662bd..9828ad72ec909 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_analysis/index.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_analysis/index.ts @@ -7,6 +7,10 @@ export * from './log_analysis_capabilities'; export * from './log_analysis_cleanup'; export * from './log_analysis_module'; +export * from './log_analysis_module_configuration'; +export * from './log_analysis_module_definition'; export * from './log_analysis_module_status'; export * from './log_analysis_module_types'; export * from './log_analysis_setup_state'; + +export { JobModelSizeStats, JobSummary } from './api/ml_get_jobs_summary_api'; diff --git a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx index efe9629e84e42..99c5a3df7c9b1 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx +++ b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useCallback, useEffect, useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import { useTrackedPromise } from '../../../utils/use_tracked_promise'; import { useModuleStatus } from './log_analysis_module_status'; @@ -17,36 +17,10 @@ export const useLogAnalysisModule = ({ sourceConfiguration: ModuleSourceConfiguration; moduleDescriptor: ModuleDescriptor; }) => { - const { spaceId, sourceId, timestampField, indices } = sourceConfiguration; - const [moduleStatus, dispatchModuleStatus] = useModuleStatus(moduleDescriptor.jobTypes, { - bucketSpan: moduleDescriptor.bucketSpan, - indexPattern: indices.join(','), - timestampField, - }); + const { spaceId, sourceId, timestampField } = sourceConfiguration; + const [moduleStatus, dispatchModuleStatus] = useModuleStatus(moduleDescriptor.jobTypes); - const [fetchModuleDefinitionRequest, fetchModuleDefinition] = useTrackedPromise( - { - cancelPreviousOn: 'resolution', - createPromise: async () => { - dispatchModuleStatus({ type: 'fetchingModuleDefinition' }); - return await moduleDescriptor.getModuleDefinition(); - }, - onResolve: response => { - dispatchModuleStatus({ - type: 'fetchedModuleDefinition', - spaceId, - sourceId, - moduleDefinition: response, - }); - }, - onReject: () => { - dispatchModuleStatus({ type: 'failedFetchingModuleDefinition' }); - }, - }, - [moduleDescriptor.getModuleDefinition, spaceId, sourceId] - ); - - const [fetchJobStatusRequest, fetchJobStatus] = useTrackedPromise( + const [, fetchJobStatus] = useTrackedPromise( { cancelPreviousOn: 'resolution', createPromise: async () => { @@ -68,12 +42,6 @@ export const useLogAnalysisModule = ({ [spaceId, sourceId] ); - const isLoadingModuleStatus = useMemo( - () => - fetchJobStatusRequest.state === 'pending' || fetchModuleDefinitionRequest.state === 'pending', - [fetchJobStatusRequest.state, fetchModuleDefinitionRequest.state] - ); - const [, setUpModule] = useTrackedPromise( { cancelPreviousOn: 'resolution', @@ -83,15 +51,24 @@ export const useLogAnalysisModule = ({ end: number | undefined ) => { dispatchModuleStatus({ type: 'startedSetup' }); - return await moduleDescriptor.setUpModule(start, end, { + const setupResult = await moduleDescriptor.setUpModule(start, end, { indices: selectedIndices, sourceId, spaceId, timestampField, }); + const jobSummaries = await moduleDescriptor.getJobSummary(spaceId, sourceId); + return { setupResult, jobSummaries }; }, - onResolve: ({ datafeeds, jobs }) => { - dispatchModuleStatus({ type: 'finishedSetup', datafeeds, jobs, spaceId, sourceId }); + onResolve: ({ setupResult: { datafeeds, jobs }, jobSummaries }) => { + dispatchModuleStatus({ + type: 'finishedSetup', + datafeedSetupResults: datafeeds, + jobSetupResults: jobs, + jobSummaries, + spaceId, + sourceId, + }); }, onReject: () => { dispatchModuleStatus({ type: 'failedSetup' }); @@ -146,36 +123,14 @@ export const useLogAnalysisModule = ({ sourceId, ]); - useEffect(() => { - dispatchModuleStatus({ - type: 'updatedSourceConfiguration', - spaceId, - sourceId, - sourceConfiguration: { - timestampField, - indexPattern: indices.join(','), - bucketSpan: moduleDescriptor.bucketSpan, - }, - }); - }, [ - dispatchModuleStatus, - indices, - moduleDescriptor.bucketSpan, - sourceConfiguration, - sourceId, - spaceId, - timestampField, - ]); - return { cleanUpAndSetUpModule, cleanUpModule, fetchJobStatus, - fetchModuleDefinition, isCleaningUp, - isLoadingModuleStatus, jobIds, jobStatus: moduleStatus.jobStatus, + jobSummaries: moduleStatus.jobSummaries, lastSetupErrorMessages: moduleStatus.lastSetupErrorMessages, moduleDescriptor, setUpModule, diff --git a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_configuration.ts b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_configuration.ts new file mode 100644 index 0000000000000..ba2185ff83784 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_configuration.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useMemo } from 'react'; +import { JobSummary } from './api/ml_get_jobs_summary_api'; +import { ModuleDescriptor, ModuleSourceConfiguration } from './log_analysis_module_types'; + +export const useLogAnalysisModuleConfiguration = ({ + moduleDescriptor, + sourceConfiguration, +}: { + moduleDescriptor: ModuleDescriptor; + sourceConfiguration: ModuleSourceConfiguration; +}) => { + const getIsJobConfigurationOutdated = useMemo( + () => isJobConfigurationOutdated(moduleDescriptor, sourceConfiguration), + [sourceConfiguration, moduleDescriptor] + ); + + return { + getIsJobConfigurationOutdated, + }; +}; + +export const isJobConfigurationOutdated = ( + { bucketSpan }: ModuleDescriptor, + currentSourceConfiguration: ModuleSourceConfiguration +) => (jobSummary: JobSummary): boolean => { + if (!jobSummary.fullJob || !jobSummary.fullJob.custom_settings) { + return false; + } + + const jobConfiguration = jobSummary.fullJob.custom_settings.logs_source_config; + + return !( + jobConfiguration && + jobConfiguration.bucketSpan === bucketSpan && + jobConfiguration.indexPattern && + isSubset( + new Set(jobConfiguration.indexPattern.split(',')), + new Set(currentSourceConfiguration.indices) + ) && + jobConfiguration.timestampField === currentSourceConfiguration.timestampField + ); +}; + +const isSubset = (subset: Set, superset: Set) => { + return Array.from(subset).every(subsetElement => superset.has(subsetElement)); +}; diff --git a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_definition.tsx b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_definition.tsx new file mode 100644 index 0000000000000..ea0cd32c0b93e --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_definition.tsx @@ -0,0 +1,76 @@ +/* + * 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 { useCallback, useMemo, useState } from 'react'; +import { getJobId } from '../../../../common/log_analysis'; +import { useTrackedPromise } from '../../../utils/use_tracked_promise'; +import { JobSummary } from './api/ml_get_jobs_summary_api'; +import { GetMlModuleResponsePayload, JobDefinition } from './api/ml_get_module'; +import { ModuleDescriptor, ModuleSourceConfiguration } from './log_analysis_module_types'; + +export const useLogAnalysisModuleDefinition = ({ + sourceConfiguration: { spaceId, sourceId }, + moduleDescriptor, +}: { + sourceConfiguration: ModuleSourceConfiguration; + moduleDescriptor: ModuleDescriptor; +}) => { + const [moduleDefinition, setModuleDefinition] = useState< + GetMlModuleResponsePayload | undefined + >(); + + const jobDefinitionByJobId = useMemo( + () => + moduleDefinition + ? moduleDefinition.jobs.reduce>( + (accumulatedJobDefinitions, jobDefinition) => ({ + ...accumulatedJobDefinitions, + [getJobId(spaceId, sourceId, jobDefinition.id)]: jobDefinition, + }), + {} + ) + : {}, + [moduleDefinition, sourceId, spaceId] + ); + + const [fetchModuleDefinitionRequest, fetchModuleDefinition] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: async () => { + return await moduleDescriptor.getModuleDefinition(); + }, + onResolve: response => { + setModuleDefinition(response); + }, + onReject: () => { + setModuleDefinition(undefined); + }, + }, + [moduleDescriptor.getModuleDefinition, spaceId, sourceId] + ); + + const getIsJobDefinitionOutdated = useCallback( + (jobSummary: JobSummary): boolean => { + const jobDefinition: JobDefinition | undefined = jobDefinitionByJobId[jobSummary.id]; + + if (jobDefinition == null) { + return false; + } + + const currentRevision = jobDefinition?.config.custom_settings.job_revision; + return (jobSummary.fullJob?.custom_settings?.job_revision ?? 0) < (currentRevision ?? 0); + }, + [jobDefinitionByJobId] + ); + + return { + fetchModuleDefinition, + fetchModuleDefinitionRequestState: fetchModuleDefinitionRequest.state, + getIsJobDefinitionOutdated, + jobDefinitionByJobId, + moduleDefinition, + }; +}; diff --git a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_status.tsx b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_status.tsx index cf248f419f6f2..b5530f9ebf72e 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_status.tsx +++ b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_status.tsx @@ -7,25 +7,21 @@ import { useReducer } from 'react'; import { - JobSourceConfiguration, JobStatus, - SetupStatus, getDatafeedId, getJobId, isJobStatusWithResults, + SetupStatus, } from '../../../../common/log_analysis'; import { FetchJobStatusResponsePayload, JobSummary } from './api/ml_get_jobs_summary_api'; -import { GetMlModuleResponsePayload, JobDefinition } from './api/ml_get_module'; import { SetupMlModuleResponsePayload } from './api/ml_setup_module_api'; import { MandatoryProperty } from '../../../../common/utility_types'; interface StatusReducerState { - jobDefinitions: JobDefinition[]; jobStatus: Record; jobSummaries: JobSummary[]; lastSetupErrorMessages: string[]; setupStatus: SetupStatus; - sourceConfiguration: JobSourceConfiguration; } type StatusReducerAction = @@ -34,8 +30,9 @@ type StatusReducerAction = type: 'finishedSetup'; sourceId: string; spaceId: string; - jobs: SetupMlModuleResponsePayload['jobs']; - datafeeds: SetupMlModuleResponsePayload['datafeeds']; + jobSetupResults: SetupMlModuleResponsePayload['jobs']; + jobSummaries: FetchJobStatusResponsePayload; + datafeedSetupResults: SetupMlModuleResponsePayload['datafeeds']; } | { type: 'failedSetup' } | { type: 'fetchingJobStatuses' } @@ -46,32 +43,15 @@ type StatusReducerAction = payload: FetchJobStatusResponsePayload; } | { type: 'failedFetchingJobStatuses' } - | { type: 'fetchingModuleDefinition' } - | { - type: 'fetchedModuleDefinition'; - spaceId: string; - sourceId: string; - moduleDefinition: GetMlModuleResponsePayload; - } - | { type: 'failedFetchingModuleDefinition' } - | { - type: 'updatedSourceConfiguration'; - spaceId: string; - sourceId: string; - sourceConfiguration: JobSourceConfiguration; - } | { type: 'requestedJobConfigurationUpdate' } | { type: 'requestedJobDefinitionUpdate' } | { type: 'viewedResults' }; const createInitialState = ({ jobTypes, - sourceConfiguration, }: { jobTypes: JobType[]; - sourceConfiguration: JobSourceConfiguration; }): StatusReducerState => ({ - jobDefinitions: [], jobStatus: jobTypes.reduce( (accumulatedJobStatus, jobType) => ({ ...accumulatedJobStatus, @@ -81,8 +61,7 @@ const createInitialState = ({ ), jobSummaries: [], lastSetupErrorMessages: [], - setupStatus: 'initializing', - sourceConfiguration, + setupStatus: { type: 'initializing' }, }); const createStatusReducer = (jobTypes: JobType[]) => ( @@ -100,39 +79,44 @@ const createStatusReducer = (jobTypes: JobType[]) => ( }), {} as Record ), - setupStatus: 'pending', + setupStatus: { type: 'pending' }, }; } case 'finishedSetup': { - const { jobs, datafeeds, spaceId, sourceId } = action; + const { datafeedSetupResults, jobSetupResults, jobSummaries, spaceId, sourceId } = action; const nextJobStatus = jobTypes.reduce( (accumulatedJobStatus, jobType) => ({ ...accumulatedJobStatus, [jobType]: - hasSuccessfullyCreatedJob(getJobId(spaceId, sourceId, jobType))(jobs) && - hasSuccessfullyStartedDatafeed(getDatafeedId(spaceId, sourceId, jobType))(datafeeds) + hasSuccessfullyCreatedJob(getJobId(spaceId, sourceId, jobType))(jobSetupResults) && + hasSuccessfullyStartedDatafeed(getDatafeedId(spaceId, sourceId, jobType))( + datafeedSetupResults + ) ? 'started' : 'failed', }), {} as Record ); - const nextSetupStatus = Object.values(nextJobStatus).every( + const nextSetupStatus: SetupStatus = Object.values(nextJobStatus).every( jobState => jobState === 'started' ) - ? 'succeeded' - : 'failed'; - const nextErrorMessages = [ - ...Object.values(datafeeds) - .filter(hasError) - .map(datafeed => datafeed.error.msg), - ...Object.values(jobs) - .filter(hasError) - .map(job => job.error.msg), - ]; + ? { type: 'succeeded' } + : { + type: 'failed', + reasons: [ + ...Object.values(datafeedSetupResults) + .filter(hasError) + .map(datafeed => datafeed.error.msg), + ...Object.values(jobSetupResults) + .filter(hasError) + .map(job => job.error.msg), + ], + }; + return { ...state, jobStatus: nextJobStatus, - lastSetupErrorMessages: nextErrorMessages, + jobSummaries, setupStatus: nextSetupStatus, }; } @@ -146,19 +130,19 @@ const createStatusReducer = (jobTypes: JobType[]) => ( }), {} as Record ), - setupStatus: 'failed', + setupStatus: { type: 'failed', reasons: ['unknown'] }, }; } - case 'fetchingModuleDefinition': case 'fetchingJobStatuses': { return { ...state, - setupStatus: state.setupStatus === 'unknown' ? 'initializing' : state.setupStatus, + setupStatus: + state.setupStatus.type === 'unknown' ? { type: 'initializing' } : state.setupStatus, }; } case 'fetchedJobStatuses': { const { payload: jobSummaries, spaceId, sourceId } = action; - const { jobDefinitions, setupStatus, sourceConfiguration } = state; + const { setupStatus } = state; const nextJobStatus = jobTypes.reduce( (accumulatedJobStatus, jobType) => ({ @@ -167,14 +151,7 @@ const createStatusReducer = (jobTypes: JobType[]) => ( }), {} as Record ); - const nextSetupStatus = getSetupStatus( - spaceId, - sourceId, - sourceConfiguration, - nextJobStatus, - jobDefinitions, - jobSummaries - )(setupStatus); + const nextSetupStatus = getSetupStatus(nextJobStatus)(setupStatus); return { ...state, @@ -186,7 +163,7 @@ const createStatusReducer = (jobTypes: JobType[]) => ( case 'failedFetchingJobStatuses': { return { ...state, - setupStatus: 'unknown', + setupStatus: { type: 'unknown' }, jobStatus: jobTypes.reduce( (accumulatedJobStatus, jobType) => ({ ...accumulatedJobStatus, @@ -196,60 +173,22 @@ const createStatusReducer = (jobTypes: JobType[]) => ( ), }; } - case 'fetchedModuleDefinition': { - const { spaceId, sourceId, moduleDefinition } = action; - const { jobStatus, jobSummaries, setupStatus, sourceConfiguration } = state; - - const nextSetupStatus = getSetupStatus( - spaceId, - sourceId, - sourceConfiguration, - jobStatus, - moduleDefinition.jobs, - jobSummaries - )(setupStatus); - - return { - ...state, - jobDefinitions: moduleDefinition.jobs, - setupStatus: nextSetupStatus, - }; - } - case 'updatedSourceConfiguration': { - const { spaceId, sourceId, sourceConfiguration } = action; - const { jobDefinitions, jobStatus, jobSummaries, setupStatus } = state; - - const nextSetupStatus = getSetupStatus( - spaceId, - sourceId, - sourceConfiguration, - jobStatus, - jobDefinitions, - jobSummaries - )(setupStatus); - - return { - ...state, - setupStatus: nextSetupStatus, - sourceConfiguration, - }; - } case 'requestedJobConfigurationUpdate': { return { ...state, - setupStatus: 'requiredForReconfiguration', + setupStatus: { type: 'required', reason: 'reconfiguration' }, }; } case 'requestedJobDefinitionUpdate': { return { ...state, - setupStatus: 'requiredForUpdate', + setupStatus: { type: 'required', reason: 'update' }, }; } case 'viewedResults': { return { ...state, - setupStatus: 'hiddenAfterSuccess', + setupStatus: { type: 'skipped', newlyCreated: true }, }; } default: { @@ -307,100 +246,28 @@ const getJobStatus = (jobId: string) => (jobSummaries: FetchJobStatusResponsePay } )[0] || 'missing'; -const getSetupStatus = ( - spaceId: string, - sourceId: string, - sourceConfiguration: JobSourceConfiguration, - everyJobStatus: Record, - jobDefinitions: JobDefinition[], - jobSummaries: JobSummary[] -) => (previousSetupStatus: SetupStatus) => - Object.entries(everyJobStatus).reduce( - (setupStatus, [jobType, jobStatus]) => { - const jobId = getJobId(spaceId, sourceId, jobType); - const jobDefinition = jobDefinitions.find(({ id }) => id === jobType); - - if (jobStatus === 'missing') { - return 'required'; - } else if ( - setupStatus === 'required' || - setupStatus === 'requiredForUpdate' || - setupStatus === 'requiredForReconfiguration' - ) { - return setupStatus; - } else if ( - jobDefinition && - !isJobRevisionCurrent( - jobId, - jobDefinition.config.custom_settings.job_revision || 0 - )(jobSummaries) - ) { - return 'skippedButUpdatable'; - } else if (!isJobConfigurationConsistent(jobId, sourceConfiguration)(jobSummaries)) { - return 'skippedButReconfigurable'; - } else if (setupStatus === 'hiddenAfterSuccess') { - return setupStatus; - } else if (setupStatus === 'skipped' || isJobStatusWithResults(jobStatus)) { - return 'skipped'; - } - +const getSetupStatus = (everyJobStatus: Record) => ( + previousSetupStatus: SetupStatus +): SetupStatus => + Object.entries(everyJobStatus).reduce((setupStatus, [, jobStatus]) => { + if (jobStatus === 'missing') { + return { type: 'required', reason: 'missing' }; + } else if (setupStatus.type === 'required') { return setupStatus; - }, - previousSetupStatus - ); - -const isJobRevisionCurrent = (jobId: string, currentRevision: number) => ( - jobSummaries: FetchJobStatusResponsePayload -): boolean => - jobSummaries - .filter(jobSummary => jobSummary.id === jobId) - .every( - jobSummary => (jobSummary?.fullJob?.custom_settings?.job_revision ?? 0) >= currentRevision - ); - -const isJobConfigurationConsistent = ( - jobId: string, - sourceConfiguration: { - bucketSpan: number; - indexPattern: string; - timestampField: string; - } -) => (jobSummaries: FetchJobStatusResponsePayload): boolean => - jobSummaries - .filter(jobSummary => jobSummary.id === jobId) - .every(jobSummary => { - if (!jobSummary.fullJob || !jobSummary.fullJob.custom_settings) { - return false; - } - - const jobConfiguration = jobSummary.fullJob.custom_settings.logs_source_config; - - return ( - jobConfiguration && - jobConfiguration.bucketSpan === sourceConfiguration.bucketSpan && - jobConfiguration.indexPattern && - isIndexPatternSubset(jobConfiguration.indexPattern, sourceConfiguration.indexPattern) && - jobConfiguration.timestampField === sourceConfiguration.timestampField - ); - }); - -const isIndexPatternSubset = (indexPatternSubset: string, indexPatternSuperset: string) => { - const subsetSubPatterns = indexPatternSubset.split(','); - const supersetSubPatterns = new Set(indexPatternSuperset.split(',')); + } else if (setupStatus.type === 'skipped' || isJobStatusWithResults(jobStatus)) { + return { + type: 'skipped', + // preserve newlyCreated status + newlyCreated: setupStatus.type === 'skipped' && setupStatus.newlyCreated, + }; + } - return subsetSubPatterns.every(subPattern => supersetSubPatterns.has(subPattern)); -}; + return setupStatus; + }, previousSetupStatus); const hasError = (value: Value): value is MandatoryProperty => value.error != null; -export const useModuleStatus = ( - jobTypes: JobType[], - sourceConfiguration: JobSourceConfiguration -) => { - return useReducer( - createStatusReducer(jobTypes), - { jobTypes, sourceConfiguration }, - createInitialState - ); +export const useModuleStatus = (jobTypes: JobType[]) => { + return useReducer(createStatusReducer(jobTypes), { jobTypes }, createInitialState); }; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx index d1c10820f7c9c..ed1aa9e72ebae 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx @@ -37,18 +37,13 @@ export const LogEntryCategoriesPageContent = () => { hasLogAnalysisSetupCapabilities, } = useLogAnalysisCapabilitiesContext(); - const { - fetchJobStatus, - fetchModuleDefinition, - setupStatus, - } = useLogEntryCategoriesModuleContext(); + const { fetchJobStatus, setupStatus } = useLogEntryCategoriesModuleContext(); useEffect(() => { if (hasLogAnalysisReadCapabilities) { - fetchModuleDefinition(); fetchJobStatus(); } - }, [fetchJobStatus, fetchModuleDefinition, hasLogAnalysisReadCapabilities]); + }, [fetchJobStatus, hasLogAnalysisReadCapabilities]); if (isLoadingSource || isUninitialized) { return ; @@ -58,7 +53,7 @@ export const LogEntryCategoriesPageContent = () => { return ; } else if (!hasLogAnalysisReadCapabilities) { return ; - } else if (setupStatus === 'initializing') { + } else if (setupStatus.type === 'initializing') { return ( { })} /> ); - } else if (setupStatus === 'unknown') { + } else if (setupStatus.type === 'unknown') { return ; } else if (isSetupStatusWithResults(setupStatus)) { return ; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx index b783aa9c79007..e304a8835e99c 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx @@ -9,17 +9,11 @@ import { EuiFlexGroup, EuiFlexItem, EuiPage, EuiPanel, EuiSuperDatePicker } from import { i18n } from '@kbn/i18n'; import moment from 'moment'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; - import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled, useTrackPageview } from '../../../../../observability/public'; import { TimeRange } from '../../../../common/http_api/shared/time_range'; -import { - LogAnalysisJobProblemIndicator, - jobHasProblem, -} from '../../../components/logging/log_analysis_job_status'; -import { FirstUseCallout } from '../../../components/logging/log_analysis_results'; import { useInterval } from '../../../hooks/use_interval'; -import { useTrackPageview } from '../../../../../observability/public'; +import { CategoryJobNoticesSection } from './sections/notices/notices_section'; import { TopCategoriesSection } from './sections/top_categories'; import { useLogEntryCategoriesModuleContext } from './use_log_entry_categories_module'; import { useLogEntryCategoriesResults } from './use_log_entry_categories_results'; @@ -36,11 +30,15 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent = () => { const { fetchJobStatus, - jobStatus, + fetchModuleDefinition, setupStatus, viewSetupForReconfiguration, viewSetupForUpdate, + hasOutdatedJobConfigurations, + hasOutdatedJobDefinitions, + hasStoppedJobs, jobIds, + categoryQualityWarnings, sourceConfiguration: { sourceId }, } = useLogEntryCategoriesModuleContext(); @@ -124,12 +122,15 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent = () => { [setAutoRefresh] ); - const isFirstUse = useMemo(() => setupStatus === 'hiddenAfterSuccess', [setupStatus]); - const hasResults = useMemo(() => topLogEntryCategories.length > 0, [ topLogEntryCategories.length, ]); + const isFirstUse = useMemo( + () => setupStatus.type === 'skipped' && !!setupStatus.newlyCreated && !hasResults, + [hasResults, setupStatus] + ); + useEffect(() => { getTopLogEntryCategories(); }, [getTopLogEntryCategories, categoryQueryDatasets, categoryQueryTimeRange.lastChangedTime]); @@ -138,6 +139,10 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent = () => { getLogEntryCategoryDatasets(); }, [getLogEntryCategoryDatasets, categoryQueryTimeRange.lastChangedTime]); + useEffect(() => { + fetchModuleDefinition(); + }, [fetchModuleDefinition]); + useInterval(() => { fetchJobStatus(); }, JOB_STATUS_POLLING_INTERVAL); @@ -172,21 +177,17 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent = () => { - {jobHasProblem(jobStatus['log-entry-categories-count'], setupStatus) ? ( - - - - ) : null} - {isFirstUse && !hasResults ? ( - - - - ) : null} + + + void; + onRecreateMlJobForUpdate: () => void; + qualityWarnings: QualityWarning[]; +}> = ({ + hasOutdatedJobConfigurations, + hasOutdatedJobDefinitions, + hasStoppedJobs, + isFirstUse, + onRecreateMlJobForReconfiguration, + onRecreateMlJobForUpdate, + qualityWarnings, +}) => ( + <> + + + +); diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/notices/quality_warning_notices.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/notices/quality_warning_notices.tsx new file mode 100644 index 0000000000000..73b6b88db873a --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/notices/quality_warning_notices.tsx @@ -0,0 +1,98 @@ +/* + * 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 { EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; +import { CategoryQualityWarningReason, QualityWarning } from './quality_warnings'; + +export const CategoryQualityWarnings: React.FC<{ qualityWarnings: QualityWarning[] }> = ({ + qualityWarnings, +}) => ( + <> + {qualityWarnings.map((qualityWarning, qualityWarningIndex) => ( + +

+ +

+
    + {qualityWarning.reasons.map((reason, reasonIndex) => ( +
  • + +
  • + ))} +
+ + ))} + +); + +const categoryQualityWarningCalloutTitle = i18n.translate( + 'xpack.infra.logs.logEntryCategories.categoryQUalityWarningCalloutTitle', + { + defaultMessage: 'Quality warning', + } +); + +const CategoryQualityWarningReasonDescription: React.FC<{ + reason: CategoryQualityWarningReason; +}> = ({ reason }) => { + switch (reason.type) { + case 'singleCategory': + return ( + + ); + case 'manyRareCategories': + return ( + + ); + case 'manyCategories': + return ( + + ); + case 'noFrequentCategories': + return ( + + ); + case 'manyDeadCategories': + return ( + + ); + } +}; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/notices/quality_warnings.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/notices/quality_warnings.tsx new file mode 100644 index 0000000000000..e0d3aa105e004 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/notices/quality_warnings.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +interface ManyCategoriesWarningReason { + type: 'manyCategories'; + categoriesDocumentRatio: number; +} + +interface ManyDeadCategoriesWarningReason { + type: 'manyDeadCategories'; + deadCategoriesRatio: number; +} + +interface ManyRareCategoriesWarningReason { + type: 'manyRareCategories'; + rareCategoriesRatio: number; +} + +interface NoFrequentCategoriesWarningReason { + type: 'noFrequentCategories'; +} + +interface SingleCategoryWarningReason { + type: 'singleCategory'; +} + +export type CategoryQualityWarningReason = + | ManyCategoriesWarningReason + | ManyDeadCategoriesWarningReason + | ManyRareCategoriesWarningReason + | NoFrequentCategoriesWarningReason + | SingleCategoryWarningReason; + +export type CategoryQualityWarningReasonType = CategoryQualityWarningReason['type']; + +export interface CategoryQualityWarning { + type: 'categoryQualityWarning'; + jobId: string; + reasons: CategoryQualityWarningReason[]; +} + +export type QualityWarning = CategoryQualityWarning; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/use_log_entry_categories_module.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/use_log_entry_categories_module.tsx index 918c252f6350c..2917719af283b 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/use_log_entry_categories_module.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/use_log_entry_categories_module.tsx @@ -6,12 +6,14 @@ import createContainer from 'constate'; import { useMemo } from 'react'; - import { - useLogAnalysisModule, ModuleSourceConfiguration, + useLogAnalysisModule, + useLogAnalysisModuleConfiguration, + useLogAnalysisModuleDefinition, } from '../../../containers/logs/log_analysis'; import { logEntryCategoriesModule } from './module_descriptor'; +import { useLogEntryCategoriesQuality } from './use_log_entry_categories_quality'; export const useLogEntryCategoriesModule = ({ indexPattern, @@ -34,10 +36,51 @@ export const useLogEntryCategoriesModule = ({ [indexPattern, sourceId, spaceId, timestampField] ); - return useLogAnalysisModule({ + const logAnalysisModule = useLogAnalysisModule({ + moduleDescriptor: logEntryCategoriesModule, + sourceConfiguration, + }); + + const { getIsJobConfigurationOutdated } = useLogAnalysisModuleConfiguration({ + sourceConfiguration, moduleDescriptor: logEntryCategoriesModule, + }); + + const { fetchModuleDefinition, getIsJobDefinitionOutdated } = useLogAnalysisModuleDefinition({ sourceConfiguration, + moduleDescriptor: logEntryCategoriesModule, + }); + + const { categoryQualityWarnings } = useLogEntryCategoriesQuality({ + jobSummaries: logAnalysisModule.jobSummaries, }); + + const hasOutdatedJobConfigurations = useMemo( + () => logAnalysisModule.jobSummaries.some(getIsJobConfigurationOutdated), + [getIsJobConfigurationOutdated, logAnalysisModule.jobSummaries] + ); + + const hasOutdatedJobDefinitions = useMemo( + () => logAnalysisModule.jobSummaries.some(getIsJobDefinitionOutdated), + [getIsJobDefinitionOutdated, logAnalysisModule.jobSummaries] + ); + + const hasStoppedJobs = useMemo( + () => + Object.values(logAnalysisModule.jobStatus).some( + currentJobStatus => currentJobStatus === 'stopped' + ), + [logAnalysisModule.jobStatus] + ); + + return { + ...logAnalysisModule, + categoryQualityWarnings, + fetchModuleDefinition, + hasOutdatedJobConfigurations, + hasOutdatedJobDefinitions, + hasStoppedJobs, + }; }; export const [ diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/use_log_entry_categories_quality.ts b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/use_log_entry_categories_quality.ts new file mode 100644 index 0000000000000..031479e66386f --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/use_log_entry_categories_quality.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useMemo } from 'react'; + +import { JobModelSizeStats, JobSummary } from '../../../containers/logs/log_analysis'; +import { QualityWarning, CategoryQualityWarningReason } from './sections/notices/quality_warnings'; + +export const useLogEntryCategoriesQuality = ({ jobSummaries }: { jobSummaries: JobSummary[] }) => { + const categoryQualityWarnings: QualityWarning[] = useMemo( + () => + jobSummaries + .filter( + jobSummary => jobSummary.fullJob?.model_size_stats?.categorization_status === 'warn' + ) + .map(jobSummary => ({ + type: 'categoryQualityWarning', + jobId: jobSummary.id, + reasons: jobSummary.fullJob?.model_size_stats + ? getCategoryQualityWarningReasons(jobSummary.fullJob.model_size_stats) + : [], + })), + [jobSummaries] + ); + + return { + categoryQualityWarnings, + }; +}; + +const getCategoryQualityWarningReasons = ({ + categorized_doc_count: categorizedDocCount, + dead_category_count: deadCategoryCount, + frequent_category_count: frequentCategoryCount, + rare_category_count: rareCategoryCount, + total_category_count: totalCategoryCount, +}: JobModelSizeStats): CategoryQualityWarningReason[] => { + const rareCategoriesRatio = rareCategoryCount / totalCategoryCount; + const categoriesDocumentRatio = totalCategoryCount / categorizedDocCount; + const deadCategoriesRatio = deadCategoryCount / totalCategoryCount; + + return [ + ...(totalCategoryCount === 1 + ? [ + { + type: 'singleCategory' as const, + }, + ] + : []), + ...(rareCategoriesRatio >= 0.9 + ? [ + { + type: 'manyRareCategories' as const, + rareCategoriesRatio, + }, + ] + : []), + ...(categorizedDocCount > 100 && categoriesDocumentRatio >= 0.5 + ? [ + { + type: 'manyCategories' as const, + categoriesDocumentRatio, + }, + ] + : []), + ...(frequentCategoryCount === 0 + ? [ + { + type: 'noFrequentCategories' as const, + }, + ] + : []), + ...(deadCategoriesRatio >= 0.5 + ? [ + { + type: 'manyDeadCategories' as const, + deadCategoriesRatio, + }, + ] + : []), + ]; +}; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx index fe12ca5455a75..2f34e62d8e611 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx @@ -37,14 +37,13 @@ export const LogEntryRatePageContent = () => { hasLogAnalysisSetupCapabilities, } = useLogAnalysisCapabilitiesContext(); - const { fetchJobStatus, fetchModuleDefinition, setupStatus } = useLogEntryRateModuleContext(); + const { fetchJobStatus, setupStatus } = useLogEntryRateModuleContext(); useEffect(() => { if (hasLogAnalysisReadCapabilities) { - fetchModuleDefinition(); fetchJobStatus(); } - }, [fetchJobStatus, fetchModuleDefinition, hasLogAnalysisReadCapabilities]); + }, [fetchJobStatus, hasLogAnalysisReadCapabilities]); if (isLoadingSource || isUninitialized) { return ; @@ -54,7 +53,7 @@ export const LogEntryRatePageContent = () => { return ; } else if (!hasLogAnalysisReadCapabilities) { return ; - } else if (setupStatus === 'initializing') { + } else if (setupStatus.type === 'initializing') { return ( { })} /> ); - } else if (setupStatus === 'unknown') { + } else if (setupStatus.type === 'unknown') { return ; } else if (isSetupStatusWithResults(setupStatus)) { return ; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx index 50d58865e9746..156c9a919440e 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx @@ -11,21 +11,19 @@ import { EuiFlexItem, EuiPage, EuiPanel, - EuiSpacer, EuiSuperDatePicker, EuiText, } from '@elastic/eui'; import numeral from '@elastic/numeral'; import { FormattedMessage } from '@kbn/i18n/react'; import moment from 'moment'; -import React, { useCallback, useMemo, useState, useEffect } from 'react'; - -import { euiStyled } from '../../../../../observability/public'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { euiStyled, useTrackPageview } from '../../../../../observability/public'; import { TimeRange } from '../../../../common/http_api/shared/time_range'; import { bucketSpan } from '../../../../common/log_analysis'; import { LoadingOverlayWrapper } from '../../../components/loading_overlay_wrapper'; +import { LogAnalysisJobProblemIndicator } from '../../../components/logging/log_analysis_job_status'; import { useInterval } from '../../../hooks/use_interval'; -import { useTrackPageview } from '../../../../../observability/public'; import { useKibanaUiSetting } from '../../../utils/use_kibana_ui_setting'; import { AnomaliesResults } from './sections/anomalies'; import { LogRateResults } from './sections/log_rate'; @@ -35,7 +33,6 @@ import { StringTimeRange, useLogAnalysisResultsUrlState, } from './use_log_entry_rate_results_url_state'; -import { FirstUseCallout } from '../../../components/logging/log_analysis_results'; const JOB_STATUS_POLLING_INTERVAL = 30000; @@ -47,10 +44,13 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { const { fetchJobStatus, - jobStatus, + fetchModuleDefinition, setupStatus, viewSetupForReconfiguration, viewSetupForUpdate, + hasOutdatedJobConfigurations, + hasOutdatedJobDefinitions, + hasStoppedJobs, jobIds, sourceConfiguration: { sourceId }, } = useLogEntryRateModuleContext(); @@ -82,10 +82,6 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { bucketDuration, }); - const hasResults = useMemo(() => (logEntryRate?.histogramBuckets?.length ?? 0) > 0, [ - logEntryRate, - ]); - const handleQueryTimeRangeChange = useCallback( ({ start: startTime, end: endTime }: { start: string; end: string }) => { setQueryTimeRange({ @@ -131,12 +127,23 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { [setAutoRefresh] ); - const isFirstUse = useMemo(() => setupStatus === 'hiddenAfterSuccess', [setupStatus]); + const hasResults = useMemo(() => (logEntryRate?.histogramBuckets?.length ?? 0) > 0, [ + logEntryRate, + ]); + + const isFirstUse = useMemo( + () => setupStatus.type === 'skipped' && !!setupStatus.newlyCreated && !hasResults, + [hasResults, setupStatus] + ); useEffect(() => { getLogEntryRate(); }, [getLogEntryRate, queryTimeRange.lastChangedTime]); + useEffect(() => { + fetchModuleDefinition(); + }, [fetchModuleDefinition]); + useInterval(() => { fetchJobStatus(); }, JOB_STATUS_POLLING_INTERVAL); @@ -195,14 +202,18 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { + + + - {isFirstUse && !hasResults ? ( - <> - - - - ) : null} { diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx index 2551170c44f4e..e216162d7a295 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx @@ -20,7 +20,7 @@ import React, { useMemo } from 'react'; import { euiStyled } from '../../../../../../../observability/public'; import { LogEntryRateResults } from '../../use_log_entry_rate_results'; import { TimeRange } from '../../../../../../common/http_api/shared/time_range'; -import { formatAnomalyScore, JobStatus, SetupStatus } from '../../../../../../common/log_analysis'; +import { formatAnomalyScore } from '../../../../../../common/log_analysis'; import { getAnnotationsForAll, getLogEntryRateCombinedSeries, @@ -28,34 +28,18 @@ import { } from '../helpers/data_formatters'; import { AnomaliesChart } from './chart'; import { AnomaliesTable } from './table'; -import { - LogAnalysisJobProblemIndicator, - RecreateJobButton, -} from '../../../../../components/logging/log_analysis_job_status'; +import { RecreateJobButton } from '../../../../../components/logging/log_analysis_job_status'; import { AnalyzeInMlButton } from '../../../../../components/logging/log_analysis_results'; import { LoadingOverlayWrapper } from '../../../../../components/loading_overlay_wrapper'; export const AnomaliesResults: React.FunctionComponent<{ isLoading: boolean; - jobStatus: JobStatus; results: LogEntryRateResults | null; setTimeRange: (timeRange: TimeRange) => void; - setupStatus: SetupStatus; timeRange: TimeRange; viewSetupForReconfiguration: () => void; - viewSetupForUpdate: () => void; jobId: string; -}> = ({ - isLoading, - jobStatus, - results, - setTimeRange, - setupStatus, - timeRange, - viewSetupForReconfiguration, - viewSetupForUpdate, - jobId, -}) => { +}> = ({ isLoading, results, setTimeRange, timeRange, viewSetupForReconfiguration, jobId }) => { const hasAnomalies = useMemo(() => { return results && results.histogramBuckets ? results.histogramBuckets.some(bucket => { @@ -100,20 +84,13 @@ export const AnomaliesResults: React.FunctionComponent<{ - + - - }> {!results || (results && results.histogramBuckets && !results.histogramBuckets.length) ? ( logAnalysisModule.jobSummaries.some(getIsJobConfigurationOutdated), + [getIsJobConfigurationOutdated, logAnalysisModule.jobSummaries] + ); + + const hasOutdatedJobDefinitions = useMemo( + () => logAnalysisModule.jobSummaries.some(getIsJobDefinitionOutdated), + [getIsJobDefinitionOutdated, logAnalysisModule.jobSummaries] + ); + + const hasStoppedJobs = useMemo( + () => + Object.values(logAnalysisModule.jobStatus).some( + currentJobStatus => currentJobStatus === 'stopped' + ), + [logAnalysisModule.jobStatus] + ); + + return { + ...logAnalysisModule, + fetchModuleDefinition, + hasOutdatedJobConfigurations, + hasOutdatedJobDefinitions, + hasStoppedJobs, + }; }; export const [LogEntryRateModuleProvider, useLogEntryRateModuleContext] = createContainer( diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/components/closable_popover_title.test.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/components/closable_popover_title.test.tsx new file mode 100644 index 0000000000000..8e9daf5f26c40 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/components/closable_popover_title.test.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; +import { mount } from 'enzyme'; +import { ClosablePopoverTitle } from './closable_popover_title'; + +describe('closable popover title', () => { + it('renders with defined options', () => { + const onClose = jest.fn(); + const children =
; + const wrapper = mount( + {children} + ); + expect(wrapper.contains(
)).toBeTruthy(); + }); + + it('onClose function gets called', () => { + const onClose = jest.fn(); + const children =
; + const wrapper = mount( + {children} + ); + wrapper.find('EuiButtonIcon').simulate('click'); + expect(onClose).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/components/closable_popover_title.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/components/closable_popover_title.tsx new file mode 100644 index 0000000000000..f24dfe987a140 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/components/closable_popover_title.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiPopoverTitle, EuiFlexGroup, EuiFlexItem, EuiButtonIcon } from '@elastic/eui'; + +interface ClosablePopoverTitleProps { + children: JSX.Element; + onClose: () => void; +} + +export const ClosablePopoverTitle = ({ children, onClose }: ClosablePopoverTitleProps) => { + return ( + + + {children} + + onClose()} + /> + + + + ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/components/index.ts b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/components/index.ts new file mode 100644 index 0000000000000..d2ea51d37e84a --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/components/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ClosablePopoverTitle } from './closable_popover_title'; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.test.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.test.tsx index e66bb1e7b4b9a..95c16fcea914d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.test.tsx @@ -5,7 +5,6 @@ */ import * as React from 'react'; import { shallow } from 'enzyme'; -import { EuiPopoverTitle } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { ForLastExpression } from './for_the_last'; @@ -40,12 +39,10 @@ describe('for the last expression', () => { expect(wrapper.find('[value="s"]').length > 0).toBeTruthy(); expect( wrapper.contains( - - - + ) ).toBeTruthy(); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.tsx index 673391dd9cbad..defad2b801718 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.tsx @@ -10,7 +10,6 @@ import { i18n } from '@kbn/i18n'; import { EuiExpression, EuiPopover, - EuiPopoverTitle, EuiSelect, EuiFlexGroup, EuiFormRow, @@ -20,6 +19,7 @@ import { import { getTimeUnitLabel } from '../lib/get_time_unit_label'; import { TIME_UNITS } from '../../application/constants'; import { getTimeOptions } from '../lib/get_time_options'; +import { ClosablePopoverTitle } from './components'; interface ForLastExpressionProps { timeWindowSize?: number; @@ -82,12 +82,12 @@ export const ForLastExpression = ({ anchorPosition={popupPosition ?? 'downLeft'} >
- + setAlertDurationPopoverOpen(false)}> - + { it('renders with builtin group by types', () => { @@ -97,6 +97,13 @@ describe('group by expression', () => { ); wrapper.simulate('click'); expect(wrapper.find('[value="all"]').length > 0).toBeTruthy(); - expect(wrapper.contains(over)).toBeTruthy(); + expect( + wrapper.contains( + + ) + ).toBeTruthy(); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.tsx index 7ca32764dbdfd..6ad52a5416163 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.tsx @@ -5,11 +5,11 @@ */ import React, { useState, Fragment } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { EuiExpression, EuiPopover, - EuiPopoverTitle, EuiFlexGroup, EuiFlexItem, EuiFormRow, @@ -18,6 +18,7 @@ import { } from '@elastic/eui'; import { builtInGroupByTypes } from '../constants'; import { GroupByType } from '../types'; +import { ClosablePopoverTitle } from './components'; interface GroupByExpressionProps { groupBy: string; @@ -112,14 +113,12 @@ export const GroupByExpression = ({ anchorPosition={popupPosition ?? 'downRight'} >
- - {i18n.translate( - 'xpack.triggersActionsUI.common.expressionItems.groupByType.overButtonLabel', - { - defaultMessage: 'over', - } - )} - + setGroupByPopoverOpen(false)}> + + { it('renders of builtin aggregation types', () => { @@ -121,6 +121,13 @@ describe('of expression', () => { /> ); wrapper.simulate('click'); - expect(wrapper.contains(of)).toBeTruthy(); + expect( + wrapper.contains( + + ) + ).toBeTruthy(); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.tsx index fdf68cc49572f..70aa4be6b7f87 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.tsx @@ -6,10 +6,10 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiExpression, EuiPopover, - EuiPopoverTitle, EuiFlexGroup, EuiFlexItem, EuiFormRow, @@ -17,6 +17,7 @@ import { } from '@elastic/eui'; import { builtInAggregationTypes } from '../constants'; import { AggregationType } from '../types'; +import { ClosablePopoverTitle } from './components'; interface OfExpressionProps { aggType: string; @@ -100,11 +101,12 @@ export const OfExpression = ({ zIndex={8000} >
- - {i18n.translate('xpack.triggersActionsUI.common.expressionItems.of.popoverTitle', { - defaultMessage: 'of', - })} - + setAggFieldPopoverOpen(false)}> + + { @@ -64,6 +63,6 @@ describe('threshold expression', () => { onChangeSelectedThresholdComparator={onChangeSelectedThresholdComparator} /> ); - expect(wrapper.contains(Is between)).toBeTruthy(); + expect(wrapper.contains('Is between')).toBeTruthy(); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx index d0de7ae77a81e..fb3ff9ceb0926 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx @@ -9,7 +9,6 @@ import { i18n } from '@kbn/i18n'; import { EuiExpression, EuiPopover, - EuiPopoverTitle, EuiFlexGroup, EuiFlexItem, EuiFormRow, @@ -19,6 +18,7 @@ import { } from '@elastic/eui'; import { builtInComparators } from '../constants'; import { Comparator } from '../types'; +import { ClosablePopoverTitle } from './components'; interface ThresholdExpressionProps { thresholdComparator: string; @@ -97,7 +97,9 @@ export const ThresholdExpression = ({ anchorPosition={popupPosition ?? 'downLeft'} >
- {comparators[thresholdComparator].text} + setAlertThresholdPopoverOpen(false)}> + <>{comparators[thresholdComparator].text} + { it('renders with builtin aggregation types', () => { @@ -100,6 +100,13 @@ describe('when expression', () => { ); wrapper.simulate('click'); expect(wrapper.find('[value="avg"]').length > 0).toBeTruthy(); - expect(wrapper.contains(when)).toBeTruthy(); + expect( + wrapper.contains( + + ) + ).toBeTruthy(); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/when.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/when.tsx index b20040608ed9e..65184065e359e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/when.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/when.tsx @@ -6,9 +6,11 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiExpression, EuiPopover, EuiPopoverTitle, EuiSelect } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiExpression, EuiPopover, EuiSelect } from '@elastic/eui'; import { builtInAggregationTypes } from '../constants'; import { AggregationType } from '../types'; +import { ClosablePopoverTitle } from './components'; interface WhenExpressionProps { aggType: string; @@ -64,11 +66,12 @@ export const WhenExpression = ({ anchorPosition={popupPosition ?? 'downLeft'} >
- - {i18n.translate('xpack.triggersActionsUI.common.expressionItems.threshold.popoverTitle', { - defaultMessage: 'when', - })} - + setAggTypePopoverOpen(false)}> + +