├── .eslintrc ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── README_ja.md ├── gather-info.js ├── index.js ├── karma.conf.js ├── package.json ├── public ├── app.js ├── const.js ├── controller │ ├── alertList.controller.js │ ├── alertSetting.controller.js │ └── common │ │ └── header.controller.js ├── less │ └── main.less ├── modules │ └── modals │ │ ├── alertBulkEditModal.html │ │ ├── alertBulkEditModal.js │ │ ├── dashboardSelectModal.html │ │ ├── dashboardSelectModal.js │ │ ├── modal_overlay.html │ │ ├── modal_overlay.js │ │ ├── savedSearchSelectModal.html │ │ └── savedSearchSelectModal.js ├── script │ └── script.js ├── service │ ├── alert.service.js │ ├── esConsole.service.js │ └── mlJob.service.js └── templates │ ├── alertList.html │ ├── alertSetting.html │ └── common │ └── header.html ├── public_test └── service │ ├── alert.service.test.js │ └── esConsole.service.test.js ├── server ├── __tests__ │ └── index.js └── serverInit.js └── translations ├── en.json └── ja.json /.eslintrc: -------------------------------------------------------------------------------- 1 | --- 2 | extends: "@elastic/kibana" 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log* 2 | node_modules 3 | /build/ 4 | coverage 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // 既定の設定とユーザー設定を上書きするには、このファイル内に設定を挿入します 2 | { 3 | "editor.tabSize": 2, 4 | "editor.renderWhitespace": "boundary", 5 | "editor.insertSpaces": true 6 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | README in other languages: [日本語](./README_ja.md) 2 | 3 | Kibana app plugin for creating alert settings of Elasticsearch Machine Leaning Job easily 4 | ==== 5 | 6 | This plugin is for creating alert settings of X-Pack Machine Learning easily on Kibana UI. 7 | 8 | settigs 9 | 10 | # Requirement 11 | 12 | |No |item |required version | 13 | |---|---|---| 14 | |1|Kibana|6.0.0, 6.0.1, 6.1.0, 6.1.1, 6.1.2, 6.2.1, 6.2.2, 6.2.3, 6.2.4| 15 | 16 | # How to use 17 | 18 | You will see "ML Alert" menu in Kibana side bar. 19 | Start to click this menu. 20 | 21 | ## Alert Settings 22 | Select ML job first. 23 | Then input alertID, description and other forms. 24 | 25 | settigs 26 | 27 | You can set the following information 28 | + Mail address 29 | + Slack channel 30 | + Dashboards to show link in the notification message 31 | + Saved Search to show link in the notification message 32 | + Threshold of anomaly score 33 | 34 | Other settings are set automatically. 35 | But you can change in advanced settings. 36 | 37 | Press Save button to save the alert. 38 | 39 | condition 40 | 41 | condition detail 42 | 43 | ## Alert List 44 | This view shows the list of alerts which are made through this plugin.
45 | Bulk operation is also supported.
46 | 47 | list 48 | 49 | bulk edit 50 | 51 | # Installation and prerequisite settings 52 | 53 | ## Plugin installation to Kibana 54 | Get plugin files from [Release page](https://github.com/serive/elastic-ml-alert-plugin/releases). 55 | 56 | Go to Kibana installation directory, stop Kibana process and run the installation command. 57 | ``` 58 | sudo bin/kibana-plugin install file:///es_ml_alert-x.x.x_y.y.y.zip 59 | ``` 60 | 61 | + Stop Kibana process before plugin installation! It may take more than hours to install the plugin if the Kibana process is running. 62 | + Plugin version and Kibana version must be same. 63 | 64 | ## Mail account settings(If you notify by e-mail) 65 | Add mail account settings to elasticsearch.yml. 66 | 67 | [Configuring Email Accounts](https://www.elastic.co/guide/en/x-pack/current/actions-email.html#configuring-email) 68 | 69 | ### Example 70 | ``` 71 | xpack.notification.email.account: 72 | some_mail_account: 73 | email_defaults: 74 | from: notification@example.com 75 | smtp: 76 | auth: true 77 | starttls.enable: true 78 | host: smtp.example.com 79 | port: 587 80 | user: notification@example.com 81 | password: passw0rd 82 | ``` 83 | ### Example of mail notification 84 | mail 85 | 86 | 87 | ## Slack account settings(If you notify by Slack) 88 | Add Slack account settings to elasticsearch.yml. 89 | 90 | [Configuring Slack Accounts](https://www.elastic.co/guide/en/x-pack/current/actions-slack.html#configuring-slack) 91 | 92 | ### Example 93 | ``` 94 | xpack.notification.slack: 95 | account: 96 | ml_alert: 97 | url: https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXX 98 | message_defaults: 99 | from: elastic-ml-alert 100 | ``` 101 | 102 | ### Example of Slack notification 103 | slack 104 | 105 | + Elasticsearch 6.1.1 X-Pack Watcher has a problem sending multibyte characters to slack(actually, it is the problem of webhook). Non-ASCII characters are replaced to "?". Therefore it may cause a problem if Machine Learning Job partition field or partition value has Non-ASCII characters. 106 | 107 | ## LINE Notify settings(If you notify by LINE) 108 | Get access token from [LINE Notify](https://notify-bot.line.me/) . 109 | 110 | You don't need to write it in elasticsearch.yml. 111 | 112 | + Link to Dashboard, Saved Search and Single Metric Viewer are not contained in the notification message of LINE Notify. 113 | 114 | ### Example of LINE Notify message 115 | slack 116 | 117 | # About development 118 | 119 | This plugin is Kibana plugin. 120 | 121 | See the [kibana contributing guide](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md) for instructions setting up your development environment. Once you have completed that, use the following npm tasks. 122 | 123 | - `npm start` 124 | 125 | Start kibana and have it include this plugin 126 | 127 | - `npm start -- --config kibana.yml` 128 | 129 | You can pass any argument that you would normally send to `bin/kibana` by putting them after `--` when running `npm start` 130 | 131 | - `npm run build` 132 | 133 | Build a distributable archive 134 | 135 | - `npm run test:browser` 136 | 137 | Run the browser tests in a real web browser 138 | 139 | - `npm run test:server` 140 | 141 | Run the server tests using mocha 142 | 143 | For more information about any of these commands run `npm run ${task} -- --help`. 144 | 145 | # Licence 146 | 147 | [Apache Version 2.0](https://github.com/serive/es-ml-alert/blob/master/LICENSE) 148 | 149 | # Author 150 | @serive
151 | Twitter: @serive8 152 | -------------------------------------------------------------------------------- /README_ja.md: -------------------------------------------------------------------------------- 1 | README in other languages: [English](./README.md) 2 | 3 | Elasticsearch X-Pack Machine Learning用Alert通知簡単設定プラグイン 4 | ==== 5 | 6 | Kibana UI上で簡単に
7 | Elasticsearch Machine Learningの異常検知時の通知を
8 | 設定することができます。 9 | 10 | settigs 11 | 12 | # Requirement 13 | 14 | |No |項目名 |必須バージョン | 15 | |---|---|---| 16 | |1|Kibana|6.0.0, 6.0.1, 6.1.0, 6.1.1, 6.1.2, 6.2.1, 6.2.2, 6.2.3, 6.2.4| 17 | 18 | # 使い方 19 | 20 | インストール実施した後に、Kibanaを起動してください。 21 | Kibanaにアクセスすると、サイドメニューに「ML Alert」が追加されます。 22 | こちらを選択して下さい。 23 | 本機能が起動します。 24 | 25 | ## Alert設定追加画面 26 | 異常検知結果を通知させたいML jobを選択し、設定を開始します。 27 | 28 | settigs 29 | 30 | ## Alert条件設定 31 | 通知先や通知条件を設定します。以下の設定ができます。 32 | + 通知先メールアドレス 33 | + 通知先のSlack channel 34 | + LINE Notify通知用のアクセストークン 35 | + 通知メッセージに含めるリンク先のDashboard 36 | + 通知メッセージに含めるリンク先のSaved Search 37 | + 通知を行う異常検知スコア値 38 | 39 | Alertの起動タイミングなど、上記以外の設定値は自動で生成しますが、詳細設定で変更することもできます。 40 | 41 | 設定後に、Saveボタンを押して保存してください。 42 | 43 | condition 44 | 45 | condition detail 46 | 47 | ## Alert一覧画面 48 | 追加したAlert設定の一覧を表示します。
49 | 複数のAlertを選択して、一括で削除やDeactivateなどの操作をすることも可能です。
50 | 51 | list 52 | 53 | bulk edit 54 | 55 | # インストール手順 56 | 57 | ## Kibanaへのプラグインインストール 58 | 59 | [リリースページ](https://github.com/serive/elastic-ml-alert-plugin/releases) から、Kibanaのバージョンに合ったファイルを取得してください。 60 | 61 | Kibanaのインストールディレクトリに移動し、Kibanaが停止している状態で、以下のコマンドを実行してインストールします。 62 | ``` 63 | sudo bin/kibana-plugin install file:///es_ml_alert-x.x.x_y.y.y.zip 64 | ``` 65 | 66 | ※Kibanaプロセス実行中にインストールコマンドを実行すると、インストールに1時間以上かかる場合があるので注意してください。 67 | 68 | ※Kibanaのバージョンに合ったインストール媒体を使ってください。バージョンが合わないと、インストールに失敗します。 69 | 70 | ## Elasticsearchへの、メール設定追加(メール通知を行う場合) 71 | 以下を参考に、elasticsearch.yml にメール通知用の設定を追加してください。 72 | 73 | [Configuring Email Accounts](https://www.elastic.co/guide/en/x-pack/current/actions-email.html#configuring-email) 74 | 75 | ### 設定例 76 | ``` 77 | xpack.notification.email.account: 78 | some_mail_account: 79 | email_defaults: 80 | from: notification@example.com 81 | smtp: 82 | auth: true 83 | starttls.enable: true 84 | host: smtp.example.com 85 | port: 587 86 | user: notification@example.com 87 | password: passw0rd 88 | ``` 89 | ### 通知メールの例 90 | mail 91 | 92 | 93 | ## Elasticsearchへの、Slack設定追加(Slack通知を行う場合) 94 | 以下を参考に、elasticsearch.yml にSlack通知用の設定を追加してください。 95 | 96 | [Configuring Slack Accounts](https://www.elastic.co/guide/en/x-pack/current/actions-slack.html#configuring-slack) 97 | 98 | ### 設定例 99 | ``` 100 | xpack.notification.slack: 101 | account: 102 | ml_alert: 103 | url: https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXX 104 | message_defaults: 105 | from: elastic-ml-alert 106 | ``` 107 | 108 | ### Slack通知の例 109 | slack 110 | 111 | ※ Elasticsearch 6.1.1 のX-Pack Watcherには、非ASCII文字をSlackに送ると(webhookも同様)全て「?」に変換されてしまう問題があります。そのため、Machine Learning Jobのpartition fieldやpartition valueにマルチバイト文字が含まれると、通知メッセージの表示がおかしくなったりリンクが壊れる場合があります。今後のバージョンアップで修正されるものと思われます。 112 | 113 | ## LINE Notifyの設定(LINEに通知する場合) 114 | [LINE Notify](https://notify-bot.line.me/ja/) から、アクセストークンを取得してください。 115 | 116 | 取得したアクセストークンを指定すれば、通知が届くようになります。 117 | 118 | ※LINE Notifyによる通知メッセージには、Dashboard, Saved Search, Single Metric Viewerなどへのリンクは含まれません 119 | 120 | ### LINE Notifyによる通知メッセージの例 121 | slack 122 | 123 | # 開発に関して 124 | 125 | Kibanaプラグインとして開発しています。
126 | 以下にKibanaプラグインの開発情報を記載します。 127 | 128 | See the [kibana contributing guide](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md) for instructions setting up your development environment. Once you have completed that, use the following npm tasks. 129 | 130 | - `npm start` 131 | 132 | Start kibana and have it include this plugin 133 | 134 | - `npm start -- --config kibana.yml` 135 | 136 | You can pass any argument that you would normally send to `bin/kibana` by putting them after `--` when running `npm start` 137 | 138 | - `npm run build` 139 | 140 | Build a distributable archive 141 | 142 | - `npm run test:browser` 143 | 144 | Run the browser tests in a real web browser 145 | 146 | - `npm run test:server` 147 | 148 | Run the server tests using mocha 149 | 150 | For more information about any of these commands run `npm run ${task} -- --help`. 151 | 152 | # Licence 153 | 154 | [Apache Version 2.0](https://github.com/serive/es-ml-alert/blob/master/LICENSE) 155 | 156 | # Author 157 | @serive
158 | Twitter: @serive8 159 | -------------------------------------------------------------------------------- /gather-info.js: -------------------------------------------------------------------------------- 1 | const templatePkg = require('./package.json'); 2 | const kibanaPkg = require('../kibana/package.json'); 3 | 4 | const debugInfo = { 5 | kibana: { 6 | version: kibanaPkg.version, 7 | build: kibanaPkg.build, 8 | engines: kibanaPkg.engines, 9 | }, 10 | plugin: { 11 | name: templatePkg.name, 12 | version: templatePkg.version, 13 | kibana: templatePkg.kibana, 14 | dependencies: templatePkg.dependencies, 15 | }, 16 | }; 17 | 18 | console.log(debugInfo); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import serverInit from './server/serverInit'; 3 | 4 | export default function (kibana) { 5 | return new kibana.Plugin({ 6 | require: ['elasticsearch'], 7 | uiExports: { 8 | 9 | app: { 10 | title: 'Ml Alert', 11 | description: 'Create Alert Setting for ML', 12 | main: 'plugins/es_ml_alert/app' 13 | }, 14 | translations: [ 15 | resolve(__dirname, './translations/en.json'), 16 | resolve(__dirname, './translations/ja.json') 17 | ] 18 | }, 19 | config(Joi) { 20 | return Joi.object({ 21 | enabled: Joi.boolean().default(true), 22 | }).default(); 23 | }, 24 | init(server, options) { 25 | // Add server routes and initalize the plugin here 26 | serverInit(server, options); 27 | } 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Mon Jun 12 2017 08:09:36 GMT+0900 (東京 (標準時)) 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['mocha', 'browserify'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | 'public/**/*.js', 19 | 'public_test/**/*.js' 20 | ], 21 | 22 | 23 | // list of files to exclude 24 | // Angularを起動するテストは、別に実施可能なので、ここではやらない。 25 | exclude: [ 26 | 'public/app.js' 27 | ], 28 | 29 | 30 | // preprocess matching files before serving them to the browser 31 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 32 | preprocessors: { 33 | 'public/**/*.js': 'browserify', 34 | 'public_test/**/*.js': 'browserify' 35 | }, 36 | browserify: { 37 | debug: true, 38 | transform: [ 39 | ['babelify', { presets: ['es2015'], plugins: ['istanbul'] }] 40 | ] 41 | }, 42 | 43 | // test results reporter to use 44 | // possible values: 'dots', 'progress' 45 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 46 | reporters: ['progress', 'coverage'], 47 | 48 | coverageReporter: { type: 'lcov' }, 49 | 50 | // web server port 51 | port: 9876, 52 | 53 | 54 | // enable / disable colors in the output (reporters and logs) 55 | colors: true, 56 | 57 | 58 | // level of logging 59 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 60 | logLevel: config.LOG_DEBUG, 61 | 62 | 63 | // enable / disable watching file and executing tests whenever any file changes 64 | autoWatch: true, 65 | 66 | 67 | // start these browsers 68 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 69 | browsers: ['Chrome'], 70 | 71 | 72 | // Continuous Integration mode 73 | // if true, Karma captures browsers, runs the tests and exits 74 | singleRun: false, 75 | 76 | // Concurrency level 77 | // how many browser should be started simultaneous 78 | concurrency: Infinity 79 | }); 80 | }; 81 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "es_ml_alert", 3 | "version": "0.4.1", 4 | "description": "Create Alert Setting for ML", 5 | "main": "index.js", 6 | "kibana": { 7 | "version": "6.2.4", 8 | "templateVersion": "7.2.0" 9 | }, 10 | "scripts": { 11 | "lint": "eslint", 12 | "start": "plugin-helpers start", 13 | "test:server": "plugin-helpers test:server", 14 | "test:browser": "plugin-helpers test:browser", 15 | "test:local": "karma start", 16 | "build": "plugin-helpers build", 17 | "postinstall": "plugin-helpers postinstall", 18 | "gather-info": "node gather-info.js" 19 | }, 20 | "devDependencies": { 21 | "@elastic/eslint-config-kibana": "^0.6.1", 22 | "@elastic/plugin-helpers": "^7.0.0", 23 | "babel-eslint": "^7.2.3", 24 | "babel-plugin-istanbul": "^4.1.4", 25 | "babel-preset-es2015": "^6.24.1", 26 | "babel-preset-es2016": "^6.24.1", 27 | "babelify": "^7.3.0", 28 | "browserify": "^14.4.0", 29 | "chai": "^3.5.0", 30 | "eslint": "^3.19.0", 31 | "eslint-plugin-babel": "^4.1.1", 32 | "eslint-plugin-import": "^2.3.0", 33 | "eslint-plugin-mocha": "^4.9.0", 34 | "eslint-plugin-react": "^7.0.1", 35 | "karma-browserify": "^5.1.1", 36 | "karma-chrome-launcher": "^2.1.1", 37 | "karma-coverage": "^1.1.1", 38 | "karma-mocha": "^1.3.0", 39 | "mocha": "^3.4.2", 40 | "watchify": "^3.9.0" 41 | }, 42 | "dependencies": { 43 | "bootstrap": "^3.3.7", 44 | "elasticsearch": "13.0.1", 45 | "install": "^0.10.1", 46 | "npm": "^5.0.3", 47 | "parse-duration": "^0.1.1" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /public/app.js: -------------------------------------------------------------------------------- 1 | var parse = require('parse-duration'); 2 | 3 | import moment from 'moment'; 4 | import chrome from 'ui/chrome'; 5 | import { uiModules } from 'ui/modules'; 6 | import uiRoutes from 'ui/routes'; 7 | 8 | import 'ui/es'; 9 | import 'ui/modals'; 10 | import 'ui/courier'; 11 | import 'ui/tooltip'; 12 | import 'ui/doc_title'; 13 | import 'ui/autoload/styles'; 14 | import 'ui/react_components'; 15 | import './less/main.less'; 16 | import 'plugins/kibana/dashboard/saved_dashboard/saved_dashboards'; 17 | import 'plugins/kibana/discover/saved_searches/saved_searches'; 18 | import alertListTemplate from './templates/alertList.html'; 19 | import alertSettingTemplate from './templates/alertSetting.html'; 20 | import headerTemplate from './templates/common/header.html'; 21 | 22 | import { alertBulkEditModal } from './modules/modals/alertBulkEditModal'; 23 | import { dashboardSelectModal } from './modules/modals/dashboardSelectModal'; 24 | import { savedSearchSelectModal } from './modules/modals/savedSearchSelectModal'; 25 | 26 | import HeaderController from './controller/common/header.controller'; 27 | import AlertListController from './controller/alertList.controller'; 28 | import AlertSettingController from './controller/alertSetting.controller'; 29 | 30 | import EsDevToolService from './service/esConsole.service'; 31 | import MlJobService from './service/mlJob.service'; 32 | import AlertService from './service/alert.service'; 33 | 34 | import constValue from './const'; 35 | import { script, scriptSlack, scriptLine } from './script/script'; 36 | 37 | uiRoutes.enable(); 38 | uiRoutes 39 | .when('/alert_setting/:alertId', { 40 | template: alertSettingTemplate, 41 | controller: 'AlertSettingController', 42 | controllerAs: 'asCtrl' 43 | }) 44 | .when('/alert_setting', { 45 | template: alertSettingTemplate, 46 | controller: 'AlertSettingController', 47 | controllerAs: 'asCtrl' 48 | }) 49 | .when('/alert_list', { 50 | template: alertListTemplate, 51 | controller: 'AlertListController', 52 | controllerAs: 'alCtrl' 53 | }) 54 | .when('/', { 55 | redirectTo: '/alert_list' 56 | }); 57 | 58 | uiModules 59 | .get('app/ml_alert', []) 60 | .constant('mlaConst', constValue) 61 | .constant('script', script) 62 | .constant('scriptSlack', scriptSlack) 63 | .constant('scriptLine', scriptLine) 64 | .constant('parse', parse) 65 | .controller('AlertListController', AlertListController) 66 | .controller('AlertSettingController', AlertSettingController) 67 | .directive('mlHeader', function () { 68 | return { 69 | restrict: 'E', 70 | template: headerTemplate, 71 | controller: HeaderController, 72 | controllerAs: 'hCtrl' 73 | }; 74 | }) 75 | .factory('EsDevToolService', EsDevToolService) 76 | .factory('MlJobService', MlJobService) 77 | .factory('AlertService', AlertService); 78 | -------------------------------------------------------------------------------- /public/const.js: -------------------------------------------------------------------------------- 1 | var constValue = { 2 | names: { 3 | appName: 'ml_alert', 4 | mlIndexName: '.ml-anomalies-*', 5 | indexName: '.ml-alert', 6 | scriptForMail: 'create_partition_notify_for_mail', 7 | scriptForSlack: 'create_partition_notify_for_slack', 8 | scriptForLine: 'create_partition_notify_for_line' 9 | }, 10 | displayNames: { 11 | 'alert_list': 'Alert List', 12 | 'alert_setting': 'Alert Settings' 13 | }, 14 | paths: { 15 | console: { 16 | method: 'POST', 17 | path: '/api/console/proxy' 18 | }, 19 | mlJobList: { 20 | method: 'GET', 21 | path: '_xpack/ml/anomaly_detectors/' 22 | }, 23 | mlJobDataFeed: { 24 | method: 'GET', 25 | path: '_xpack/ml/datafeeds/' 26 | }, 27 | getWatch: { 28 | method: 'GET', 29 | path: '_xpack/watcher/watch/' 30 | }, 31 | deleteWatch: { 32 | method: 'DELETE', 33 | path: '_xpack/watcher/watch/' 34 | }, 35 | editWatch: { 36 | method: 'PUT', 37 | path: '_xpack/watcher/watch/' 38 | }, 39 | getScript: { 40 | method: 'GET', 41 | path: '_scripts/' 42 | }, 43 | putScript: { 44 | method: 'PUT', 45 | path: '_scripts/' 46 | } 47 | }, 48 | alert: { 49 | threshold: 40, 50 | notification: 'mail', 51 | processTime: '3m' 52 | }, 53 | mailAction: { 54 | "transform": { 55 | "script": { 56 | "id": "create_partition_notify_for_mail" 57 | } 58 | }, 59 | "email": { 60 | "profile": "standard", 61 | "to": [ 62 | "sample@sample.com" 63 | ], 64 | "subject": "{{ctx.metadata.subject}}", 65 | "body": { 66 | "html": "{{ctx.payload.message}}" 67 | } 68 | } 69 | }, 70 | slackAction: { 71 | "transform": { 72 | "script": { 73 | "id": "create_partition_notify_for_slack" 74 | } 75 | }, 76 | "slack" : { 77 | "message" : { 78 | "to" : [], 79 | "text": "Elasticsearch ML Anomaly Detection", 80 | "attachments" : [ 81 | { 82 | "title": "{{ctx.payload.severity}}", 83 | "text": "{{ctx.payload.message}}", 84 | "color": "{{ctx.payload.severityColor}}" 85 | } 86 | ] 87 | } 88 | } 89 | }, 90 | lineAction: { 91 | "transform": { 92 | "script": { 93 | "id": "create_partition_notify_for_line" 94 | }, 95 | }, 96 | "webhook": { 97 | "method": "POST", 98 | "host": "notify-api.line.me", 99 | "port": 443, 100 | "path": "/api/notify", 101 | "scheme": "https", 102 | "headers" : { 103 | "Authorization": "Bearer {{ctx.metadata.line_notify_access_token}}" 104 | }, 105 | "params" : { 106 | "message" : "{{ctx.payload.message}}" 107 | } 108 | } 109 | }, 110 | alertTemplate: { 111 | "trigger": { 112 | "schedule": { 113 | "interval": "1m" 114 | } 115 | }, 116 | "input": { 117 | "search": { 118 | "request": { 119 | "search_type": "query_then_fetch", 120 | "indices": [ 121 | ".ml-anomalies-*" 122 | ], 123 | "types": [], 124 | "body": { 125 | "sort": [ 126 | { 127 | "timestamp": { 128 | "order": "asc" 129 | } 130 | } 131 | ], 132 | "query": { 133 | "bool": { 134 | "must": [ 135 | { 136 | "match": { 137 | "result_type": "record" 138 | } 139 | }, 140 | { 141 | "match": { 142 | "job_id": "{{ctx.metadata.job_id}}" 143 | } 144 | }, 145 | { 146 | "range": { 147 | "record_score": { 148 | "gt": "{{ctx.metadata.threshold}}" 149 | } 150 | } 151 | }, 152 | { 153 | "range": { 154 | "timestamp": { 155 | "from": "now-{{ctx.metadata.detect_interval}}-{{ctx.metadata.ml_process_time}}", 156 | "to": "now-{{ctx.metadata.ml_process_time}}" 157 | } 158 | } 159 | } 160 | ] 161 | } 162 | } 163 | } 164 | } 165 | } 166 | }, 167 | "condition": { 168 | "compare": { 169 | "ctx.payload.hits.total": { 170 | "gt": 0 171 | } 172 | } 173 | }, 174 | "actions": { 175 | "send_email": { 176 | "transform": { 177 | "script": { 178 | "id": "create_partition_notify_for_mail" 179 | } 180 | }, 181 | "email": { 182 | "profile": "standard", 183 | "to": [ 184 | "sample@sample.com" 185 | ], 186 | "subject": "{{ctx.metadata.subject}}", 187 | "body": { 188 | "html": "{{ctx.payload.message}}" 189 | } 190 | } 191 | }, 192 | "notify_slack": { 193 | "transform": { 194 | "script": { 195 | "id": "create_partition_notify_for_slack" 196 | }, 197 | }, 198 | "slack" : { 199 | "message" : { 200 | "to" : [], 201 | "text": "Elasticsearch ML Anomaly Detection", 202 | "attachments" : [ 203 | { 204 | "title": "{{ctx.payload.severity}}", 205 | "text": "{{ctx.payload.message}}", 206 | "color": "{{ctx.payload.severityColor}}" 207 | } 208 | ] 209 | } 210 | } 211 | }, 212 | "notify_line": { 213 | "transform": { 214 | "script": { 215 | "id": "create_partition_notify_for_line" 216 | }, 217 | }, 218 | "webhook": { 219 | "method": "POST", 220 | "host": "notify-api.line.me", 221 | "port": 443, 222 | "path": "/api/notify", 223 | "scheme": "https", 224 | "headers" : { 225 | "Authorization": "Bearer {{ctx.metadata.line_notify_access_token}}" 226 | }, 227 | "params" : { 228 | "message" : "{{ctx.payload.message}}" 229 | } 230 | } 231 | } 232 | }, 233 | "metadata": { 234 | "quate": "'", 235 | "link_dashboards": [], 236 | "kibana_display_term": 3600, 237 | "detect_interval": "1m", 238 | "description": "", 239 | "threshold": 0, 240 | "locale": "Asia/Tokyo", 241 | "kibana_url": "http://localhost:5601/", 242 | "alert_type": "mla", 243 | "ml_process_time": "3m", 244 | "subject": "Elasticsearch ML 異常検知通知", 245 | "line_notify_access_token": "", 246 | "double_quate": "\"", 247 | "job_id": "", 248 | "date_format": "yyyy/MM/dd HH:mm:ss" 249 | } 250 | } 251 | }; 252 | export default constValue; -------------------------------------------------------------------------------- /public/controller/alertList.controller.js: -------------------------------------------------------------------------------- 1 | export default function AlertListController($scope, $routeParams, $location, $translate, docTitle, AlertService, confirmModal, Notifier, alertBulkEditModal) { 2 | docTitle.change('ML Alert'); 3 | const notify = new Notifier({ location: 'ML Alert' }); 4 | $scope.selectedItems = []; 5 | $scope.data = []; 6 | $scope.filterText = ""; 7 | $scope.areAllRowsChecked = function areAllRowsChecked() { 8 | if ($scope.data.length === 0) { 9 | return false; 10 | } 11 | return $scope.selectedItems.length === $scope.data.length; 12 | }; 13 | $scope.filterAlertId = function($event) { 14 | $scope.mlJobAlerts = $scope.data 15 | .filter(datum => ~datum["_id"].indexOf($scope.filterText)) 16 | .reduce(function (prev, alert) { 17 | var jobId = alert["_source"]["metadata"]["job_id"]; 18 | if (jobId in prev) { 19 | prev[jobId].push(alert); 20 | } else { 21 | prev[jobId] = [alert]; 22 | } 23 | return prev; 24 | }, {}); 25 | }; 26 | $scope.activateAlert = function ($event) { 27 | var alertId = $event.currentTarget.value; 28 | AlertService.activate([alertId], function (successCount, totalCount) { 29 | $translate('MLA-ENABLED_ALERT', {"alertId": alertId}).then(function(translation) { 30 | notify.info(translation); 31 | }); 32 | }, function (failCount, totalCount) { 33 | $translate('MLA-ENABLE_ALERT_FAILED', {"alertId": alertId}).then(function(translation) { 34 | notify.error(translation); 35 | }); 36 | }) 37 | .then($scope.init) 38 | .catch(error => notify.error(error)); 39 | }; 40 | $scope.deactivateAlert = function ($event) { 41 | var alertId = $event.currentTarget.value; 42 | AlertService.deactivate([alertId], function (successCount, totalCount) { 43 | $translate('MLA-DISABLED_ALERT', {"alertId": alertId}).then(function(translation) { 44 | notify.info(translation); 45 | }); 46 | }, function (failCount, totalCount) { 47 | $translate('MLA-DISABLE_ALERT_FAILED', {"alertId": alertId}).then(function(translation) { 48 | notify.error(translation); 49 | }); 50 | }) 51 | .then($scope.init) 52 | .catch(error => notify.error(error)); 53 | }; 54 | $scope.moveCreate = function () { 55 | $location.path('/alert_setting'); 56 | }; 57 | $scope.moveEdit = function ($event, clone) { 58 | if (clone) { 59 | $location.path('/alert_setting/' + $event.currentTarget.value).search({'clone': 'true'});; 60 | } else { 61 | $location.path('/alert_setting/' + $event.currentTarget.value); 62 | } 63 | }; 64 | $scope.delete = function ($event) { 65 | var alertId = $event.currentTarget.value; 66 | function doDelete() { 67 | AlertService.delete([alertId], function () { 68 | $translate('MLA-DELETED_ALERT', {"alertId": alertId}).then(function(translation) { 69 | notify.info(translation); 70 | }); 71 | }, function () { 72 | $translate('MLA-DELETE_ALERT_FAILED', {"alertId": alertId}).then(function(translation) { 73 | notify.error(translation); 74 | }); 75 | }) 76 | .then($scope.init) 77 | .then(function () { 78 | $scope.selectedItems.length = 0; 79 | }) 80 | .catch(error => notify.error(error)); 81 | } 82 | const confirmModalOptions = { 83 | confirmButtonText: `Delete ${alertId}`, 84 | onConfirm: doDelete 85 | }; 86 | $translate('MLA-CONFIRM_DELETE_ALERT', {"alertId": alertId}).then(function(translation) { 87 | confirmModal( 88 | translation, 89 | confirmModalOptions 90 | ); 91 | }); 92 | }; 93 | $scope.bulkEdit = function () { 94 | function doBulkDelete() { 95 | AlertService.delete($scope.selectedItems.map(item => item['_id']), function (successCount, totalCount) { 96 | $translate('MLA-DELETED_ALERTS', {"totalCount": totalCount, "successCount": successCount}).then(function(translation) { 97 | notify.info(translation); 98 | }); 99 | }, function (failCount, totalCount) { 100 | $translate('MLA-DELETE_ALERTS_FAILED', {"totalCount": totalCount, "failCount": failCount}).then(function(translation) { 101 | notify.error(translation); 102 | }); 103 | }) 104 | .then($scope.init) 105 | .then(function () { 106 | $scope.selectedItems.length = 0; 107 | }) 108 | .catch(error => notify.error(error)); 109 | } 110 | function doBulkActivate() { 111 | AlertService.activate($scope.selectedItems.map(item => item['_id']), function (successCount, totalCount) { 112 | $translate('MLA-ENABLED_ALERTS', {"totalCount": totalCount, "successCount": successCount}).then(function(translation) { 113 | notify.info(translation); 114 | }); 115 | }, function (failCount, totalCount) { 116 | $translate('MLA-ENABLE_ALERTS_FAILED', {"totalCount": totalCount, "failCount": failCount}).then(function(translation) { 117 | notify.error(translation); 118 | }); 119 | }) 120 | .then($scope.init) 121 | .then(function () { 122 | $scope.selectedItems.length = 0; 123 | }) 124 | .catch(error => notify.error(error)); 125 | } 126 | function doBulkDeactivate() { 127 | AlertService.deactivate($scope.selectedItems.map(item => item['_id']), function (successCount, totalCount) { 128 | $translate('MLA-DISABLED_ALERTS', {"totalCount": totalCount, "successCount": successCount}).then(function(translation) { 129 | notify.info(translation); 130 | }); 131 | }, function (failCount, totalCount) { 132 | $translate('MLA-DISABLE_ALERTS_FAILED', {"totalCount": totalCount, "failCount": failCount}).then(function(translation) { 133 | notify.error(translation); 134 | }); 135 | }) 136 | .then($scope.init) 137 | .then(function () { 138 | $scope.selectedItems.length = 0; 139 | }) 140 | .catch(error => notify.error(error)); 141 | } 142 | function doBulkUpdate(input) { 143 | AlertService.bulkUpdate($scope.selectedItems.map(item => item['_id']), input, function (successCount, totalCount) { 144 | $translate('MLA-UPDATED_ALERTS', {"totalCount": totalCount, "successCount": successCount}).then(function(translation) { 145 | notify.info(translation); 146 | }); 147 | }, function (failCount, totalCount) { 148 | $translate('MLA-UPDATE_ALERTS_FAILED', {"totalCount": totalCount, "failCount": failCount}).then(function(translation) { 149 | notify.error(translation); 150 | }); 151 | }) 152 | .then($scope.init) 153 | .then(function () { 154 | $scope.selectedItems.length = 0; 155 | }) 156 | .catch(error => notify.error(error)); 157 | } 158 | $translate(["MLA-DELETE_ALERTS_MESSAGE", "MLA-ENABLE_ALERTS_MESSAGE", "MLA-DISABLE_ALERTS_MESSAGE", "MLA-BULK_OPERATION_TITLE", "MLA-UPDATE_ALERTS_MESSAGE", "MLA-BULK_UPDATE"]).then(function(translations) { 159 | const confirmModalOptions = { 160 | onDelete: doBulkDelete, 161 | onActivate: doBulkActivate, 162 | onDeactivate: doBulkDeactivate, 163 | onBulkUpdate: doBulkUpdate, 164 | deleteMessage: translations["MLA-DELETE_ALERTS_MESSAGE"], 165 | activateMessage: translations["MLA-ENABLE_ALERTS_MESSAGE"], 166 | deactivateMessage: translations["MLA-DISABLE_ALERTS_MESSAGE"], 167 | title: translations["MLA-BULK_OPERATION_TITLE"], 168 | updateButtonText: translations["MLA-BULK_UPDATE"], 169 | updateMessage: translations["MLA-UPDATE_ALERTS_MESSAGE"], 170 | showClose: true 171 | }; 172 | alertBulkEditModal( 173 | confirmModalOptions 174 | ); 175 | }); 176 | }; 177 | $scope.toggleAll = function () { 178 | if ($scope.selectedItems.length === $scope.data.length) { 179 | $scope.selectedItems.length = 0; 180 | } else { 181 | $scope.selectedItems = [].concat($scope.data); 182 | } 183 | }; 184 | $scope.toggleItem = function (item) { 185 | const i = $scope.selectedItems.indexOf(item); 186 | if (i >= 0) { 187 | $scope.selectedItems.splice(i, 1); 188 | } else { 189 | $scope.selectedItems.push(item); 190 | } 191 | }; 192 | // Get watches of elastic-ml-alert 193 | $scope.init = function () { 194 | AlertService.searchList(function (body) { 195 | $scope.data = body["hits"]["hits"]; 196 | $scope.mlJobAlerts = body["hits"]["hits"] 197 | .filter(datum => ~datum["_id"].indexOf($scope.filterText)) 198 | .reduce(function (prev, alert) { 199 | var jobId = alert["_source"]["metadata"]["job_id"]; 200 | if (jobId in prev) { 201 | prev[jobId].push(alert); 202 | } else { 203 | prev[jobId] = [alert]; 204 | } 205 | return prev; 206 | }, {}); 207 | if (Object.keys($scope.mlJobAlerts).length == 0) { 208 | $scope.moveCreate(); 209 | } 210 | }, function (error) { 211 | console.error(error.message); 212 | }); 213 | } 214 | } -------------------------------------------------------------------------------- /public/controller/alertSetting.controller.js: -------------------------------------------------------------------------------- 1 | export default function AlertSettingController($scope, $routeParams, $location, $translate, docTitle, Notifier, mlaConst, MlJobService, AlertService, dashboardSelectModal, savedSearchSelectModal, savedDashboards, savedSearches) { 2 | docTitle.change('ML Alert'); 3 | const notify = new Notifier({ location: 'ML Alert' }); 4 | var vm = this; 5 | vm.compareOptions = [ 6 | {compareType:'gte', operator:'≧'}, 7 | {compareType:'gt', operator:'>'}, 8 | {compareType:'lte', operator:'≦'}, 9 | {compareType:'lt', operator:'<'} 10 | ]; 11 | // default values 12 | vm.input = { 13 | mlJobId: '', 14 | alertId: '', 15 | description: '', 16 | subject: 'Elasticsearch ML Anomaly Detection', 17 | sendMail: true, 18 | mailAddressTo: [ 19 | {value: ''} 20 | ], 21 | mailAddressCc: [], 22 | mailAddressBcc: [], 23 | notifySlack: false, 24 | slackAccount: '', 25 | slackTo: [ 26 | {value: ''} 27 | ], 28 | notifyLine: false, 29 | lineNotifyAccessToken: '', 30 | linkDashboards: [], 31 | linkSavedSearches: [], 32 | threshold: 0, 33 | scheduleKind: 'cron', 34 | triggerSchedule: '0 * * * * ?', 35 | detectInterval: '1m', 36 | kibanaDisplayTerm: 900, 37 | locale: 'Asia/Tokyo', 38 | mlProcessTime: '10m', 39 | filterByActualValue: false, 40 | actualValueThreshold: 0, 41 | compareOption: vm.compareOptions[0], 42 | kibanaUrl: "http://localhost:5601/" 43 | }; 44 | // other default values 45 | vm.internal = { 46 | showSetting: false, 47 | ShowDetailSetting: false 48 | }; 49 | vm.dashboards = []; 50 | vm.savedSearches = []; 51 | vm.existsAlert = false; 52 | vm.autoSettingEnabled = true; 53 | vm.frequency = '150s'; 54 | vm.queryDelay = '60s'; 55 | var isFetch = false; 56 | var isChange = false; 57 | vm.changeJobId = function () { 58 | updateJobList(vm.input.mlJobId); 59 | if (isFetch) { 60 | isChange = true; 61 | return; 62 | } 63 | isChange = false; 64 | isFetch = true; 65 | MlJobService.search(vm.input.mlJobId, function (res) { 66 | let data = res.data; 67 | if (!data) { 68 | displayJobSearchResult(false); 69 | return; 70 | } 71 | if (!data.jobs) { 72 | displayJobSearchResult(false); 73 | return; 74 | } 75 | if (data.jobs.length === 0) { 76 | displayJobSearchResult(false); 77 | return; 78 | } 79 | displayJobSearchResult(true); 80 | vm.mlJob = data.jobs[0]; 81 | autoSetting(vm.mlJob); 82 | finishSearch(); 83 | }, function () { 84 | displayJobSearchResult(false); 85 | finishSearch(); 86 | }); 87 | }; 88 | function displayJobSearchResult(isFetch) { 89 | if (isFetch) { 90 | vm.internal.showSetting = true; 91 | } else { 92 | vm.internal.showSetting = false; 93 | } 94 | } 95 | vm.showDetail = function () { 96 | vm.internal.ShowDetailSetting = true; 97 | }; 98 | vm.hideDetail = function () { 99 | vm.internal.ShowDetailSetting = false; 100 | }; 101 | vm.save = function () { 102 | AlertService.checkScripts(function(){ 103 | AlertService.save(vm.input, function() { 104 | $location.path('/alert_list'); 105 | }, function(error) { 106 | notify.error(error); 107 | console.error(error); 108 | }); 109 | }, function(error){ 110 | notify.error(error); 111 | console.error(error); 112 | }); 113 | }; 114 | vm.selectJob = function (jobId) { 115 | vm.input.mlJobId = jobId; 116 | vm.changeJobId(); 117 | }; 118 | vm.checkAlertId = function() { 119 | if (!vm.input.alertId || vm.input.alertId === "") { 120 | vm.existsAlert = false; 121 | } else { 122 | AlertService.search(vm.input.alertId, function(res) { 123 | vm.existsAlert = true; 124 | }, function(e) { 125 | vm.existsAlert = false; 126 | }); 127 | } 128 | }; 129 | vm.setThreshold = function(threshold) { 130 | vm.input.threshold = threshold; 131 | }; 132 | vm.addTo = function() { 133 | vm.input.mailAddressTo.push({value: ''}); 134 | }; 135 | vm.deleteTo = function(index) { 136 | vm.input.mailAddressTo.splice(index, 1); 137 | }; 138 | vm.addCc = function() { 139 | vm.input.mailAddressCc.push({value: ''}); 140 | }; 141 | vm.deleteCc = function(index) { 142 | vm.input.mailAddressCc.splice(index, 1); 143 | }; 144 | vm.addBcc = function() { 145 | vm.input.mailAddressBcc.push({value: ''}); 146 | }; 147 | vm.deleteBcc = function(index) { 148 | vm.input.mailAddressBcc.splice(index, 1); 149 | }; 150 | vm.addSlackTo = function() { 151 | vm.input.slackTo.push({value: ''}); 152 | }; 153 | vm.deleteSlackTo = function(index) { 154 | vm.input.slackTo.splice(index, 1); 155 | }; 156 | vm.removeDashboard = function(index) { 157 | vm.dashboards.splice(index, 1); 158 | vm.input.linkDashboards = vm.dashboards.map(item => ({ 159 | id : item.id, 160 | title : item.title 161 | })); 162 | }; 163 | vm.removeSavedSearch = function(index) { 164 | vm.savedSearches.splice(index, 1); 165 | vm.input.linkSavedSearches = vm.savedSearches.map(item => ({ 166 | id : item.id, 167 | title : item.title 168 | })); 169 | }; 170 | vm.selectDashboard = function () { 171 | function select(dashboard) { 172 | if (!~vm.input.linkDashboards.map(item => item.id).indexOf(dashboard.id)) { 173 | vm.dashboards.push(dashboard); 174 | vm.input.linkDashboards = vm.dashboards.map(item => ({ 175 | id : item.id, 176 | title : item.title 177 | })); 178 | } 179 | } 180 | $translate('MLA-SELECT_DASHBOARDS', ).then(function(translation) { 181 | const confirmModalOptions = { 182 | select: select, 183 | title: translation, 184 | showClose: true 185 | }; 186 | dashboardSelectModal( 187 | confirmModalOptions 188 | ); 189 | }); 190 | }; 191 | vm.selectSavedSearch = function () { 192 | function select(savedSearch) { 193 | if (!~vm.input.linkSavedSearches.map(item => item.id).indexOf(savedSearch.id)) { 194 | vm.savedSearches.push(savedSearch); 195 | vm.input.linkSavedSearches = vm.savedSearches.map(item => ({ 196 | id : item.id, 197 | title : item.title 198 | })); 199 | } 200 | } 201 | $translate('MLA-SELECT_SAVED_SEARCH', ).then(function(translation) { 202 | const confirmModalOptions = { 203 | select: select, 204 | title: translation, 205 | showClose: true 206 | }; 207 | savedSearchSelectModal( 208 | confirmModalOptions 209 | ); 210 | }); 211 | }; 212 | vm.init = function () { 213 | setKibanaUrl(); 214 | let alertId = $routeParams.alertId; 215 | if (alertId) { 216 | AlertService.search(alertId, function(res) { 217 | setInput(res["data"]); 218 | }, function(res) { 219 | vm.existsAlert = false; 220 | $translate('MLA-ALERT_NOT_EXIST', {"alertId": alertId}).then(function(translation) { 221 | notify.error(translation); 222 | }); 223 | }); 224 | } 225 | MlJobService.searchList(function (res) { 226 | $scope.mlJobs = res["data"]["jobs"]; 227 | $scope.mlJobsCandidates = $scope.mlJobs; 228 | $scope.loaded = true; 229 | }, function (error) { 230 | console.error(error); 231 | }); 232 | } 233 | 234 | function updateJobList(inputStr) { 235 | if (!inputStr || inputStr === "") { 236 | $scope.mlJobsCandidates = $scope.mlJobs; 237 | } else { 238 | $scope.mlJobsCandidates = $scope.mlJobs.filter(job => ~job.job_id.indexOf(vm.input.mlJobId)); 239 | } 240 | } 241 | 242 | function setKibanaUrl() { 243 | let pattern = /^(.+)app\/es_ml_alert.*/; 244 | vm.input.kibanaUrl = $location.absUrl().replace(pattern, "$1"); 245 | } 246 | 247 | function finishSearch() { 248 | isFetch = false; 249 | // Search again if job_id is changed 250 | if (isChange) { 251 | isChange = false; 252 | vm.changeJobId(); 253 | } 254 | } 255 | 256 | function getDefault(value, defaultValue) { 257 | if (typeof value === "undefined") { 258 | return defaultValue; 259 | } 260 | return value; 261 | } 262 | 263 | function setInput(data) { 264 | vm.autoSettingEnabled = false; 265 | if (!$routeParams.clone) { 266 | vm.input.alertId = data._id; 267 | vm.existsAlert = true; 268 | } 269 | vm.input.mlJobId = data.watch.metadata.job_id; 270 | vm.input.description = data.watch.metadata.description; 271 | vm.input.threshold = getDefault(data.watch.metadata.threshold, vm.input.threshold); 272 | vm.input.detectInterval = getDefault(data.watch.metadata.detect_interval, vm.input.detectInterval); 273 | vm.input.kibanaDisplayTerm = getDefault(data.watch.metadata.kibana_display_term, vm.input.kibanaDisplayTerm); 274 | vm.input.locale = getDefault(data.watch.metadata.locale, vm.input.locale); 275 | vm.input.mlProcessTime = getDefault(data.watch.metadata.ml_process_time, vm.input.mlProcessTime); 276 | vm.input.linkDashboards = getDefault(data.watch.metadata.link_dashboards, vm.input.linkDashboards); 277 | vm.input.linkSavedSearches = getDefault(data.watch.metadata.link_saved_searches, vm.input.linkSavedSearches); 278 | vm.input.kibanaUrl = getDefault(data.watch.metadata.kibana_url, vm.input.kibanaUrl); 279 | vm.input.subject = getDefault(data.watch.metadata.subject, vm.input.subject); 280 | vm.input.filterByActualValue = getDefault(data.watch.metadata.filterByActualValue, vm.input.filterByActualValue); 281 | vm.input.actualValueThreshold = getDefault(data.watch.metadata.actualValueThreshold, vm.input.actualValueThreshold); 282 | vm.input.lineNotifyAccessToken = getDefault(data.watch.metadata.line_notify_access_token, vm.input.lineNotifyAccessToken); 283 | if (data.watch.actions.notify_line) { 284 | vm.input.notifyLine = true; 285 | } 286 | let compareOptionIndex = 0; 287 | if (vm.compareOptions) { 288 | compareOptionIndex = Math.max(0, vm.compareOptions.map(option => option.compareType).indexOf(data.watch.metadata.compareOption.compareType)); 289 | } 290 | vm.input.compareOption = vm.compareOptions[compareOptionIndex]; 291 | if (data.watch.trigger.schedule.hasOwnProperty('cron')) { 292 | vm.input.scheduleKind = 'cron'; 293 | vm.input.triggerSchedule = data.watch.trigger.schedule.cron; 294 | } else if (data.watch.trigger.schedule.hasOwnProperty('interval')) { 295 | vm.input.scheduleKind = 'interval'; 296 | vm.input.triggerSchedule = data.watch.trigger.schedule.interval; 297 | } 298 | if (data.watch.actions.send_email) { 299 | vm.input.sendMail = true; 300 | vm.input.mailAddressTo = data.watch.actions.send_email.email.to.map(address => ({value:address})); 301 | if (data.watch.actions.send_email.email.cc) { 302 | vm.input.mailAddressCc = data.watch.actions.send_email.email.cc.map(address => ({value:address})); 303 | } 304 | if (data.watch.actions.send_email.email.bcc) { 305 | vm.input.mailAddressBcc = data.watch.actions.send_email.email.bcc.map(address => ({value:address})); 306 | } 307 | } else { 308 | vm.input.sendMail = false; 309 | } 310 | if (data.watch.actions.notify_slack && data.watch.actions.notify_slack.slack.message.to) { 311 | vm.input.notifySlack = true; 312 | vm.input.slackAccount = getDefault(data.watch.actions.notify_slack.slack.account, vm.input.slackAccount); 313 | vm.input.slackTo = data.watch.actions.notify_slack.slack.message.to.map(slackTarget => ({value:slackTarget})); 314 | } else { 315 | vm.input.notifySlack = false; 316 | } 317 | savedDashboards.find("").then(function(savedData) { 318 | vm.dashboards = savedData.hits.filter( 319 | hit => ~data.watch.metadata.link_dashboards.map(dashboard => dashboard.id).indexOf(hit.id) 320 | ); 321 | vm.input.linkDashboards = vm.dashboards.map(item => ({ 322 | id : item.id, 323 | title : item.title 324 | })); 325 | vm.changeJobId(); 326 | }); 327 | savedSearches.find("").then(function(savedData) { 328 | vm.savedSearches = savedData.hits.filter( 329 | hit => ~data.watch.metadata.link_saved_searches.map(savedSearch => savedSearch.id).indexOf(hit.id) 330 | ); 331 | vm.input.linkSavedSearches = vm.savedSearches.map(item => ({ 332 | id : item.id, 333 | title : item.title 334 | })); 335 | vm.changeJobId(); 336 | }); 337 | } 338 | 339 | /** 340 | * Set initial values automatically according to the ML job information 341 | * @param job ML Job information 342 | * @return initial values 343 | */ 344 | function autoSetting(job) { 345 | // Values are not overwritten when existing alert is edited. 346 | // But they should be changed if the target job is changed. 347 | if (!vm.autoSettingEnabled) { 348 | vm.autoSettingEnabled = true; 349 | return; 350 | } 351 | 352 | // kibanaDisplayTerm should be long if bucket_span is long. 353 | vm.input.kibanaDisplayTerm = AlertService.calculateKibanaDisplayTerm(job); 354 | 355 | // mlProcessTime is set according to the datafeed settings 356 | MlJobService.getDataFeed(job.job_id, function (res) { 357 | let datafeeds = res.data.datafeeds; 358 | if (datafeeds && datafeeds.length != 0) { 359 | let datafeed = datafeeds[0]; 360 | vm.input.mlProcessTime = AlertService.calculateMlProcessTime(job, datafeed); 361 | } 362 | }, function(error) { 363 | console.error(error); 364 | }); 365 | }; 366 | } -------------------------------------------------------------------------------- /public/controller/common/header.controller.js: -------------------------------------------------------------------------------- 1 | export default function HeaderController($scope, $route, $location, mlaConst) { 2 | var vm = this; 3 | let path = $location.path(); 4 | let paths = path.split('/'); 5 | vm.path = paths[1]; 6 | } -------------------------------------------------------------------------------- /public/less/main.less: -------------------------------------------------------------------------------- 1 | .container { 2 | margin-top: 30px; 3 | } 4 | 5 | #mlJobIdLabel { 6 | float: left; 7 | margin-top: 7px; 8 | } 9 | 10 | #alertId { 11 | width: 50%; 12 | } 13 | 14 | .kuiTableRow{ 15 | &.isSelected { 16 | background-color: #ddf6ff; 17 | cursor: default; 18 | } 19 | } 20 | 21 | .kuiTableRowCell { 22 | &.category { 23 | background-color: #efefef; 24 | } 25 | } 26 | 27 | .kuiTableHeaderCell { 28 | &.alertId { 29 | width: 20%; 30 | } 31 | &.description { 32 | width: 25%; 33 | } 34 | &.state { 35 | width: 10%; 36 | } 37 | &.actions { 38 | width: 140px; 39 | } 40 | &.jobId { 41 | width: 30%; 42 | } 43 | &.dashboardTitle { 44 | width: 40%; 45 | } 46 | &.deleteButton { 47 | width:60px; 48 | } 49 | } 50 | 51 | .modalBlock { 52 | padding: 10px; 53 | display: -webkit-box; 54 | display: -webkit-flex; 55 | display: -ms-flexbox; 56 | display: flex; 57 | border-bottom: 1px solid #D9D9D9; 58 | } 59 | 60 | .modalEdit { 61 | display: -webkit-box; 62 | display: -webkit-flex; 63 | display: -ms-flexbox; 64 | display: flex; 65 | } 66 | 67 | .modalEditBlock { 68 | padding: 10px; 69 | border-bottom: 1px solid #D9D9D9; 70 | } 71 | 72 | .modalButton { 73 | padding: 0px 8px 0px 4px; 74 | width: 120px; 75 | } 76 | 77 | .modalMessage { 78 | padding: 0px 4px 0px 4px; 79 | width: 330px; 80 | } 81 | 82 | .description { 83 | white-space: normal; 84 | } 85 | 86 | .sevierity-level { 87 | border: 1px solid transparent; 88 | padding: 1px 5px; 89 | margin: 0px 5px; 90 | line-height: 1.5; 91 | border-radius: 3px; 92 | background-color: #E4E4E4; 93 | } 94 | 95 | .sevierity-level:hover { 96 | color: #ffffff; 97 | background-color: #444444; 98 | } 99 | 100 | .multi-line { 101 | padding: 2px 8px 2px 8px; 102 | } 103 | 104 | .multi-line-title { 105 | padding: 7px 8px 2px 8px; 106 | } 107 | 108 | .form-control-inline { 109 | height: 32px; 110 | padding: 5px 15px; 111 | font-size: 14px; 112 | line-height: 1.42857143; 113 | color: #2D2D2D; 114 | background-color: #ffffff; 115 | background-image: none; 116 | border: 1px solid #D9D9D9; 117 | border-radius: 4px; 118 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); 119 | transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; 120 | } 121 | .form-control-inline:focus { 122 | border-color: #0079a5; 123 | outline: 0; 124 | box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(0, 121, 165, 0.6); 125 | } 126 | .form-control-inline::-moz-placeholder { 127 | color: #acb6c0; 128 | opacity: 1; 129 | } 130 | .form-control-inline:-ms-input-placeholder { 131 | color: #acb6c0; 132 | } 133 | .form-control-inline::-webkit-input-placeholder { 134 | color: #acb6c0; 135 | } 136 | .form-control-inline::-ms-expand { 137 | border: 0; 138 | background-color: transparent; 139 | } 140 | .form-control-inline[disabled], 141 | .form-control-inline[readonly], 142 | fieldset[disabled] .form-control { 143 | background-color: #D9D9D9; 144 | opacity: 1; 145 | } 146 | .form-control-inline[disabled], 147 | fieldset[disabled] .form-control { 148 | cursor: not-allowed; 149 | } 150 | .inline-group { 151 | padding: 1px 0px; 152 | } 153 | .sub-form { 154 | padding-left: 1em; 155 | } 156 | .multi-line-paragraph { 157 | white-space: pre-wrap; 158 | } 159 | 160 | /* From X-Pack Machine Learning */ 161 | .icon-severity-critical { 162 | color: #fe5050; 163 | } 164 | .icon-severity-major { 165 | color: #fba740; 166 | } 167 | .icon-severity-minor { 168 | color: #fbfb49; 169 | } 170 | .icon-severity-warning { 171 | color: #8bc8fb; 172 | } 173 | -------------------------------------------------------------------------------- /public/modules/modals/alertBulkEditModal.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {{title}} 5 |
6 |
8 |
9 |
10 |
11 |
12 | 15 |
16 |
17 | {{deleteMessage}} 18 |
19 |
20 |
21 |
22 | 25 |
26 |
27 | {{activateMessage}} 28 |
29 |
30 |
31 |
32 | 35 |
36 |
37 | {{deactivateMessage}} 38 |
39 |
40 |
41 |
42 |
43 | 44 |
45 |
46 | 47 | {{'MLA-UPDATE_MAIL_ADDRESS'|translate}} 48 |
49 |
50 | 51 |
52 | 54 | 58 |
59 | 63 |
64 |
65 | 66 |
67 | 69 | 73 |
74 | 78 |
79 |
80 | 81 |
82 | 84 | 88 |
89 | 93 |
94 |
95 | 96 | {{'MLA-UPDATE_SLACK'|translate}} 97 |
98 |
99 |
100 | 102 | 106 |
107 | 111 |
112 |
113 | 114 | {{'MLA-UPDATE_LINE_NOTIFY'|translate}} 115 |
116 |
117 | 118 |
119 |
120 | 121 | {{'MLA-UPDATE_DASHBOARDS'|translate}} 122 | 123 | 124 | 125 | 128 | 131 | 132 | 133 | 134 | 135 | 136 | 141 | 146 | 153 | 154 | 155 |
126 | Title 127 | 129 | Description 130 |
137 |
138 | {{dashboard.title}} 139 |
140 |
142 |
143 | {{dashboard.description}} 144 |
145 |
147 |
148 | 151 |
152 |
156 |
157 | 160 |
161 |
162 |
163 |
164 | 167 |
168 |
169 |
170 |
171 |
172 | 173 |
174 | 177 |
178 |
-------------------------------------------------------------------------------- /public/modules/modals/alertBulkEditModal.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | import { noop } from 'lodash'; 3 | import { uiModules } from 'ui/modules'; 4 | import template from './alertBulkEditModal.html'; 5 | import { ModalOverlay } from './modal_overlay'; 6 | 7 | const module = uiModules.get('kibana'); 8 | 9 | export const ConfirmationButtonTypes = { 10 | CONFIRM: 'Confirm', 11 | CANCEL: 'Cancel' 12 | }; 13 | 14 | /** 15 | * @typedef {Object} ConfirmModalOptions 16 | * @property {String} confirmButtonText 17 | * @property {String=} cancelButtonText 18 | * @property {function} onConfirm 19 | * @property {function=} onCancel 20 | * @property {String=} title - If given, shows a title on the confirm modal. A title must be given if 21 | * showClose is true, for aesthetic reasons. 22 | * @property {Boolean=} showClose - If true, shows an [x] icon close button which by default is a noop 23 | * @property {function=} onClose - Custom close button to call if showClose is true. If not supplied 24 | * but showClose is true, the function defaults to onCancel. 25 | */ 26 | 27 | module.factory('alertBulkEditModal', function ($rootScope, $compile, $translate, dashboardSelectModal) { 28 | let modalPopover; 29 | const confirmQueue = []; 30 | 31 | /** 32 | * @param {String} message - the message to show in the body of the confirmation dialog. 33 | * @param {ConfirmModalOptions} - Options to further customize the dialog. 34 | */ 35 | return function alertBulkEditModal(customOptions) { 36 | const defaultOptions = { 37 | onCancel: noop, 38 | cancelButtonText: 'Cancel', 39 | showClose: false, 40 | defaultFocusedButton: ConfirmationButtonTypes.CANCEL, 41 | deleteButtonText: 'Delete', 42 | activateButtonText: 'Activate', 43 | deactivateButtonText: 'Deactivate', 44 | updateButtonText: 'Bulk Update', 45 | deleteMessage: 'Delete selected alerts', 46 | activateMessage: 'Activate selected alerts', 47 | deactivateMessage: 'Deactivate selected alerts', 48 | updateMessage: 'Bulk update alert settings' 49 | }; 50 | 51 | if (customOptions.showClose === true && !customOptions.title) { 52 | throw new Error('A title must be supplied when a close icon is shown'); 53 | } 54 | 55 | if (!customOptions.onDelete || !customOptions.onActivate || !customOptions.onDeactivate || !customOptions.onBulkUpdate) { 56 | throw new Error('Please specify onDelete, onActivate onDeactivate, and onBulkUpdate action'); 57 | } 58 | 59 | const options = Object.assign(defaultOptions, customOptions); 60 | 61 | // Special handling for onClose - if no specific callback was supplied, default to the 62 | // onCancel callback. 63 | options.onClose = customOptions.onClose || options.onCancel; 64 | 65 | const confirmScope = $rootScope.$new(); 66 | 67 | confirmScope.cancelButtonText = options.cancelButtonText; 68 | confirmScope.deleteButtonText = options.deleteButtonText; 69 | confirmScope.activateButtonText = options.activateButtonText; 70 | confirmScope.deactivateButtonText = options.deactivateButtonText; 71 | confirmScope.updateButtonText = options.updateButtonText; 72 | confirmScope.deleteMessage = options.deleteMessage; 73 | confirmScope.activateMessage = options.activateMessage; 74 | confirmScope.deactivateMessage = options.deactivateMessage; 75 | confirmScope.updateMessage = options.updateMessage; 76 | confirmScope.title = options.title; 77 | confirmScope.input = { 78 | mailAddressTo: [ 79 | {value: ''} 80 | ], 81 | mailAddressCc: [], 82 | mailAddressBcc: [], 83 | linkDashboards: [], 84 | editMail: false, 85 | slackTo: [ 86 | {value: ''} 87 | ], 88 | editSlack: false, 89 | editLine: false, 90 | lineNotifyAccessToken: "", 91 | editDashboard: false 92 | }; 93 | confirmScope.dashboards = []; 94 | confirmScope.showClose = options.showClose; 95 | confirmScope.onDelete = () => { 96 | destroy(); 97 | options.onDelete(); 98 | }; 99 | confirmScope.onActivate = () => { 100 | destroy(); 101 | options.onActivate(); 102 | }; 103 | confirmScope.onDeactivate = () => { 104 | destroy(); 105 | options.onDeactivate(); 106 | }; 107 | confirmScope.onBulkUpdate = () => { 108 | destroy(); 109 | options.onBulkUpdate(confirmScope.input); 110 | }; 111 | confirmScope.onCancel = () => { 112 | destroy(); 113 | options.onCancel(); 114 | }; 115 | confirmScope.onClose = () => { 116 | destroy(); 117 | options.onClose(); 118 | }; 119 | confirmScope.addTo = function() { 120 | confirmScope.input.mailAddressTo.push({value: ''}); 121 | }; 122 | confirmScope.deleteTo = function(index) { 123 | confirmScope.input.mailAddressTo.splice(index, 1); 124 | }; 125 | confirmScope.addCc = function() { 126 | confirmScope.input.mailAddressCc.push({value: ''}); 127 | }; 128 | confirmScope.deleteCc = function(index) { 129 | confirmScope.input.mailAddressCc.splice(index, 1); 130 | }; 131 | confirmScope.addBcc = function() { 132 | confirmScope.input.mailAddressBcc.push({value: ''}); 133 | }; 134 | confirmScope.deleteBcc = function(index) { 135 | confirmScope.input.mailAddressBcc.splice(index, 1); 136 | }; 137 | confirmScope.addSlackTo = function() { 138 | confirmScope.input.slackTo.push({value: ''}); 139 | }; 140 | confirmScope.deleteSlackTo = function(index) { 141 | confirmScope.input.slackTo.splice(index, 1); 142 | }; 143 | confirmScope.toggleEditMail = function(index) { 144 | confirmScope.input.editMail = !confirmScope.input.editMail; 145 | }; 146 | confirmScope.toggleEditSlack = function(index) { 147 | confirmScope.input.editSlack = !confirmScope.input.editSlack; 148 | }; 149 | confirmScope.toggleEditLine = function(index) { 150 | confirmScope.input.editLine = !confirmScope.input.editLine; 151 | }; 152 | confirmScope.toggleEditDashboard = function(index) { 153 | confirmScope.input.editDashboard = !confirmScope.input.editDashboard; 154 | }; 155 | confirmScope.removeDashboard = function(index) { 156 | confirmScope.dashboards.splice(index, 1); 157 | confirmScope.input.linkDashboards = confirmScope.dashboards.map(item => ({ 158 | id : item.id, 159 | title : item.title 160 | })); 161 | }; 162 | confirmScope.selectDashboard = function () { 163 | function select(dashboard) { 164 | if (!~confirmScope.input.linkDashboards.map(item => item.id).indexOf(dashboard.id)) { 165 | confirmScope.dashboards.push(dashboard); 166 | confirmScope.input.linkDashboards = confirmScope.dashboards.map(item => ({ 167 | id : item.id, 168 | title : item.title 169 | })); 170 | } 171 | } 172 | $translate('MLA-SELECT_DASHBOARDS', ).then(function(translation) { 173 | const confirmModalOptions = { 174 | select: select, 175 | title: translation, 176 | showClose: true 177 | }; 178 | dashboardSelectModal( 179 | confirmModalOptions 180 | ); 181 | }); 182 | }; 183 | 184 | function showModal(confirmScope) { 185 | const modalInstance = $compile(template)(confirmScope); 186 | modalPopover = new ModalOverlay(modalInstance); 187 | angular.element(document.body).on('keydown', (event) => { 188 | if (event.keyCode === 27) { 189 | confirmScope.onCancel(); 190 | } 191 | }); 192 | 193 | switch (options.defaultFocusedButton) { 194 | case ConfirmationButtonTypes.CONFIRM: 195 | modalInstance.find('[data-test-subj=alertBulkEditModalConfirmButton]').focus(); 196 | break; 197 | case ConfirmationButtonTypes.CANCEL: 198 | modalInstance.find('[data-test-subj=alertBulkEditModalCancelButton]').focus(); 199 | break; 200 | default: 201 | } 202 | } 203 | 204 | if (modalPopover) { 205 | confirmQueue.unshift(confirmScope); 206 | } else { 207 | showModal(confirmScope); 208 | } 209 | 210 | function destroy() { 211 | modalPopover.destroy(); 212 | modalPopover = undefined; 213 | angular.element(document.body).off('keydown'); 214 | confirmScope.$destroy(); 215 | 216 | if (confirmQueue.length > 0) { 217 | showModal(confirmQueue.pop()); 218 | } 219 | } 220 | }; 221 | }); -------------------------------------------------------------------------------- /public/modules/modals/dashboardSelectModal.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {{title}} 5 |
6 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | 15 | 17 |
18 |
19 |
20 | 21 | 22 | 23 | 26 | 29 | 30 | 31 | 32 | 33 | 38 | 43 | 44 | 45 |
24 | Title 25 | 27 | Description 28 |
34 |
35 | {{dashboard.title}} 36 |
37 |
39 |
40 | {{dashboard.description}} 41 |
42 |
46 | 50 |
51 |
52 | No dashboards matched your search. 53 |
54 |
55 |
56 |
57 | 58 |
59 | 62 |
63 |
-------------------------------------------------------------------------------- /public/modules/modals/dashboardSelectModal.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | import { noop } from 'lodash'; 3 | import { uiModules } from 'ui/modules'; 4 | import template from './dashboardSelectModal.html'; 5 | import { ModalOverlay } from './modal_overlay'; 6 | 7 | const module = uiModules.get('kibana'); 8 | 9 | export const ConfirmationButtonTypes = { 10 | CONFIRM: 'Confirm', 11 | CANCEL: 'Cancel' 12 | }; 13 | 14 | /** 15 | * @typedef {Object} ConfirmModalOptions 16 | * @property {String} confirmButtonText 17 | * @property {String=} cancelButtonText 18 | * @property {function} onConfirm 19 | * @property {function=} onCancel 20 | * @property {String=} title - If given, shows a title on the confirm modal. A title must be given if 21 | * showClose is true, for aesthetic reasons. 22 | * @property {Boolean=} showClose - If true, shows an [x] icon close button which by default is a noop 23 | * @property {function=} onClose - Custom close button to call if showClose is true. If not supplied 24 | * but showClose is true, the function defaults to onCancel. 25 | */ 26 | 27 | module.factory('dashboardSelectModal', function ($rootScope, $compile, savedDashboards) { 28 | let modalPopover; 29 | const confirmQueue = []; 30 | 31 | /** 32 | * @param {String} message - the message to show in the body of the confirmation dialog. 33 | * @param {ConfirmModalOptions} - Options to further customize the dialog. 34 | */ 35 | return function dashboardSelectModal(customOptions) { 36 | const defaultOptions = { 37 | onCancel: noop, 38 | cancelButtonText: 'Cancel', 39 | showClose: false, 40 | defaultFocusedButton: ConfirmationButtonTypes.CANCEL 41 | }; 42 | 43 | if (customOptions.showClose === true && !customOptions.title) { 44 | throw new Error('A title must be supplied when a close icon is shown'); 45 | } 46 | 47 | if (!customOptions.select) { 48 | throw new Error('Please specify select action'); 49 | } 50 | 51 | const options = Object.assign(defaultOptions, customOptions); 52 | 53 | // Special handling for onClose - if no specific callback was supplied, default to the 54 | // onCancel callback. 55 | options.onClose = customOptions.onClose || options.onCancel; 56 | 57 | const confirmScope = $rootScope.$new(); 58 | 59 | confirmScope.dashboards = []; 60 | confirmScope.dashboardsFilter = ""; 61 | confirmScope.cancelButtonText = options.cancelButtonText; 62 | confirmScope.title = options.title; 63 | confirmScope.showClose = options.showClose; 64 | confirmScope.searchDashboards = () => { 65 | savedDashboards.find(confirmScope.dashboardsFilter).then(function(data) { 66 | confirmScope.dashboards = data.hits; 67 | }); 68 | }; 69 | confirmScope.select = (dashboard) => { 70 | destroy(); 71 | options.select(dashboard); 72 | }; 73 | confirmScope.onCancel = () => { 74 | destroy(); 75 | options.onCancel(); 76 | }; 77 | confirmScope.onClose = () => { 78 | destroy(); 79 | options.onClose(); 80 | }; 81 | 82 | function showModal(confirmScope) { 83 | const modalInstance = $compile(template)(confirmScope); 84 | modalPopover = new ModalOverlay(modalInstance); 85 | angular.element(document.body).on('keydown', (event) => { 86 | if (event.keyCode === 27) { 87 | confirmScope.onCancel(); 88 | } 89 | }); 90 | 91 | switch (options.defaultFocusedButton) { 92 | case ConfirmationButtonTypes.CONFIRM: 93 | modalInstance.find('[data-test-subj=dashboardSelectModalConfirmButton]').focus(); 94 | break; 95 | case ConfirmationButtonTypes.CANCEL: 96 | modalInstance.find('[data-test-subj=dashboardSelectModalCancelButton]').focus(); 97 | break; 98 | default: 99 | } 100 | confirmScope.searchDashboards(); 101 | } 102 | 103 | if (modalPopover) { 104 | confirmQueue.unshift(confirmScope); 105 | } else { 106 | showModal(confirmScope); 107 | } 108 | 109 | function destroy() { 110 | modalPopover.destroy(); 111 | modalPopover = undefined; 112 | angular.element(document.body).off('keydown'); 113 | confirmScope.$destroy(); 114 | 115 | if (confirmQueue.length > 0) { 116 | showModal(confirmQueue.pop()); 117 | } 118 | } 119 | }; 120 | }); -------------------------------------------------------------------------------- /public/modules/modals/modal_overlay.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /public/modules/modals/modal_overlay.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | import modalOverlayTemplate from './modal_overlay.html'; 3 | 4 | /** 5 | * Appends the modal to the dom on instantiation, and removes it when destroy is called. 6 | */ 7 | export class ModalOverlay { 8 | constructor(modalElement) { 9 | this.overlayElement = angular.element(modalOverlayTemplate); 10 | this.overlayElement.append(modalElement); 11 | 12 | angular.element(document.body).append(this.overlayElement); 13 | } 14 | 15 | /** 16 | * Removes the overlay and modal from the dom. 17 | */ 18 | destroy() { 19 | this.overlayElement.remove(); 20 | } 21 | } -------------------------------------------------------------------------------- /public/modules/modals/savedSearchSelectModal.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {{title}} 5 |
6 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | 15 | 17 |
18 |
19 |
20 | 21 | 22 | 23 | 26 | 29 | 30 | 31 | 32 | 33 | 38 | 43 | 44 | 45 |
24 | Title 25 | 27 | Description 28 |
34 |
35 | {{savedSearch.title}} 36 |
37 |
39 |
40 | {{savedSearch.description}} 41 |
42 |
46 | 50 |
51 |
52 | No savedSearches matched your search. 53 |
54 |
55 |
56 |
57 | 58 |
59 | 62 |
63 |
-------------------------------------------------------------------------------- /public/modules/modals/savedSearchSelectModal.js: -------------------------------------------------------------------------------- 1 | import angular from 'angular'; 2 | import { noop } from 'lodash'; 3 | import { uiModules } from 'ui/modules'; 4 | import template from './savedSearchSelectModal.html'; 5 | import { ModalOverlay } from './modal_overlay'; 6 | 7 | const module = uiModules.get('kibana'); 8 | 9 | export const ConfirmationButtonTypes = { 10 | CONFIRM: 'Confirm', 11 | CANCEL: 'Cancel' 12 | }; 13 | 14 | /** 15 | * @typedef {Object} ConfirmModalOptions 16 | * @property {String} confirmButtonText 17 | * @property {String=} cancelButtonText 18 | * @property {function} onConfirm 19 | * @property {function=} onCancel 20 | * @property {String=} title - If given, shows a title on the confirm modal. A title must be given if 21 | * showClose is true, for aesthetic reasons. 22 | * @property {Boolean=} showClose - If true, shows an [x] icon close button which by default is a noop 23 | * @property {function=} onClose - Custom close button to call if showClose is true. If not supplied 24 | * but showClose is true, the function defaults to onCancel. 25 | */ 26 | 27 | module.factory('savedSearchSelectModal', function ($rootScope, $compile, savedSearches) { 28 | let modalPopover; 29 | const confirmQueue = []; 30 | 31 | /** 32 | * @param {String} message - the message to show in the body of the confirmation dialog. 33 | * @param {ConfirmModalOptions} - Options to further customize the dialog. 34 | */ 35 | return function savedSearchSelectModal(customOptions) { 36 | const defaultOptions = { 37 | onCancel: noop, 38 | cancelButtonText: 'Cancel', 39 | showClose: false, 40 | defaultFocusedButton: ConfirmationButtonTypes.CANCEL 41 | }; 42 | 43 | if (customOptions.showClose === true && !customOptions.title) { 44 | throw new Error('A title must be supplied when a close icon is shown'); 45 | } 46 | 47 | if (!customOptions.select) { 48 | throw new Error('Please specify select action'); 49 | } 50 | 51 | const options = Object.assign(defaultOptions, customOptions); 52 | 53 | // Special handling for onClose - if no specific callback was supplied, default to the 54 | // onCancel callback. 55 | options.onClose = customOptions.onClose || options.onCancel; 56 | 57 | const confirmScope = $rootScope.$new(); 58 | 59 | confirmScope.savedSearches = []; 60 | confirmScope.savedSearchesFilter = ""; 61 | confirmScope.cancelButtonText = options.cancelButtonText; 62 | confirmScope.title = options.title; 63 | confirmScope.showClose = options.showClose; 64 | confirmScope.searchSavedSearches = () => { 65 | savedSearches.find(confirmScope.savedSearchesFilter).then(function(data) { 66 | confirmScope.savedSearches = data.hits; 67 | }); 68 | }; 69 | confirmScope.select = (savedSearch) => { 70 | destroy(); 71 | options.select(savedSearch); 72 | }; 73 | confirmScope.onCancel = () => { 74 | destroy(); 75 | options.onCancel(); 76 | }; 77 | confirmScope.onClose = () => { 78 | destroy(); 79 | options.onClose(); 80 | }; 81 | 82 | function showModal(confirmScope) { 83 | const modalInstance = $compile(template)(confirmScope); 84 | modalPopover = new ModalOverlay(modalInstance); 85 | angular.element(document.body).on('keydown', (event) => { 86 | if (event.keyCode === 27) { 87 | confirmScope.onCancel(); 88 | } 89 | }); 90 | 91 | switch (options.defaultFocusedButton) { 92 | case ConfirmationButtonTypes.CONFIRM: 93 | modalInstance.find('[data-test-subj=savedSearchSelectModalConfirmButton]').focus(); 94 | break; 95 | case ConfirmationButtonTypes.CANCEL: 96 | modalInstance.find('[data-test-subj=savedSearchSelectModalCancelButton]').focus(); 97 | break; 98 | default: 99 | } 100 | confirmScope.searchSavedSearches(); 101 | } 102 | 103 | if (modalPopover) { 104 | confirmQueue.unshift(confirmScope); 105 | } else { 106 | showModal(confirmScope); 107 | } 108 | 109 | function destroy() { 110 | modalPopover.destroy(); 111 | modalPopover = undefined; 112 | angular.element(document.body).off('keydown'); 113 | confirmScope.$destroy(); 114 | 115 | if (confirmQueue.length > 0) { 116 | showModal(confirmQueue.pop()); 117 | } 118 | } 119 | }; 120 | }); -------------------------------------------------------------------------------- /public/script/script.js: -------------------------------------------------------------------------------- 1 | /** \ and ` must be escaped. \ -> \\, ` -> \` */ 2 | export const script=`/** 3 | * Filter for max, high_mean, min and low_mean 4 | */ 5 | String makeFilter(def hit) { 6 | def rand = new Random(); 7 | def vals = hit._source; 8 | def funcName = vals.function; 9 | def fieldName = vals.field_name; 10 | def fieldValue = vals.actual[0]; 11 | def valFilterQuery = ''; 12 | // max/high_mean 13 | // filter values greater than or equal to the actual value 14 | if (funcName == 'high_mean' || funcName == 'max') { 15 | def filterAlias = fieldName + '%20%E2%89%A5%20' + fieldValue; 16 | def rangeQuery = 'query:(range:(' + fieldName + ':(gte:' + fieldValue + ')))'; 17 | valFilterQuery = '(meta:(alias:\\'' + filterAlias + '\\',disabled:!f,index:i' + rand.nextLong() + ',key:query,negate:!f,type:custom),' + rangeQuery + ')'; 18 | } 19 | // min/low_mean 20 | // filter values lower than or equal to the actual value 21 | if (funcName == 'low_mean' || funcName == 'min') { 22 | def filterAlias = fieldName + '%20%E2%89%A4%20' + fieldValue; 23 | def rangeQuery = 'query:(range:(' + fieldName + ':(lte:' + fieldValue + ')))'; 24 | valFilterQuery = '(meta:(alias:\\'' + filterAlias + '\\',disabled:!f,index:i' + rand.nextLong() + ',key:query,negate:!f,type:custom),' + rangeQuery + ')'; 25 | } 26 | return valFilterQuery; 27 | } 28 | def rand = new Random(); 29 | def dateForJpn = LocalDateTime.ofInstant(Instant.ofEpochMilli(ctx.execution_time.millis), ZoneId.of(ctx.metadata.locale)); 30 | def dateForJpnStr = dateForJpn.format(DateTimeFormatter.ofPattern(ctx.metadata.date_format)); 31 | def firstHit = ctx.payload.hits.hits[0]; 32 | def longBeforeDateMilli = firstHit._source.timestamp - ctx.metadata.kibana_display_term * 1000 - firstHit._source.bucket_span * 1000 * 100 - 1000 * 60 * 60 * 24; 33 | def beforeDateMilli = firstHit._source.timestamp - ctx.metadata.kibana_display_term * 1000; 34 | def afterDateMilli = firstHit._source.timestamp + ctx.metadata.kibana_display_term * 1000 + firstHit._source.bucket_span * 1000; 35 | def longBeforeDate = Instant.ofEpochMilli(longBeforeDateMilli); 36 | def beforeDate = Instant.ofEpochMilli(beforeDateMilli); 37 | def afterDate = Instant.ofEpochMilli(afterDateMilli); 38 | def url = ctx.metadata.kibana_url + 'app/ml#/explorer?_g=(ml:(jobIds:!(\\'' + ctx.metadata.job_id + '\\')),time:(from:'+ ctx.metadata.quate + beforeDate + ctx.metadata.quate + ',mode:absolute,to:' + ctx.metadata.quate + afterDate + ctx.metadata.quate + '))'; 39 | def link = 'Anomaly Explorer'; 40 | def smvUrl = ctx.metadata.kibana_url + 'app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(\\'' + ctx.metadata.job_id + '\\')),time:(from:'+ ctx.metadata.quate + longBeforeDate + ctx.metadata.quate + ',mode:absolute,to:' + ctx.metadata.quate + afterDate + ctx.metadata.quate + '))'; 41 | def smvLink = 'Single Metric Viewer'; 42 | def message = ctx.metadata.subject; 43 | message += '
'; 44 | message += '
Alert ID: ' + ctx.watch_id; 45 | message += '
' + ctx.metadata.description; 46 | message += '
'; 47 | message += '
' + link; 48 | message += '
' + smvLink; 49 | message += '
Alert Triggered Time: ' + dateForJpnStr; 50 | message += '
ML JobID: ' + ctx.metadata.job_id; 51 | if (ctx.metadata.link_dashboards.length != 0) { 52 | message += '
Dashboard: '; 53 | for (def dashboard : ctx.metadata.link_dashboards) { 54 | def dashboardUrl = ctx.metadata.kibana_url + 'app/kibana#/dashboard/' + dashboard.id + '?_g=(time:(from:'+ ctx.metadata.quate + beforeDate + ctx.metadata.quate + ',mode:absolute,to:' + ctx.metadata.quate + afterDate + ctx.metadata.quate + '))'; 55 | def dashboardLink = '' + dashboard.title + ''; 56 | message += '
  ' + dashboardLink; 57 | } 58 | } 59 | message += '

Detail'; 60 | for (def hit : ctx.payload.hits.hits) { 61 | def anomalyDate = LocalDateTime.ofInstant(Instant.ofEpochMilli(hit._source.timestamp), ZoneId.of(ctx.metadata.locale)); 62 | def anomalyDateStr = anomalyDate.format(DateTimeFormatter.ofPattern(ctx.metadata.date_format)); 63 | message += '
  timestamp: ' + anomalyDateStr; 64 | def partitionName = hit._source.partition_field_name; 65 | String partitionValue = hit._source.partition_field_value; 66 | def detectorIndex = 0; 67 | def partitionFilter = ''; 68 | double score = hit._source.record_score; 69 | message += '
  function: ' + hit._source.function; 70 | message += '
  description: ' + hit._source.function_description; 71 | message += '
  field name: ' + hit._source.field_name; 72 | message += '
  anomaly score: ' + score; 73 | def typical = hit._source.typical[0]; 74 | def actual = hit._source.actual[0]; 75 | message += '
  actual: ' + actual; 76 | message += '
  typical: ' + typical; 77 | message += '
  probability: ' + hit._source.probability; 78 | if (hit._source.detector_index != null) { 79 | detectorIndex = hit._source.detector_index; 80 | } 81 | if (partitionName != null) { 82 | String escapedPartitionValue = partitionValue.replace('!', '!!'); 83 | escapedPartitionValue = escapedPartitionValue.replace('\\'', '!\\''); 84 | escapedPartitionValue = escapedPartitionValue.replace('%', '%25'); 85 | escapedPartitionValue = escapedPartitionValue.replace('"', '%22'); 86 | escapedPartitionValue = escapedPartitionValue.replace('#', '%23'); 87 | escapedPartitionValue = escapedPartitionValue.replace('&', '%26'); 88 | escapedPartitionValue = escapedPartitionValue.replace('+', '%2B'); 89 | escapedPartitionValue = escapedPartitionValue.replace(' ', '%20'); 90 | escapedPartitionValue = escapedPartitionValue.replace('\`', '%60'); 91 | partitionFilter = '(meta:(alias:!n,disabled:!f,index:i' + rand.nextLong() + ',key:' + partitionName + ',negate:!f,params:(query:\\'' + escapedPartitionValue + '\\',type:phrase),type:phrase,value:\\'' + escapedPartitionValue + '\\'),query:(match:(' + partitionName + ':(query:\\'' + escapedPartitionValue + '\\',type:phrase))))'; 92 | String partitionedSmvFilter = '&_a=(mlTimeSeriesExplorer:(detectorIndex:' + detectorIndex + ',entities:(' + partitionName + ':\\'' + escapedPartitionValue + '\\')))'; 93 | String partitionedSmvUrl = ctx.metadata.kibana_url + 'app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(\\'' + ctx.metadata.job_id + '\\')),time:(from:'+ ctx.metadata.quate + longBeforeDate + ctx.metadata.quate + ',mode:absolute,to:' + ctx.metadata.quate + afterDate + ctx.metadata.quate + '))' + partitionedSmvFilter; 94 | String partitionedSmvLink = '' + 'Single Metric Viewer'; 95 | message += '
  partitions:
    partition name: ' + partitionName + '
    partition value: ' + partitionValue + '
    ' + partitionedSmvLink; 96 | def partitionQuery = '&_a=(filters:!(' + partitionFilter + '))'; 97 | for (def dashboard : ctx.metadata.link_dashboards) { 98 | def dashboardUrl = ctx.metadata.kibana_url + 'app/kibana#/dashboard/' + dashboard.id + '?_g=(time:(from:'+ ctx.metadata.quate + beforeDate + ctx.metadata.quate + ',mode:absolute,to:' + ctx.metadata.quate + afterDate + ctx.metadata.quate + '))' + partitionQuery; 99 | def dashboardLink = '' + dashboard.title + ''; 100 | message += '
    ' + dashboardLink; 101 | } 102 | } 103 | 104 | // Saved Search URL 105 | if (ctx.metadata.link_saved_searches.length == 1) { 106 | def savedSearchId = ctx.metadata.link_saved_searches[0].id; 107 | def startAnomalyDate = Instant.ofEpochMilli(hit._source.timestamp); 108 | def endAnomalyDateMilli =hit._source.timestamp + hit._source.bucket_span * 1000; 109 | def endAnomalyDate = Instant.ofEpochMilli(endAnomalyDateMilli); 110 | def timeQuery = '_g=(time:(from:'+ ctx.metadata.quate + startAnomalyDate + ctx.metadata.quate + ',mode:absolute,to:' + ctx.metadata.quate + endAnomalyDate + ctx.metadata.quate + '))'; 111 | def targetDiscoveryUrl = ''; 112 | def discoveryUrl = ctx.metadata.kibana_url + 'app/kibana#/discover/' + savedSearchId + '?'; 113 | if (partitionName != null) { 114 | targetDiscoveryUrl = discoveryUrl + timeQuery + '&_a=(filters:!(' + partitionFilter + '))'; 115 | } else { 116 | targetDiscoveryUrl = discoveryUrl + timeQuery; 117 | } 118 | message += '
  Open Saved Search'; 119 | // Saved Search filtered by value 120 | def valQuery = makeFilter(hit); 121 | if (valQuery != '') { 122 | def queryFilterQuery = ''; 123 | if (partitionName != null) { 124 | queryFilterQuery = '&_a=(filters:!(' + partitionFilter + ',' + valQuery + '))'; 125 | } else { 126 | queryFilterQuery = '&_a=(filters:!(' + valQuery + '))'; 127 | } 128 | def targetFilterDiscoveryUrl = discoveryUrl + timeQuery + queryFilterQuery; 129 | message += '
  Open Saved Search filtered by value'; 130 | } 131 | } 132 | message += '
'; 133 | } 134 | return [ 'message' : message ];`; 135 | export const scriptSlack=`// Non-ASCII character cannot be used in slack integration(6.1.0) 136 | /** 137 | * Filter for max, high_mean, min and low_mean 138 | */ 139 | String makeFilter(def hit) { 140 | def rand = new Random(); 141 | def vals = hit._source; 142 | def funcName = vals.function; 143 | def fieldName = vals.field_name; 144 | def fieldValue = vals.actual[0]; 145 | def valFilterQuery = ''; 146 | // max/high_mean 147 | // filter values greater than or equal to the actual value 148 | if (funcName == 'high_mean' || funcName == 'max') { 149 | def filterAlias = fieldName + '%20%E2%89%A5%20' + fieldValue; 150 | def rangeQuery = 'query:(range:(' + fieldName + ':(gte:' + fieldValue + ')))'; 151 | valFilterQuery = '(meta:(alias:\\'' + filterAlias + '\\',disabled:!f,index:i' + rand.nextLong() + ',key:query,negate:!f,type:custom),' + rangeQuery + ')'; 152 | } 153 | // min/low_mean 154 | // filter values lower than or equal to the actual value 155 | if (funcName == 'low_mean' || funcName == 'min') { 156 | def filterAlias = fieldName + '%20%E2%89%A4%20' + fieldValue; 157 | def rangeQuery = 'query:(range:(' + fieldName + ':(lte:' + fieldValue + ')))'; 158 | valFilterQuery = '(meta:(alias:\\'' + filterAlias + '\\',disabled:!f,index:i' + rand.nextLong() + ',key:query,negate:!f,type:custom),' + rangeQuery + ')'; 159 | } 160 | return valFilterQuery; 161 | } 162 | def rand = new Random(); 163 | def dateForJpn = LocalDateTime.ofInstant(Instant.ofEpochMilli(ctx.execution_time.millis), ZoneId.of(ctx.metadata.locale)); 164 | def dateForJpnStr = dateForJpn.format(DateTimeFormatter.ofPattern(ctx.metadata.date_format)); 165 | def firstHit = ctx.payload.hits.hits[0]; 166 | def longBeforeDateMilli = firstHit._source.timestamp - ctx.metadata.kibana_display_term * 1000 - firstHit._source.bucket_span * 1000 * 100 - 1000 * 60 * 60 * 24; 167 | def beforeDateMilli = firstHit._source.timestamp - ctx.metadata.kibana_display_term * 1000; 168 | def afterDateMilli = firstHit._source.timestamp + ctx.metadata.kibana_display_term * 1000 + firstHit._source.bucket_span * 1000; 169 | def longBeforeDate = Instant.ofEpochMilli(longBeforeDateMilli); 170 | def beforeDate = Instant.ofEpochMilli(beforeDateMilli); 171 | def afterDate = Instant.ofEpochMilli(afterDateMilli); 172 | def url = ctx.metadata.kibana_url + 'app/ml#/explorer?_g=(ml:(jobIds:!(\\'' + ctx.metadata.job_id + '\\')),time:(from:'+ ctx.metadata.quate + beforeDate + ctx.metadata.quate + ',mode:absolute,to:' + ctx.metadata.quate + afterDate + ctx.metadata.quate + '))'; 173 | def link = '<' + url + '|Anomaly Explorer>'; 174 | def smvUrl = ctx.metadata.kibana_url + 'app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(\\'' + ctx.metadata.job_id + '\\')),time:(from:'+ ctx.metadata.quate + longBeforeDate + ctx.metadata.quate + ',mode:absolute,to:' + ctx.metadata.quate + afterDate + ctx.metadata.quate + '))'; 175 | def smvLink = '<' + smvUrl + '|Single Metric Viewer>'; 176 | def severity = 'Warning'; 177 | def severityColor = '#8BC8FB'; 178 | double maxScore = 0; 179 | for (def hit : ctx.payload.hits.hits) { 180 | if (maxScore < hit._source.record_score) { 181 | maxScore = hit._source.record_score; 182 | } 183 | } 184 | if (maxScore >= 75) { 185 | severity = 'Critical'; 186 | severityColor = '#FE5050'; 187 | } else if (maxScore >= 50) { 188 | severity = 'Major'; 189 | severityColor = '#FBA740'; 190 | } else if (maxScore >= 25) { 191 | severity = 'Minor'; 192 | severityColor = '#FDEC25'; 193 | } 194 | def message = 'Alert ID: ' + ctx.watch_id; 195 | //message += ' 196 | //' + ctx.metadata.description; 197 | //message += ' 198 | message += ' 199 | ' + link; 200 | message += ' 201 | ' + smvLink; 202 | message += ' 203 | 204 | Alert Triggered Time: ' + dateForJpnStr; 205 | message += ' 206 | ML JobID: ' + ctx.metadata.job_id; 207 | if (ctx.metadata.link_dashboards.length != 0) { 208 | message += ' 209 | Dashboard:'; 210 | for (def dashboard : ctx.metadata.link_dashboards) { 211 | def dashboardUrl = ctx.metadata.kibana_url + 'app/kibana#/dashboard/' + dashboard.id + '?_g=(time:(from:'+ ctx.metadata.quate + beforeDate + ctx.metadata.quate + ',mode:absolute,to:' + ctx.metadata.quate + afterDate + ctx.metadata.quate + '))'; 212 | def dashboardLink = '<' + dashboardUrl + '|' + dashboard.title + '>'; 213 | message += ' 214 | ' + dashboardLink; 215 | } 216 | } 217 | message += ' 218 | 219 | Detail'; 220 | for (def hit : ctx.payload.hits.hits) { 221 | def anomalyDate = LocalDateTime.ofInstant(Instant.ofEpochMilli(hit._source.timestamp), ZoneId.of(ctx.metadata.locale)); 222 | def anomalyDateStr = anomalyDate.format(DateTimeFormatter.ofPattern(ctx.metadata.date_format)); 223 | message += ' 224 | timestamp: ' + anomalyDateStr; 225 | def partitionName = hit._source.partition_field_name; 226 | String partitionValue = hit._source.partition_field_value; 227 | def detectorIndex = 0; 228 | def partitionFilter = ''; 229 | double score = hit._source.record_score; 230 | message += ' 231 | function: ' + hit._source.function; 232 | message += ' 233 | description: ' + hit._source.function_description; 234 | message += ' 235 | field name: ' + hit._source.field_name; 236 | message += ' 237 | anomaly score: ' + score; 238 | def typical = hit._source.typical[0]; 239 | def actual = hit._source.actual[0]; 240 | message += ' 241 | actual: ' + actual; 242 | message += ' 243 | typical: ' + typical; 244 | message += ' 245 | probability: ' + hit._source.probability; 246 | if (hit._source.detector_index != null) { 247 | detectorIndex = hit._source.detector_index; 248 | } 249 | if (partitionName != null) { 250 | String escapedPartitionValue = partitionValue.replace('!', '!!'); 251 | escapedPartitionValue = escapedPartitionValue.replace('\\'', '!\\''); 252 | escapedPartitionValue = escapedPartitionValue.replace('%', '%25'); 253 | escapedPartitionValue = escapedPartitionValue.replace('"', '%22'); 254 | escapedPartitionValue = escapedPartitionValue.replace('#', '%23'); 255 | escapedPartitionValue = escapedPartitionValue.replace('&', '%26'); 256 | escapedPartitionValue = escapedPartitionValue.replace('+', '%2B'); 257 | escapedPartitionValue = escapedPartitionValue.replace(' ', '%20'); 258 | escapedPartitionValue = escapedPartitionValue.replace('\`', '%60'); 259 | escapedPartitionValue = escapedPartitionValue.replace('|', '%7C'); 260 | escapedPartitionValue = escapedPartitionValue.replace('>', '%3E'); 261 | escapedPartitionValue = escapedPartitionValue.replace('<', '%3C'); 262 | partitionFilter = '(meta:(alias:!n,disabled:!f,index:i' + rand.nextLong() + ',key:' + partitionName + ',negate:!f,params:(query:\\'' + escapedPartitionValue + '\\',type:phrase),type:phrase,value:\\'' + escapedPartitionValue + '\\'),query:(match:(' + partitionName + ':(query:\\'' + escapedPartitionValue + '\\',type:phrase))))'; 263 | String partitionedSmvFilter = '&_a=(mlTimeSeriesExplorer:(detectorIndex:' + detectorIndex + ',entities:(' + partitionName + ':\\'' + escapedPartitionValue + '\\')))'; 264 | String partitionedSmvUrl = ctx.metadata.kibana_url + 'app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(\\'' + ctx.metadata.job_id + '\\')),time:(from:'+ ctx.metadata.quate + longBeforeDate + ctx.metadata.quate + ',mode:absolute,to:' + ctx.metadata.quate + afterDate + ctx.metadata.quate + '))' + partitionedSmvFilter; 265 | String partitionedSmvLink = '<' + partitionedSmvUrl + '|Single Metric Viewer>'; 266 | message += ' 267 | partitions: 268 | partition name: ' + partitionName + ' 269 | partition value: ' + partitionValue + ' 270 | ' + partitionedSmvLink; 271 | def partitionQuery = '&_a=(filters:!(' + partitionFilter + '))'; 272 | for (def dashboard : ctx.metadata.link_dashboards) { 273 | def dashboardUrl = ctx.metadata.kibana_url + 'app/kibana#/dashboard/' + dashboard.id + '?_g=(time:(from:'+ ctx.metadata.quate + beforeDate + ctx.metadata.quate + ',mode:absolute,to:' + ctx.metadata.quate + afterDate + ctx.metadata.quate + '))' + partitionQuery; 274 | def dashboardLink = '<' + dashboardUrl + '|' + dashboard.title + '>'; 275 | message += ' 276 | ' + dashboardLink; 277 | } 278 | } 279 | 280 | // Saved Search URL 281 | if (ctx.metadata.link_saved_searches.length == 1) { 282 | def savedSearchId = ctx.metadata.link_saved_searches[0].id; 283 | def startAnomalyDate = Instant.ofEpochMilli(hit._source.timestamp); 284 | def endAnomalyDateMilli =hit._source.timestamp + hit._source.bucket_span * 1000; 285 | def endAnomalyDate = Instant.ofEpochMilli(endAnomalyDateMilli); 286 | def timeQuery = '_g=(time:(from:'+ ctx.metadata.quate + startAnomalyDate + ctx.metadata.quate + ',mode:absolute,to:' + ctx.metadata.quate + endAnomalyDate + ctx.metadata.quate + '))'; 287 | def targetDiscoveryUrl = ''; 288 | def discoveryUrl = ctx.metadata.kibana_url + 'app/kibana#/discover/' + savedSearchId + '?'; 289 | if (partitionName != null) { 290 | targetDiscoveryUrl = discoveryUrl + timeQuery + '&_a=(filters:!(' + partitionFilter + '))'; 291 | } else { 292 | targetDiscoveryUrl = discoveryUrl + timeQuery; 293 | } 294 | message += ' 295 | <' + targetDiscoveryUrl + '|Open Saved Search>'; 296 | // Saved Search filtered by value 297 | def valQuery = makeFilter(hit); 298 | if (valQuery != '') { 299 | def queryFilterQuery = ''; 300 | if (partitionName != null) { 301 | queryFilterQuery = '&_a=(filters:!(' + partitionFilter + ',' + valQuery + '))'; 302 | } else { 303 | queryFilterQuery = '&_a=(filters:!(' + valQuery + '))'; 304 | } 305 | def targetFilterDiscoveryUrl = discoveryUrl + timeQuery + queryFilterQuery; 306 | message += ' 307 | <' + targetFilterDiscoveryUrl + '|Open Saved Search filtered by value>'; 308 | } 309 | } 310 | message += ' 311 | '; 312 | } 313 | return [ 'message' : message , 'severity' : severity , 'severityColor' : severityColor ];`; 314 | export const scriptLine=`/** 315 | * Filter for max, high_mean, min and low_mean 316 | */ 317 | String mainTemplate = '%s 318 | %s 319 | Alert ID: %s 320 | Description: %s 321 | Alert Triggered Time: %s 322 | ML JobID: %s 323 | 324 | Detail:'; 325 | 326 | String detailTemplate = ' timestamp: %s 327 | function: %s 328 | description: %s 329 | field name: %s 330 | anomaly score: %.3f 331 | actual: %.3f 332 | typical: %.3f 333 | probability: %.6f 334 | '; 335 | 336 | String partitionTemplate = ' partitions: 337 | partition name: %s 338 | partition value: %s 339 | '; 340 | 341 | def dateForJpn = LocalDateTime.ofInstant(Instant.ofEpochMilli(ctx.execution_time.millis), ZoneId.of(ctx.metadata.locale)); 342 | def dateForJpnStr = dateForJpn.format(DateTimeFormatter.ofPattern(ctx.metadata.date_format)); 343 | def firstHit = ctx.payload.hits.hits[0]; 344 | def severity = 'Warning'; 345 | def severityColor = '#8BC8FB'; 346 | double maxScore = 0; 347 | for (def hit : ctx.payload.hits.hits) { 348 | if (maxScore < hit._source.record_score) { 349 | maxScore = hit._source.record_score; 350 | } 351 | } 352 | if (maxScore >= 75) { 353 | severity = 'Critical'; 354 | severityColor = '#FE5050'; 355 | } else if (maxScore >= 50) { 356 | severity = 'Major'; 357 | severityColor = '#FBA740'; 358 | } else if (maxScore >= 25) { 359 | severity = 'Minor'; 360 | severityColor = '#FDEC25'; 361 | } 362 | def values = new def[] {ctx.metadata.subject, severity, ctx.watch_id, ctx.metadata.description, dateForJpnStr, ctx.metadata.job_id}; 363 | def message = String.format(mainTemplate, values); 364 | 365 | for (def hit : ctx.payload.hits.hits) { 366 | message += ' 367 | '; 368 | def anomalyDate = LocalDateTime.ofInstant(Instant.ofEpochMilli(hit._source.timestamp), ZoneId.of(ctx.metadata.locale)); 369 | def anomalyDateStr = anomalyDate.format(DateTimeFormatter.ofPattern(ctx.metadata.date_format)); 370 | def partitionName = hit._source.partition_field_name; 371 | String partitionValue = hit._source.partition_field_value; 372 | def detailValues = new def[] {anomalyDateStr, hit._source.function, hit._source.function_description, hit._source.field_name, hit._source.record_score, hit._source.actual[0], hit._source.typical[0], hit._source.probability}; 373 | message += String.format(detailTemplate, detailValues); 374 | if (partitionName != null) { 375 | def partitionValues = new def[] {partitionName, partitionValue}; 376 | message += String.format(partitionTemplate, partitionValues); 377 | } 378 | } 379 | return [ 'message' : message , 'severity' : severity ];`; -------------------------------------------------------------------------------- /public/service/alert.service.js: -------------------------------------------------------------------------------- 1 | export default function AlertService($http, mlaConst, parse, EsDevToolService, es, script, scriptSlack, scriptLine) { 2 | const PATHS = mlaConst.paths; 3 | var CPATH = '..' + PATHS.console.path; 4 | var CMETHOD = PATHS.console.method; 5 | function checkScript(scriptName, scriptSource, successCallback, errorCallback) { 6 | let queryString = EsDevToolService.createQuery(PATHS.getScript.method, PATHS.getScript.path + scriptName); 7 | let uri = CPATH + '?' + queryString; 8 | $http.post(uri).then(successCallback, function(error) { 9 | console.info("try to put script " + scriptName); 10 | let putScriptQuery = EsDevToolService.createQuery(PATHS.putScript.method, PATHS.putScript.path + scriptName); 11 | let putScriptUri = CPATH + '?' + putScriptQuery; 12 | let body = { 13 | script: { 14 | lang: "painless", 15 | source: scriptSource 16 | } 17 | }; 18 | $http.post(putScriptUri, body).then(successCallback, errorCallback); 19 | }); 20 | } 21 | return { 22 | /** 23 | * Get the list of alerts for elastic-ml-alart plugin 24 | * @param successCallback success callback 25 | * @param errorCallback callback for failure 26 | */ 27 | searchList: function (successCallback, errorCallback) { 28 | var result = es.search({ 29 | index: ".watches", 30 | body: { 31 | query: { 32 | term: { 33 | "metadata.alert_type": "mla" 34 | } 35 | }, 36 | "sort": [ 37 | { 38 | "_id": { 39 | "order": "asc" 40 | } 41 | } 42 | ] 43 | } 44 | }).then(successCallback, errorCallback); 45 | }, 46 | /** 47 | * Get alert information by alertId 48 | * @param alertId Alert ID 49 | * @param successCallback success callback 50 | * @param errorCallback callback for failure 51 | */ 52 | search: function (alertId, successCallback, errorCallback) { 53 | let queryString = EsDevToolService.createQuery(PATHS.getWatch.method, PATHS.getWatch.path + alertId); 54 | let uri = CPATH + '?' + queryString; 55 | $http.post(uri).then(successCallback, errorCallback); 56 | }, 57 | /** 58 | * Delete alerts 59 | * @param alertIds list of AlertIDs 60 | * @param successCallback success callback 61 | * @param errorCallback callback for failure 62 | */ 63 | delete: function (alertIds, successCallback, errorCallback) { 64 | var totalCount = alertIds.length; 65 | var successCount = 0; 66 | var failCount = 0; 67 | function deleteOneAlert(alertId) { 68 | const promise = new Promise((resolve, reject) => { 69 | let queryString = EsDevToolService.createQuery(PATHS.deleteWatch.method, PATHS.deleteWatch.path + alertId); 70 | let uri = CPATH + '?' + queryString; 71 | $http.post(uri).then(function () { 72 | successCount++; 73 | resolve(); 74 | }, function (err) { 75 | console.error(err); 76 | failCount++; 77 | resolve(); 78 | }); 79 | }); 80 | return promise; 81 | } 82 | var del = deleteOneAlert(alertIds[0]); 83 | for (let i = 1; i < totalCount; i++) { 84 | del = del.then(() => deleteOneAlert(alertIds[i])); 85 | } 86 | return del.then(function () { 87 | if (successCount > 0) { 88 | successCallback(successCount, totalCount); 89 | } 90 | if (failCount > 0) { 91 | errorCallback(failCount, totalCount); 92 | } 93 | }); 94 | }, 95 | /** 96 | * Activate alerts 97 | * @param alertIds list of AlertIDs 98 | * @param successCallback success callback 99 | * @param errorCallback callback for failure 100 | */ 101 | activate: function (alertIds, successCallback, errorCallback) { 102 | var totalCount = alertIds.length; 103 | var successCount = 0; 104 | var failCount = 0; 105 | function activateOneAlert(alertId) { 106 | const promise = new Promise((resolve, reject) => { 107 | let queryString = EsDevToolService.createQuery(PATHS.editWatch.method, PATHS.editWatch.path + alertId + "/_activate"); 108 | let uri = CPATH + '?' + queryString; 109 | $http.post(uri).then(function () { 110 | successCount++; 111 | resolve(); 112 | }, function (err) { 113 | console.error(err); 114 | failCount++; 115 | resolve(); 116 | }); 117 | }); 118 | return promise; 119 | } 120 | var edit = activateOneAlert(alertIds[0]); 121 | for (let i = 1; i < totalCount; i++) { 122 | edit = edit.then(() => activateOneAlert(alertIds[i])); 123 | } 124 | return edit.then(function () { 125 | if (successCount > 0) { 126 | successCallback(successCount, totalCount); 127 | } 128 | if (failCount > 0) { 129 | errorCallback(failCount, totalCount); 130 | } 131 | }); 132 | }, 133 | /** 134 | * Deactivate alerts 135 | * @param alertIds list of AlertIDs 136 | * @param successCallback success callback 137 | * @param errorCallback callback for failure 138 | */ 139 | deactivate: function (alertIds, successCallback, errorCallback) { 140 | var totalCount = alertIds.length; 141 | var successCount = 0; 142 | var failCount = 0; 143 | function deactivateOneAlert(alertId) { 144 | const promise = new Promise((resolve, reject) => { 145 | let queryString = EsDevToolService.createQuery(PATHS.editWatch.method, PATHS.editWatch.path + alertId + "/_deactivate"); 146 | let uri = CPATH + '?' + queryString; 147 | $http.post(uri).then(function () { 148 | successCount++; 149 | resolve(); 150 | }, function (err) { 151 | console.error(err); 152 | failCount++; 153 | resolve(); 154 | }); 155 | }); 156 | return promise; 157 | } 158 | var edit = deactivateOneAlert(alertIds[0]); 159 | for (let i = 1; i < totalCount; i++) { 160 | edit = edit.then(() => deactivateOneAlert(alertIds[i])); 161 | } 162 | return edit.then(function () { 163 | if (successCount > 0) { 164 | successCallback(successCount, totalCount); 165 | } 166 | if (failCount > 0) { 167 | errorCallback(failCount, totalCount); 168 | } 169 | }); 170 | }, 171 | /** 172 | * Update alerts 173 | * @param alertIds list of AlertIDs 174 | * @param successCallback success callback 175 | * @param errorCallback callback for failure 176 | */ 177 | bulkUpdate: function (alertIds, input, successCallback, errorCallback) { 178 | var totalCount = alertIds.length; 179 | var successCount = 0; 180 | var failCount = 0; 181 | function saveAlert(res) { 182 | let data = res["data"]; 183 | let alertId = data._id; 184 | let body = data.watch; 185 | if (input.editMail && input.mailAddressTo[0].value != "") { 186 | body.actions.send_email = mlaConst.mailAction; 187 | body.actions.send_email.email.to = input.mailAddressTo.map(item => item.value); 188 | body.actions.send_email.email.cc = input.mailAddressCc.map(item => item.value); 189 | body.actions.send_email.email.bcc = input.mailAddressBcc.map(item => item.value); 190 | } 191 | if (input.editSlack && input.slackTo[0].value != "") { 192 | body.actions.notify_slack = mlaConst.slackAction; 193 | body.actions.notify_slack.slack.message.to = input.slackTo.map(item => item.value); 194 | } 195 | if (input.editLine && input.lineNotifyAccessToken != "") { 196 | body.actions.notify_line = mlaConst.lineAction; 197 | body.metadata.line_notify_access_token = input.lineNotifyAccessToken; 198 | } 199 | if (input.editMail && input.mailAddressTo[0].value == "" && body.actions.send_email && (body.actions.notify_slack || body.actions.notify_line)) { 200 | delete body.actions.send_email; 201 | } 202 | if (input.editSlack && input.slackTo[0].value == "" && (body.actions.send_email || body.actions.notify_line) && body.actions.notify_slack) { 203 | delete body.actions.notify_slack; 204 | } 205 | if (input.editLine && input.lineNotifyAccessToken == "" && (body.actions.send_email || body.actions.notify_slack) && body.actions.notify_line) { 206 | delete body.actions.notify_line; 207 | body.metadata.line_notify_access_token = ""; 208 | } 209 | if (input.editDashboard) { 210 | body.metadata.link_dashboards = input.linkDashboards; 211 | } 212 | let queryString = EsDevToolService.createQuery(PATHS.editWatch.method, PATHS.editWatch.path + alertId); 213 | let uri = CPATH + '?' + queryString; 214 | return $http.post(uri, body); 215 | } 216 | function updateOneAlert(alertId) { 217 | const promise = new Promise((resolve, reject) => { 218 | let queryString = EsDevToolService.createQuery(PATHS.getWatch.method, PATHS.getWatch.path + alertId); 219 | let uri = CPATH + '?' + queryString; 220 | $http.post(uri).then(function (res) { 221 | saveAlert(res).then(function() { 222 | successCount++; 223 | resolve(); 224 | }, function (err) { 225 | console.error(err); 226 | failCount++; 227 | resolve(); 228 | }); 229 | }, function (err) { 230 | console.error(err); 231 | failCount++; 232 | resolve(); 233 | }); 234 | }); 235 | return promise; 236 | } 237 | var edit = updateOneAlert(alertIds[0], input); 238 | for (let i = 1; i < totalCount; i++) { 239 | edit = edit.then(() => updateOneAlert(alertIds[i], input)); 240 | } 241 | return edit.then(function () { 242 | if (successCount > 0) { 243 | successCallback(successCount, totalCount); 244 | } 245 | if (failCount > 0) { 246 | errorCallback(failCount, totalCount); 247 | } 248 | }); 249 | }, 250 | /** 251 | * check if painless script exists 252 | * @param successCallback callback function for success 253 | * @param errorCallback callback function for failure 254 | */ 255 | checkScripts: function (successCallback, errorCallback) { 256 | let scriptForMail = mlaConst.names.scriptForMail; 257 | let scriptForSlack = mlaConst.names.scriptForSlack; 258 | let scriptForLine = mlaConst.names.scriptForLine; 259 | checkScript(scriptForMail, script, function() { 260 | checkScript(scriptForSlack, scriptSlack, function() { 261 | checkScript(scriptForLine, scriptLine, successCallback, errorCallback); 262 | }, errorCallback); 263 | }, errorCallback); 264 | }, 265 | /** 266 | * Save an alert 267 | * @param metadata metadata of the alert 268 | * @param successCallback success callback 269 | * @param errorCallback callback for failure 270 | */ 271 | save: function (metadata, successCallback, errorCallback) { 272 | let queryString = EsDevToolService.createQuery(PATHS.editWatch.method, PATHS.editWatch.path + metadata.alertId); 273 | let uri = CPATH + '?' + queryString; 274 | let body = JSON.parse(JSON.stringify(mlaConst.alertTemplate)); 275 | if (metadata.sendMail) { 276 | // templateの{{#toJson}}が使えなかったので直接入れる 277 | body.actions.send_email.email.to = metadata.mailAddressTo.map(item => item.value); 278 | if (metadata.mailAddressCc.length > 0) { 279 | body.actions.send_email.email.cc = metadata.mailAddressCc.map(item => item.value); 280 | } 281 | if (metadata.mailAddressBcc.length > 0) { 282 | body.actions.send_email.email.bcc = metadata.mailAddressBcc.map(item => item.value); 283 | } 284 | } else { 285 | delete body.actions.send_email; 286 | } 287 | if (metadata.notifySlack) { 288 | body.actions.notify_slack.slack.message.to = metadata.slackTo.map(item => item.value); 289 | if (metadata.slackAccount != '') { 290 | body.actions.notify_slack.slack.account = metadata.slackAccount; 291 | } 292 | } else { 293 | delete body.actions.notify_slack; 294 | } 295 | if (metadata.notifyLine) { 296 | body.metadata.line_notify_access_token = metadata.lineNotifyAccessToken; 297 | } else { 298 | delete body.actions.notify_line; 299 | } 300 | body.metadata.job_id = metadata.mlJobId; 301 | body.metadata.description = metadata.description; 302 | body.metadata.subject = metadata.subject; 303 | body.metadata.link_dashboards = metadata.linkDashboards; 304 | body.metadata.link_saved_searches = metadata.linkSavedSearches; 305 | body.metadata.threshold = metadata.threshold; 306 | body.metadata.detect_interval = metadata.detectInterval; 307 | body.metadata.kibana_display_term = metadata.kibanaDisplayTerm; 308 | body.metadata.kibana_url = metadata.kibanaUrl; 309 | body.metadata.locale = metadata.locale; 310 | body.metadata.ml_process_time = metadata.mlProcessTime; 311 | body.metadata.filterByActualValue = metadata.filterByActualValue; 312 | body.metadata.actualValueThreshold = metadata.actualValueThreshold; 313 | body.metadata.compareOption = metadata.compareOption; 314 | if (metadata.scheduleKind === 'cron') { 315 | body.trigger.schedule = { 316 | cron: metadata.triggerSchedule 317 | }; 318 | } 319 | if (metadata.filterByActualValue) { 320 | let rangeCondition = { 321 | "range": { 322 | "actual": {} 323 | } 324 | }; 325 | rangeCondition.range.actual[metadata.compareOption.compareType] = "{{ctx.metadata.actualValueThreshold}}"; 326 | body.input.search.request.body.query.bool.must.push(rangeCondition); 327 | } 328 | $http.post(uri, body).then(successCallback, errorCallback); 329 | }, 330 | 331 | calculateMlProcessTime: function (job, datafeed) { 332 | let bucketSpan = job.analysis_config.bucket_span; 333 | let frequency = datafeed.frequency ? datafeed.frequency : bucketSpan; 334 | let queryDelay = datafeed.query_delay; 335 | let totalDelaySeconds = Math.ceil((parse(bucketSpan) + parse(frequency) + parse(queryDelay) + parse('30s')) / 1000); 336 | return `${totalDelaySeconds}s`; 337 | }, 338 | 339 | calculateKibanaDisplayTerm: function (job) { 340 | let bucketSpan = job.analysis_config.bucket_span; 341 | let kibanaDisplayTerm = 2 * parse(bucketSpan) / 1000; 342 | return kibanaDisplayTerm; 343 | } 344 | }; 345 | } -------------------------------------------------------------------------------- /public/service/esConsole.service.js: -------------------------------------------------------------------------------- 1 | export default function EsDevToolService() { 2 | return { 3 | /** 4 | * Create query string for DevTools 5 | * @param method request method 6 | * @param path path of elasticsearch 7 | * @return query string 8 | */ 9 | createQuery: function (method, path) { 10 | let replacePath = path.replace(new RegExp('/', 'g'), '%2F'); 11 | return 'path=' + replacePath + '&method=' + method; 12 | } 13 | }; 14 | } -------------------------------------------------------------------------------- /public/service/mlJob.service.js: -------------------------------------------------------------------------------- 1 | export default function MlJobService($http, mlaConst, EsDevToolService) { 2 | const PATHS = mlaConst.paths; 3 | var CPATH = '..' + PATHS.console.path; 4 | var CMETHOD = PATHS.console.method; 5 | return { 6 | /** 7 | * Get the list of jobs 8 | * @param successCallback success callback 9 | * @param errorCallback callback for failure 10 | */ 11 | searchList: function (successCallback, errorCallback) { 12 | let queryString = EsDevToolService.createQuery(PATHS.mlJobList.method, PATHS.mlJobList.path); 13 | let uri = CPATH + '?' + queryString; 14 | $http.post(uri).then(successCallback, errorCallback); 15 | }, 16 | /** 17 | * Get job information by JobID 18 | * @param jobId Job ID 19 | * @param successCallback success callback 20 | * @param errorCallback callback for failure 21 | */ 22 | search: function (jobId, successCallback, errorCallback) { 23 | let queryString = EsDevToolService.createQuery(PATHS.mlJobList.method, PATHS.mlJobList.path + jobId); 24 | let uri = CPATH + '?' + queryString; 25 | $http.post(uri).then(successCallback, errorCallback); 26 | }, 27 | /** 28 | * Get data feed information by JobID 29 | * @param jobId Job ID 30 | * @param successCallback success callback 31 | * @param errorCallback callback for failure 32 | */ 33 | getDataFeed: function (jobId, successCallback, errorCallback) { 34 | // Data Feed ID format is datafeed- 35 | let datafeedId = `datafeed-${jobId}`; 36 | let queryString = EsDevToolService.createQuery(PATHS.mlJobDataFeed.method, PATHS.mlJobDataFeed.path + datafeedId); 37 | let uri = CPATH + '?' + queryString; 38 | $http.post(uri).then(successCallback, errorCallback); 39 | } 40 | }; 41 | } -------------------------------------------------------------------------------- /public/templates/alertList.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 |

6 | {{'MLA-ALERT_LIST'|translate}} 7 |

8 |
9 |
10 |
11 |
12 |
13 |
14 | 15 | 16 |
17 |
18 |
19 | 24 | 29 |
30 |
31 | 32 | 43 |
44 |
45 | 46 | 47 | 48 | 49 | 54 | 57 | 60 | 63 | 66 | 69 | 72 | 73 | 74 | 75 | 76 | 81 | 82 | 83 | 88 | 93 | 98 | 103 | 129 | 134 | 153 | 154 | 155 | 156 |
50 |
51 | 52 |
53 |
55 | Alert ID 56 | 58 | {{'MLA-DESCRIPTION'|translate}} 59 | 61 | {{'MLA-STATE'|translate}} 62 | 64 | {{'MLA-TARGET'|translate}} 65 | 67 | Dashboard 68 | 70 | Actions 71 |
77 |
78 | ML Job ID : {{jobId}} 79 |
80 |
84 |
85 | 86 |
87 |
89 |
90 | {{alert._id}} 91 |
92 |
94 |
95 | {{alert._source.metadata.description}} 96 |
97 |
99 |
100 | {{alert._source.status.state.active?'OK':'Disabled'}} 101 |
102 |
104 |
105 | {{mail}} 106 |
107 |
108 | Cc: 109 |
110 |
111 | {{mail}} 112 |
113 |
114 | Bcc: 115 |
116 |
117 | {{mail}} 118 |
119 |
120 | Slack: 121 |
122 |
123 | {{slackTo}} 124 |
125 |
126 | LINE Notify 127 |
128 |
130 |
131 | {{dashboard.title}} 132 |
133 |
135 |
136 | 139 | 142 | 145 | 148 | 151 |
152 |
157 |
158 |
159 |
165 |
166 |
167 |
168 | 169 | 180 |
181 |
182 |
183 |
-------------------------------------------------------------------------------- /public/templates/alertSetting.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 |

6 | {{'MLA-ALERT_SETTINGS'|translate}} 7 |

8 |
9 |
10 |
11 |
12 |
13 | 14 |
15 | 17 |
18 | 19 | 20 | 21 | 24 | 27 | 28 | 29 | 30 | 32 | 37 | 42 | 43 | 44 |
22 | Job ID 23 | 25 | Description 26 |
33 |
34 | {{job.job_id}} 35 |
36 |
38 |
39 | {{job.description}} 40 |
41 |
45 |
46 |
47 | {{'MLA-ML_JOB_ID_NOT_EXIST'|translate}} 48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | 56 |

57 | {{'MLA-FORM_ALERT_ID_HINT'|translate}} 58 |

59 | 61 |
62 |

63 | {{'MLA-ALERT_ID_EXISTS'|translate}} 64 |

65 |
66 |
67 |
68 | 69 | 70 |
71 |
72 | 73 | 74 |
75 |
76 | 77 | 78 |
79 |
80 |
81 |
82 | 83 |
84 | 86 | 88 | 89 | 90 |
91 | 93 | 94 | 95 |
96 |
97 | 98 |
99 | 101 | 103 | 104 | 105 |
106 | 108 | 109 | 110 |
111 |
112 | 113 |
114 | 116 | 118 | 119 | 120 |
121 | 123 | 124 | 125 |
126 |
127 |
128 | 129 | 130 |
131 |
132 |
133 | 134 |
135 | 137 | 139 | 140 | 141 |
142 | 144 | 145 | 146 |
147 |
148 | 149 |

150 | {{'MLA-FORM_SLACK_ACCOUNT_HINT'|translate}} 151 |

152 | 153 |
154 |
155 |
156 | 157 | 158 |
159 |
160 |
161 | 162 | 163 |
164 |
165 |
166 | 167 | 168 | 169 | 170 | 173 | 176 | 177 | 178 | 179 | 180 | 181 | 186 | 191 | 198 | 199 | 200 |
171 | Title 172 | 174 | Description 175 |
182 |
183 | {{dashboard.title}} 184 |
185 |
187 |
188 | {{dashboard.description}} 189 |
190 |
192 |
193 | 194 | 195 | 196 |
197 |
201 |
202 |
203 | {{'MLA-SELECT_DASHBOARD'|translate}} 204 |
205 |
206 |
207 |
208 | 209 | 210 | 211 | 212 | 215 | 218 | 219 | 220 | 221 | 222 | 223 | 228 | 233 | 240 | 241 | 242 |
213 | Title 214 | 216 | Description 217 |
224 |
225 | {{savedSearch.title}} 226 |
227 |
229 |
230 | {{savedSearch.description}} 231 |
232 |
234 |
235 | 236 | 237 | 238 |
239 |
243 |
244 |
245 | {{'MLA-SELECT_SAVED_SEARCH_FROM_LIST'|translate}} 246 |
247 |
248 |
249 |
250 | 251 | 252 | 253 | {{'MLA-ALERT_THRESHOLD_CRITICAL'|translate}} 254 | 255 | 256 | 257 | {{'MLA-ALERT_THRESHOLD_MAJOR'|translate}} 258 | 259 | 260 | 261 | {{'MLA-ALERT_THRESHOLD_MINOR'|translate}} 262 | 263 | 264 | 265 | {{'MLA-ALERT_THRESHOLD_WARNING'|translate}} 266 | 267 | 269 |
270 |
271 |
273 |
274 |
276 |
277 |
278 |
279 | 280 |

281 | {{'MLA-FORM_ALERT_TRIGGER_SCHEDULE_HINT'|translate}} 282 |

283 | 287 | 288 |
289 |

290 |
291 |
292 |

293 |
294 |
295 |
296 | 297 | 298 |
299 |

300 | {{'MLA-FORM_TARGET_INTERVAL_HINT'|translate}} 301 |

302 |
303 |
304 |
305 | 306 | 307 |
308 |
309 | 310 | 311 |
312 |
313 | 314 | 315 |
316 |

317 |
318 |
319 |

320 |
321 |
322 |
323 | 324 |
325 | 326 | 327 |
328 |
329 | {{'MLA-ACTUAL_VALUE'|translate}} 330 | 331 | 332 |
333 |
334 |

335 | {{'MLA-FORM_FILTER_BY_ACTUAL_VALUE_HINT'|translate}} 336 |

337 |
338 |
339 |
340 | 341 | 342 |
343 |
344 |
345 | 348 |
349 |
350 |
351 |
352 |
-------------------------------------------------------------------------------- /public/templates/common/header.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 | 6 |
7 | ML Alert 8 |
9 |
10 |
11 | 22 |
-------------------------------------------------------------------------------- /public_test/service/alert.service.test.js: -------------------------------------------------------------------------------- 1 | var parse = require('parse-duration'); 2 | 3 | import assert from 'assert'; 4 | import AlertService from '../../public/service/alert.service.js'; 5 | import EsDevToolService from '../../public/service/esConsole.service.js'; 6 | import constValue from '../../public/const.js'; 7 | 8 | describe('AlertService', () => { 9 | var AlertServiceInstance = AlertService(null, constValue, parse, EsDevToolService); 10 | describe('buildSchedule', () => { 11 | it('less than 1m', () => { 12 | var job = { 13 | analysis_config: { 14 | bucket_span: '30s' 15 | } 16 | }; 17 | var result = AlertServiceInstance.buildSchedule(job); 18 | assert.deepEqual(result, { 19 | triggerKind: 'interval', 20 | triggerSchedule: '30' 21 | }); 22 | }); 23 | }); 24 | }); -------------------------------------------------------------------------------- /public_test/service/esConsole.service.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import EsDevToolService from '../../public/service/esConsole.service.js'; 3 | 4 | describe('EsDevToolService', () => { 5 | var EsDevToolServiceInstance = EsDevToolService(); 6 | describe('createQuery', () => { 7 | it('GET test', () => { 8 | let result = EsDevToolServiceInstance.createQuery('GET', 'path/to/test'); 9 | assert.equal(result, 'path=path%2Fto%2Ftest&method=GET'); 10 | }); 11 | }); 12 | }); -------------------------------------------------------------------------------- /server/__tests__/index.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | describe('suite', () => { 4 | it('is a test test', () => { 5 | expect(true).to.equal(false); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /server/serverInit.js: -------------------------------------------------------------------------------- 1 | import url from 'url'; 2 | 3 | export default function (server, options) { 4 | server.log(['plugin:es_ml_alert', 'info'], 'es_ml_alert initialization'); 5 | }; -------------------------------------------------------------------------------- /translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "MLA-ENABLED_ALERT": "{{alertId}} is enabled.", 3 | "MLA-ENABLE_ALERT_FAILED": "Failed to enable {{alertId}}!", 4 | "MLA-DISABLED_ALERT": "{{alertId}} is disabled.", 5 | "MLA-DISABLE_ALERT_FAILED": "Failed to disable {{alertId}}!", 6 | "MLA-DELETED_ALERT": "{{alertId}} is deleted.", 7 | "MLA-DELETE_ALERT_FAILED": "Failed to delete {{alertId}}!", 8 | "MLA-CONFIRM_DELETE_ALERT": "Are you sure you want to delete {{alertId}}? This action is irreversible!", 9 | "MLA-DELETED_ALERTS": "Deleted {{successCount}} alerts in {{totalCount}}", 10 | "MLA-DELETE_ALERTS_FAILED": "Failed to delete {{failCount}} alerts in {{totalCount}}", 11 | "MLA-ENABLED_ALERTS": "Enabled {{successCount}} alerts in {{totalCount}}", 12 | "MLA-ENABLE_ALERTS_FAILED": "Failed to enable {{failCount}} alerts in {{totalCount}}", 13 | "MLA-DISABLED_ALERTS": "Disabled {{successCount}} alerts in {{totalCount}}", 14 | "MLA-DISABLE_ALERTS_FAILED": "Failed to disable {{failCount}} alerts in {{totalCount}}", 15 | "MLA-UPDATED_ALERTS": "Updated {{successCount}} alerts in {{totalCount}}", 16 | "MLA-UPDATE_ALERTS_FAILED": "Failed to update {{failCount}} alerts in {{totalCount}}", 17 | "MLA-DELETE_ALERTS_MESSAGE": "Delete selected alerts. This action is irreversible!", 18 | "MLA-ENABLE_ALERTS_MESSAGE": "Enable selected alerts", 19 | "MLA-DISABLE_ALERTS_MESSAGE": "Disable selected alerts", 20 | "MLA-BULK_OPERATION_TITLE": "Bulk operation for selected alerts", 21 | "MLA-BULK_UPDATE": "Update", 22 | "MLA-UPDATE_ALERTS_MESSAGE": "Bulk update alert settings", 23 | "MLA-ALERT_NOT_EXIST": "{{alertId}} does not exist.", 24 | "MLA-SELECT_DASHBOARDS": "Select Dashboard", 25 | "MLA-SELECT_SAVED_SEARCH": "Select Saved Search", 26 | "MLA-ALERT_LIST": "Alert List", 27 | "MLA-ALERT_SETTINGS": "Alert Settings", 28 | "MLA-ADD_NEW": "Create new alert", 29 | "MLA-BULK_OPERATION": "Bulk operation", 30 | "MLA-DESCRIPTION": "Description", 31 | "MLA-STATE": "State", 32 | "MLA-TARGET": "Target", 33 | "MLA-UPDATE_MAIL_ADDRESS": "Update mail address", 34 | "MLA-UPDATE_SLACK": "Update Slack target", 35 | "MLA-UPDATE_LINE_NOTIFY": "Update LINE Notify target", 36 | "MLA-UPDATE_DASHBOARDS": "Update dashboards", 37 | "MLA-SELECT_DASHBOARD": "Select from dashboard list", 38 | "MLA-FORM_ML_JOB_ID": "Machine Leaning Job ID", 39 | "MLA-ML_JOB_ID_NOT_EXIST": "There is no ML jobs. Create ML job first.", 40 | "MLA-FORM_ALERT_ID": "Alert ID", 41 | "MLA-FORM_ALERT_ID_HINT": "Unique identifier for alert, can use alphanumeric, underscores and hyphens.", 42 | "MLA-ALERT_ID_EXISTS": "This alert id already exists. The alert will be overwritten when it is saved.", 43 | "MLA-FORM_DESCRIPTION": "Description for this alert", 44 | "MLA-FORM_SUBJECT": "Subject of notification e-mail", 45 | "MLA-MAIL_NOTIFICATION": "E-mail notification", 46 | "MLA-FORM_MAIL": "Mail address", 47 | "MLA-SLACK_NOTIFICATION": "Slack notification", 48 | "MLA-FORM_SLACK_TARGET": "Slack notification target", 49 | "MLA-FORM_SLACK_ACCOUNT": "Slack account", 50 | "MLA-FORM_SLACK_ACCOUNT_HINT": "You can specify Slack account in elasticsearch.yml. Default account will be used if this form is blank.", 51 | "MLA-FORM_LINE_NOTIFY_ACCESS_TOKEN": "LINE Notify access token", 52 | "MLA-FORM_LINK_DASHBOARD": "Link dashboard in the notification message", 53 | "MLA-FORM_LINK_SAVED_SEARCH": "Link saved search in the notification message", 54 | "MLA-SELECT_SAVED_SEARCH_FROM_LIST": "Select from Saved Search list", 55 | "MLA-FORM_ALERT_THRESHOLD": "Anomaly Score threshold", 56 | "MLA-ALERT_THRESHOLD_CRITICAL": "critical", 57 | "MLA-ALERT_THRESHOLD_MAJOR": "major", 58 | "MLA-ALERT_THRESHOLD_MINOR": "minor", 59 | "MLA-ALERT_THRESHOLD_WARNING": "warning", 60 | "MLA-SHOW_ADVANCED_SETTINGS": "Show advanced settings", 61 | "MLA-CLOSE_ADVANCED_SETTINGS": "Close advanced settings", 62 | "MLA-FORM_ALERT_TRIGGER_SCHEDULE": "Alert trigger schedule", 63 | "MLA-FORM_ALERT_TRIGGER_SCHEDULE_HINT": "cron or interval", 64 | "MLA-INTERVAL": "interval", 65 | "MLA-EXAMPLE_CRON": "Example: 0 3/15 * * * ?\nEvery hour 3, 18, 33, 48 minutes", 66 | "MLA-EXAMPLE_INTERVAL": "Example: 3m\nEvery 3 minutes", 67 | "MLA-FORM_TARGET_INTERVAL": "Alert target interval", 68 | "MLA-FORM_TARGET_INTERVAL_HINT": "This should be same as alert trigger schedule interval.", 69 | "MLA-FORM_KIBANA_DISPLAY_TERM": "Display term for dashboard link in the notification message", 70 | "MLA-FORM_LOCALE": "locale for time format", 71 | "MLA-FORM_DELAY": "ML delay", 72 | "MLA-FORM_DELAY_HINT": "It should be longer than\nbucket_span + frequency + query_delay + (ML processing time)\nIf this value is too small, anomaly detection result will not be notified.", 73 | "MLA-FORM_DELAY_ERROR": "Invalid format\nExample:\"60s\", \"10m\", \"4h\", \"1d\"", 74 | "MLA-FORM_FILTER_BY_ACTUAL_VALUE": "Threshold to filter by actual value", 75 | "MLA-FILTER_BY_ACTUAL_VALUE": "Filter by actual value", 76 | "MLA-ACTUAL_VALUE": "(actual value)", 77 | "MLA-FORM_FILTER_BY_ACTUAL_VALUE_HINT": "Notify only when the actual value satisfies this condition.", 78 | "MLA-KIBANA_URL": "Kibana URL", 79 | "MLA-FORM_DESCRIPTION_SAMPLE": "Anomaly detection of XXX", 80 | "MLA-FORM_SUBJECT_SAMPLE": "Elasticsearch ML Anomaly Detection" 81 | } 82 | -------------------------------------------------------------------------------- /translations/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "MLA-ENABLED_ALERT": "{{alertId}}を有効化しました", 3 | "MLA-ENABLE_ALERT_FAILED": "{{alertId}}の有効化に失敗しました", 4 | "MLA-DISABLED_ALERT": "{{alertId}}を無効化しました", 5 | "MLA-DISABLE_ALERT_FAILED": "{{alertId}}の無効化に失敗しました", 6 | "MLA-DELETED_ALERT": "{{alertId}}を削除しました", 7 | "MLA-DELETE_ALERT_FAILED": "{{alertId}}の削除に失敗しました", 8 | "MLA-CONFIRM_DELETE_ALERT": "{{alertId}}を削除しますか?この操作は取り消せません。", 9 | "MLA-DELETED_ALERTS": "{{totalCount}}個中{{successCount}}個を削除しました", 10 | "MLA-DELETE_ALERTS_FAILED": "{{totalCount}}個中{{failCount}}個の削除に失敗しました", 11 | "MLA-ENABLED_ALERTS": "{{totalCount}}個中{{successCount}}個を有効化しました", 12 | "MLA-ENABLE_ALERTS_FAILED": "{{totalCount}}個中{{failCount}}個の有効化に失敗しました", 13 | "MLA-DISABLED_ALERTS": "{{totalCount}}個中{{successCount}}個を無効化しました", 14 | "MLA-DISABLE_ALERTS_FAILED": "{{totalCount}}個中{{failCount}}個の無効化に失敗しました", 15 | "MLA-UPDATED_ALERTS": "{{totalCount}}個中{{successCount}}個を更新しました", 16 | "MLA-UPDATE_ALERTS_FAILED": "{{totalCount}}個中{{failCount}}個の更新に失敗しました", 17 | "MLA-DELETE_ALERTS_MESSAGE": "選択したアラートを削除します。この操作は取り消せません。", 18 | "MLA-ENABLE_ALERTS_MESSAGE": "選択したアラートを有効化します。", 19 | "MLA-DISABLE_ALERTS_MESSAGE": "選択したアラートを無効化します。", 20 | "MLA-BULK_OPERATION_TITLE": "アラートの一括操作", 21 | "MLA-BULK_UPDATE": "一括更新", 22 | "MLA-UPDATE_ALERTS_MESSAGE": "アラートの設定を一括更新", 23 | "MLA-ALERT_NOT_EXIST": "{{alertId}}は存在しません", 24 | "MLA-SELECT_DASHBOARDS": "Dashboard 選択", 25 | "MLA-SELECT_SAVED_SEARCH": "Saved Search 選択", 26 | "MLA-ALERT_LIST": "通知一覧", 27 | "MLA-ALERT_SETTINGS": "通知設定", 28 | "MLA-ADD_NEW": "追加", 29 | "MLA-BULK_OPERATION": "一括操作", 30 | "MLA-DESCRIPTION": "概要", 31 | "MLA-STATE": "状態", 32 | "MLA-TARGET": "通知先", 33 | "MLA-UPDATE_MAIL_ADDRESS": "メールアドレスを更新する", 34 | "MLA-UPDATE_SLACK": "Slack通知先を更新する", 35 | "MLA-UPDATE_LINE_NOTIFY": "LINE Notify設定を更新する", 36 | "MLA-UPDATE_DASHBOARDS": "通知に含めるリンク先のDashboardを更新する", 37 | "MLA-SELECT_DASHBOARD": "Dashboard一覧から選択", 38 | "MLA-FORM_ML_JOB_ID": "Machine Leaning Job ID", 39 | "MLA-ML_JOB_ID_NOT_EXIST": "Machine Learning Jobがありません。最初にMachine Learning Jobを作成してください。", 40 | "MLA-FORM_ALERT_ID": "Alert ID", 41 | "MLA-FORM_ALERT_ID_HINT": "この通知設定のIDを指定します。英数字と\"_\"及び\"-\"が利用可能です。", 42 | "MLA-ALERT_ID_EXISTS": "既に存在するアラートIDのため、保存すると上書きされます", 43 | "MLA-FORM_DESCRIPTION": "通知の概要説明", 44 | "MLA-FORM_SUBJECT": "通知メールのタイトル", 45 | "MLA-MAIL_NOTIFICATION": "メール通知", 46 | "MLA-FORM_MAIL": "メールアドレス", 47 | "MLA-SLACK_NOTIFICATION": "Slack通知", 48 | "MLA-FORM_SLACK_TARGET": "Slackの通知先", 49 | "MLA-FORM_SLACK_ACCOUNT": "Slackのaccount", 50 | "MLA-FORM_SLACK_ACCOUNT_HINT": "elasticsearch.ymlに設定したSlackのaccountを入力してください。(空の場合、デフォルトアカウントを使用します)", 51 | "MLA-FORM_LINE_NOTIFY_ACCESS_TOKEN": "LINE Notifyのアクセストークン", 52 | "MLA-FORM_LINK_DASHBOARD": "通知に含めるリンク先のDashboard", 53 | "MLA-FORM_LINK_SAVED_SEARCH": "通知に含めるリンク先のSaved search", 54 | "MLA-SELECT_SAVED_SEARCH_FROM_LIST": "Saved Search 一覧から選択", 55 | "MLA-FORM_ALERT_THRESHOLD": "通知するAnomaly Scoreの閾値", 56 | "MLA-ALERT_THRESHOLD_CRITICAL": "critical 以上", 57 | "MLA-ALERT_THRESHOLD_MAJOR": "major 以上", 58 | "MLA-ALERT_THRESHOLD_MINOR": "minor 以上", 59 | "MLA-ALERT_THRESHOLD_WARNING": "warning 以上", 60 | "MLA-SHOW_ADVANCED_SETTINGS": "詳細設定を開く", 61 | "MLA-CLOSE_ADVANCED_SETTINGS": "詳細設定を閉じる", 62 | "MLA-FORM_ALERT_TRIGGER_SCHEDULE": "通知タイミング", 63 | "MLA-FORM_ALERT_TRIGGER_SCHEDULE_HINT": "タイミングはCronまたは繰り返し期間によって指定できます。", 64 | "MLA-INTERVAL": "繰り返し期間", 65 | "MLA-EXAMPLE_CRON": "例:0 3/15 * * * ? ← 毎時 3分、18分、33分、48分に実行", 66 | "MLA-EXAMPLE_INTERVAL": "例:3m ← 3分間隔で実行", 67 | "MLA-FORM_TARGET_INTERVAL": "通知時にチェック対象とする異常検知結果の期間", 68 | "MLA-FORM_TARGET_INTERVAL_HINT": "通知タイミングの間隔に合わせてください。", 69 | "MLA-FORM_KIBANA_DISPLAY_TERM": "Dashboardのリンクを開く際に、異常検知時刻の何秒前から何秒後までの時間範囲で表示するか", 70 | "MLA-FORM_LOCALE": "通知メールに時刻を記載する際のフォーマットを決めるためのlocale", 71 | "MLA-FORM_DELAY": "MLの異常検知結果が確定するまでの遅延時間", 72 | "MLA-FORM_DELAY_HINT": "bucket_span + frequency + query_delay + (ML処理時間) とする必要があります。\nこの値を小さくし過ぎると、異常検知結果が通知されません。", 73 | "MLA-FORM_DELAY_ERROR": "入力の形式が正しくありません。\n正しい入力の例:\"60s\", \"10m\", \"4h\", \"1d\"", 74 | "MLA-FORM_FILTER_BY_ACTUAL_VALUE": "実際の値でフィルターする場合の条件", 75 | "MLA-FILTER_BY_ACTUAL_VALUE": "実際の値でフィルターする", 76 | "MLA-ACTUAL_VALUE": "(実際の値)", 77 | "MLA-FORM_FILTER_BY_ACTUAL_VALUE_HINT": "Anomaly scoreが閾値を超えていた場合も、実際の値がこの条件を満たさない場合には通知を抑制します。", 78 | "MLA-KIBANA_URL": "KibanaのURL", 79 | "MLA-FORM_DESCRIPTION_SAMPLE": "XXXの異常検知", 80 | "MLA-FORM_SUBJECT_SAMPLE": "Elasticsearch ML 異常検知通知" 81 | } 82 | --------------------------------------------------------------------------------