diff --git a/.github/workflows/ci-hertzbeat.yml b/.github/workflows/ci-hertzbeat.yml new file mode 100644 index 0000000..88d0b79 --- /dev/null +++ b/.github/workflows/ci-hertzbeat.yml @@ -0,0 +1,25 @@ +name: HertzBeat CI +on: + pull_request: + paths: + - 'applications/hertzbeat/**' + +jobs: + application-hertzbeat: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup Java Env + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'zulu' + cache: 'maven' + - name: Start OceanBase container + uses: oceanbase/setup-oceanbase-ce@v1.0 + with: + network: 'host' + - name: Build and Test + run: | + cd applications/hertzbeat + mvn clean test diff --git a/.gitignore b/.gitignore index 500bfaf..3abdae4 100644 --- a/.gitignore +++ b/.gitignore @@ -95,7 +95,6 @@ build/Release # Dependency directories node_modules/ -config/ jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) @@ -311,4 +310,4 @@ venv.bak/ # mypy .mypy_cache/ .dmypy.json -dmypy.json \ No newline at end of file +dmypy.json diff --git a/applications/hertzbeat/.asf.yaml b/applications/hertzbeat/.asf.yaml new file mode 100644 index 0000000..a857a2c --- /dev/null +++ b/applications/hertzbeat/.asf.yaml @@ -0,0 +1,60 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +github: + description: Apache HertzBeat(incubating) is a real-time monitoring system with agentless, performance cluster, prometheus-compatible, custom monitoring and status page building capabilities. + homepage: https://hertzbeat.apache.org/ + labels: + - monitoring + - monitor + - notifications + - alerting + - self-hosted + - prometheus + - zabbix + - grafana + - metrics + - observability + - uptime + - uptime-monitoring + - status + - status-page + - devops + - server + - linux + - database + - mysql + - cloud + enabled_merge_buttons: + squash: true + merge: false + rebase: false + protected_branches: + master: + required_status_checks: + strict: true + contexts: + - check-license-header + required_pull_request_reviews: + dismiss_stale_reviews: true + required_approving_review_count: 1 +notifications: + commits: notifications@hertzbeat.apache.org + issues: notifications@hertzbeat.apache.org + pullrequests: notifications@hertzbeat.apache.org + jobs: notifications@hertzbeat.apache.org + discussions: dev@hertzbeat.apache.org diff --git a/applications/hertzbeat/.gitattributes b/applications/hertzbeat/.gitattributes new file mode 100644 index 0000000..b071320 --- /dev/null +++ b/applications/hertzbeat/.gitattributes @@ -0,0 +1,23 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +/home export-ignore +/hip export-ignore +/.github export-ignore +/.idea export-ignore +.gitattributes export-ignore +.gitignore export-ignore diff --git a/applications/hertzbeat/.gitignore b/applications/hertzbeat/.gitignore new file mode 100644 index 0000000..bad93d6 --- /dev/null +++ b/applications/hertzbeat/.gitignore @@ -0,0 +1,56 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +Thumbs.db +.DS_Store +.gradle +build/ +out/ +micronaut-cli.yml +.mvn/ +mvnw +mvnw.bat +*.log +package-lock.json +*.zip +jdk/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### For icon.png ### +!.idea/icon.png + +### NetBeans ### +nbproject/private/ +build/ +nbbuild/ +dist/ +nbdist/ +.nb-gradle/ + +### VS Code ### +.vscode/ + +# dependencies +node_modules +.docusaurus + +# h2 data +*.mv.db +*.trace.db + +# debug env +application-dev.yml +application-mysql.yml +application-pg.yml diff --git a/applications/hertzbeat/.licenserc.yaml b/applications/hertzbeat/.licenserc.yaml new file mode 100644 index 0000000..8eaea84 --- /dev/null +++ b/applications/hertzbeat/.licenserc.yaml @@ -0,0 +1,103 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +header: + license: + spdx-id: Apache-2.0 + copyright-owner: Apache Software Foundation + + paths-ignore: + - '**/*.md' + - '**/*.json' + - '**/*.iml' + - '**/*.ini' + - '**/*.svg' + - '**/*.png' + - '**/*.MD' + - '**/*.ftl' + - '**/*.tpl' + - '**/*.pl' + - '**/*.dict' + - '**/*.awk' + - "**/*.lock" + - '**/*.crt' + - '**/*.pem' + - '**/*.js' + - '**/*.less' + - '**/*.txt' + - '**/target/**' + - '.gitattributes' + - '**/.gitignore' + - '**/.gitkeep' + - 'home/**' + - 'material/**' + - 'LICENSE' + - 'NOTICE' + - 'DISCLAIMER' + - '**/LICENSE' + - '**/NOTICE' + - '.all-contributorsrc' + - '**/*.AbstractCollect' + - '**/*.Plugin' + - '**/*.MockMaker' + - '.prettierrc' + - '.browserslistrc' + - '.editorconfig' + - '.eslintignore' + - '.eslintrc.js' + - '.stylelintrc' + - '.prettierignore' + - '.prettierrc.js' + - 'karma.conf.js' + - 'proxy.conf.js' + - '.helmignore' + - 'web-app/src/app/core/**' + - 'web-app/src/app/layout/**' + - 'web-app/src/app/routes/exception/**' + - 'web-app/src/app/routes/routes.module.ts' + - 'web-app/src/app/routes/routes-routing.module.ts' + - 'web-app/src/app/shared/json-schema/**' + - 'web-app/src/app/shared/index.ts' + - 'web-app/src/app/shared/shared.module.ts' + - 'web-app/src/app/shared/shared-delon.module.ts' + - 'web-app/src/app/shared/shared-zorro.module.ts' + - 'web-app/src/app/app.module.ts' + - 'web-app/src/app/app.component.ts' + - 'web-app/src/app/global-config.module.ts' + - 'web-app/src/app/icons-provider.module.ts' + - 'web-app/src/assets/color.less' + - 'web-app/src/assets/style.compact.less' + - 'web-app/src/assets/style.dark.less' + - 'web-app/src/environments/**' + - 'web-app/src/styles/**' + - 'web-app/src/index.html' + - 'web-app/src/main.ts' + - 'web-app/src/polyfills.ts' + - 'web-app/src/style-icons.ts' + - 'web-app/src/style-icons-auto.ts' + - 'web-app/src/style.less' + - 'web-app/src/test.ts' + - 'web-app/src/typings.d.ts' + + comment: on-failure + +dependency: + files: + - pom.xml + - web-app/package.json + + diff --git a/applications/hertzbeat/DISCLAIMER b/applications/hertzbeat/DISCLAIMER new file mode 100644 index 0000000..15089ac --- /dev/null +++ b/applications/hertzbeat/DISCLAIMER @@ -0,0 +1,10 @@ +Apache HertzBeat (incubating) is an effort undergoing incubation at the Apache +Software Foundation (ASF), sponsored by the Apache Incubator PMC. + +Incubation is required of all newly accepted projects until a further review +indicates that the infrastructure, communications, and decision making process +have stabilized in a manner consistent with other successful ASF projects. + +While incubation status is not necessarily a reflection of the completeness +or stability of the code, it does indicate that the project has yet to be +fully endorsed by the ASF. diff --git a/applications/hertzbeat/LICENSE b/applications/hertzbeat/LICENSE new file mode 100644 index 0000000..415987b --- /dev/null +++ b/applications/hertzbeat/LICENSE @@ -0,0 +1,237 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +======================================================================== +MIT License +======================================================================== + +The following components are provided under the MIT License +These files from https://github.com/ng-alain/ng-alain +The text of the license is included in material/licenses/frontend/LICENSE-ng-alain.txt + +web-app/src/app/core/** +web-app/src/app/layout/** +web-app/src/app/routes/exception/** +web-app/src/app/routes/routes.module.ts +web-app/src/app/routes/routes-routing.module.ts +web-app/src/app/shared/json-schema/** +web-app/src/app/shared/index.ts +web-app/src/app/shared/shared.module.ts +web-app/src/app/shared/shared-delon.module.ts +web-app/src/app/shared/shared-zorro.module.ts +web-app/src/app/app.module.ts +web-app/src/app/app.component.ts +web-app/src/app/global-config.module.ts +web-app/src/app/icons-provider.module.ts +web-app/src/assets/color.less +web-app/src/assets/style.compact.less +web-app/src/assets/style.dark.less +web-app/src/environments/** +web-app/src/styles/** +web-app/src/index.html +web-app/src/main.ts +web-app/src/polyfills.ts +web-app/src/style-icons.ts +web-app/src/style-icons-auto.ts +web-app/src/style.less +web-app/src/test.ts +web-app/src/typings.d.ts diff --git a/applications/hertzbeat/NOTICE b/applications/hertzbeat/NOTICE new file mode 100644 index 0000000..6839b47 --- /dev/null +++ b/applications/hertzbeat/NOTICE @@ -0,0 +1,5 @@ +Apache HertzBeat (incubating) +Copyright 2024 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). diff --git a/applications/hertzbeat/README.md b/applications/hertzbeat/README.md new file mode 100644 index 0000000..fe9a91e --- /dev/null +++ b/applications/hertzbeat/README.md @@ -0,0 +1,88 @@ +

+ + hertzbeat + +

+ +

+English Document | 中文文档 +

+ +> A real-time monitoring system with agentless, performance cluster, prometheus-compatible, custom monitoring and status page building capabilities. + + +## HertzBeat Start With OceanBase + +1. Start the OceanBase Database + +> here we use docker to start an ob standalone. + +```shell +docker run -p 2881:2881 --name obstandalone -e MINI_MODE=1 -d oceanbase/oceanbase-ce +``` + +2. Create the database name `hertzbeat` + +```shell +create database if not exists hertzbeat default charset utf8mb4; +``` + +3. Build the HertzBeat + +```shell +mvn clean install +``` + +4. Start the HertzBeat Backend + +Start SpringBoot Instance `org.apache.hertzbeat.manager.Manager` + +5. Start the HertzBeat Webapp + +```shell +cd web-app + +yarn install + +yarn start +``` + +6. Access `http://localhost:4200` to start, account `admin/hertzbeat` + +## Custom Database Configuration + +### Custom Local or Prod Env Configuration + +Modify the `application.yml` file in the `manager` module `manager/src/main/resources` + +```yaml +spring: + config: + activate: + on-profile: prod + + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + username: root + password: + url: jdbc:mysql://localhost:2881/hertzbeat?createDatabaseIfNotExist=true&useUnicode=true&characterEncoding=utf-8&useSSL=false + hikari: + max-lifetime: 120000 +``` + +### Custom Test Env Configuration + +This if for the test environment, `mvn clean test` + +Modify the `application-test.yml` file in the `manager` module `manager/src/main/resources` + +```yaml +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + username: root + password: + url: jdbc:mysql://localhost:2881/test?createDatabaseIfNotExist=true&useUnicode=true&characterEncoding=utf-8&useSSL=false + hikari: + max-lifetime: 120000 +``` diff --git a/applications/hertzbeat/README_CN.md b/applications/hertzbeat/README_CN.md new file mode 100644 index 0000000..4c37778 --- /dev/null +++ b/applications/hertzbeat/README_CN.md @@ -0,0 +1,87 @@ +

+ + hertzbeat + +

+ +

+中文文档 | English Document +

+ +> 实时监控系统,无需 Agent,性能集群,兼容 Prometheus,自定义监控和状态页构建能力。 + +## HertzBeat Start With OceanBase + +1. Start the OceanBase Database + +> here we use docker to start an ob standalone. + +```shell +docker run -p 2881:2881 --name obstandalone -e MINI_MODE=1 -d oceanbase/oceanbase-ce +``` + +2. Create the database name `hertzbeat` + +```shell +create database if not exists hertzbeat default charset utf8mb4; +``` + +3. Build the HertzBeat + +```shell +mvn clean install +``` + +4. Start the HertzBeat Backend + +Start SpringBoot Instance `org.apache.hertzbeat.manager.Manager` + +5. Start the HertzBeat Webapp + +```shell +cd web-app + +yarn install + +yarn start +``` + +6. Access `http://localhost:4200` to start, account `admin/hertzbeat` + +## Custom Database Configuration + +### Custom Local or Prod Env Configuration + +Modify the `application.yml` file in the `manager` module `manager/src/main/resources` + +```yaml +spring: + config: + activate: + on-profile: prod + + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + username: root + password: + url: jdbc:mysql://localhost:2881/hertzbeat?createDatabaseIfNotExist=true&useUnicode=true&characterEncoding=utf-8&useSSL=false + hikari: + max-lifetime: 120000 +``` + +### Custom Test Env Configuration + +This if for the test environment, `mvn clean test` + +Modify the `application-test.yml` file in the `manager` module `manager/src/main/resources` + +```yaml +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + username: root + password: + url: jdbc:mysql://localhost:2881/test?createDatabaseIfNotExist=true&useUnicode=true&characterEncoding=utf-8&useSSL=false + hikari: + max-lifetime: 120000 +``` diff --git a/applications/hertzbeat/alerter/pom.xml b/applications/hertzbeat/alerter/pom.xml new file mode 100644 index 0000000..dcbce50 --- /dev/null +++ b/applications/hertzbeat/alerter/pom.xml @@ -0,0 +1,87 @@ + + + + + hertzbeat + org.apache.hertzbeat + 2.0-SNAPSHOT + + 4.0.0 + + hertzbeat-alerter + ${project.artifactId} + + + + + + + + org.apache.hertzbeat + hertzbeat-common + provided + + + + org.springframework.boot + spring-boot-starter-web + provided + + + org.springframework.boot + spring-boot-autoconfigure + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.apache.kafka + kafka-clients + + + + io.lettuce + lettuce-core + provided + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + provided + + + cn.afterturn + easypoi-annotation + 4.3.0 + compile + + + org.apache.poi + poi + 4.1.1 + compile + + + + diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/AlerterProperties.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/AlerterProperties.java new file mode 100644 index 0000000..486dbae --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/AlerterProperties.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * alerter prop config + */ +@Component +@ConfigurationProperties(prefix = "alerter") +@Getter +@Setter +public class AlerterProperties { + + /** + * Alarm content console link + */ + private String consoleUrl = "https://console.tancloud.cn"; + + /** + * WeWork webhook url + */ + private String weWorkWebhookUrl = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key="; + + /** + * DingDing talk webhook url + */ + private String dingTalkWebhookUrl = "https://oapi.dingtalk.com/robot/send?access_token="; + + /** + * FlyBook webhook url + */ + private String flyBookWebhookUrl = "https://open.feishu.cn/open-apis/bot/v2/hook/"; + + /** + * Telegram Bot api url + */ + private String telegramWebhookUrl = "https://api.telegram.org/bot%s/sendMessage"; + + /** + * Discord Notify url + */ + private String discordWebhookUrl = "https://discord.com/api/v9/channels/%s/messages"; + + /** + * ServerChan Notify url + */ + private String serverChanWebhookUrl = "https://sctapi.ftqq.com/%s.send"; + /** + * Gotify Notify url + */ + private String gotifyWebhookUrl = "https://push.example.de/message?token="; + + /** + * Data entry configuration properties + */ + private EntranceProperties entrance; + + /** + * Data entry configuration properties + */ + @Getter + @Setter + public static class EntranceProperties { + + /** + * kafka configuration information + */ + private KafkaProperties kafka; + + /** + * kafka configuration information + */ + @Getter + @Setter + public static class KafkaProperties { + /** + * Whether the kafka data entry is started + */ + private boolean enabled = true; + + /** + * kafka's connection server url + */ + private String servers = "127.0.0.1:9092"; + /** + * The name of the topic that receives the data + */ + private String topic; + /** + * Consumer Group ID + */ + private String groupId; + + } + } +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/AlerterWorkerPool.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/AlerterWorkerPool.java new file mode 100644 index 0000000..a8ebf4a --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/AlerterWorkerPool.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * alarm module thread pool + */ +@Component +@Slf4j +public class AlerterWorkerPool { + + private ThreadPoolExecutor workerExecutor; + + public AlerterWorkerPool() { + initWorkExecutor(); + } + + private void initWorkExecutor() { + ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setUncaughtExceptionHandler((thread, throwable) -> { + log.error("workerExecutor has uncaughtException."); + log.error(throwable.getMessage(), throwable); + }) + .setDaemon(true) + .setNameFormat("alerter-worker-%d") + .build(); + workerExecutor = new ThreadPoolExecutor(6, + 10, + 10, + TimeUnit.SECONDS, + new SynchronousQueue<>(), + threadFactory, + new ThreadPoolExecutor.AbortPolicy()); + } + + /** + * Run the alerter task + * @param runnable task + * @throws RejectedExecutionException when The thread pool is full of + */ + public void executeJob(Runnable runnable) throws RejectedExecutionException { + workerExecutor.execute(runnable); + } +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/calculate/CalculateAlarm.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/calculate/CalculateAlarm.java new file mode 100644 index 0000000..fb60f50 --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/calculate/CalculateAlarm.java @@ -0,0 +1,482 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.calculate; + +import static org.apache.hertzbeat.common.constants.CommonConstants.ALERT_STATUS_CODE_NOT_REACH; +import static org.apache.hertzbeat.common.constants.CommonConstants.ALERT_STATUS_CODE_PENDING; +import static org.apache.hertzbeat.common.constants.CommonConstants.ALERT_STATUS_CODE_SOLVED; +import static org.apache.hertzbeat.common.constants.CommonConstants.TAG_CODE; +import static org.apache.hertzbeat.common.constants.CommonConstants.TAG_METRIC; +import static org.apache.hertzbeat.common.constants.CommonConstants.TAG_METRICS; +import static org.apache.hertzbeat.common.constants.CommonConstants.TAG_MONITOR_APP; +import static org.apache.hertzbeat.common.constants.CommonConstants.TAG_MONITOR_ID; +import static org.apache.hertzbeat.common.constants.CommonConstants.TAG_MONITOR_NAME; +import jakarta.persistence.criteria.Predicate; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.jexl3.JexlException; +import org.apache.commons.jexl3.JexlExpression; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.alert.AlerterWorkerPool; +import org.apache.hertzbeat.alert.dao.AlertMonitorDao; +import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce; +import org.apache.hertzbeat.alert.service.AlertDefineService; +import org.apache.hertzbeat.alert.service.AlertService; +import org.apache.hertzbeat.alert.util.AlertTemplateUtil; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.Alert; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.apache.hertzbeat.common.entity.manager.Monitor; +import org.apache.hertzbeat.common.entity.manager.TagItem; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.queue.CommonDataQueue; +import org.apache.hertzbeat.common.support.event.MonitorDeletedEvent; +import org.apache.hertzbeat.common.support.event.SystemConfigChangeEvent; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.apache.hertzbeat.common.util.JexlExpressionRunner; +import org.apache.hertzbeat.common.util.ResourceBundleUtil; +import org.springframework.context.event.EventListener; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; + + +/** + * Calculate alarms based on the alarm definition rules and collected data + */ +@Component +@Slf4j +public class CalculateAlarm { + + private static final String SYSTEM_VALUE_ROW_COUNT = "system_value_row_count"; + + /** + * The alarm in the process is triggered + * key - monitorId+alertDefineId+tags | The alarm is a common threshold alarm + * key - monitorId | Indicates the monitoring status availability reachability alarm + */ + private final Map triggeredAlertMap; + /** + * The not recover alert + * key - monitorId + alertDefineId + tags + */ + private final Map notRecoveredAlertMap; + private final AlerterWorkerPool workerPool; + private final CommonDataQueue dataQueue; + private final AlertDefineService alertDefineService; + private final AlarmCommonReduce alarmCommonReduce; + private ResourceBundle bundle; + private final AlertService alertService; + + public CalculateAlarm(AlerterWorkerPool workerPool, CommonDataQueue dataQueue, + AlertDefineService alertDefineService, AlertMonitorDao monitorDao, + AlarmCommonReduce alarmCommonReduce, AlertService alertService) { + this.workerPool = workerPool; + this.dataQueue = dataQueue; + this.alarmCommonReduce = alarmCommonReduce; + this.alertDefineService = alertDefineService; + this.alertService = alertService; + this.bundle = ResourceBundleUtil.getBundle("alerter"); + this.triggeredAlertMap = new ConcurrentHashMap<>(16); + this.notRecoveredAlertMap = new ConcurrentHashMap<>(16); + // Initialize stateAlertMap + List monitors = monitorDao.findMonitorsByStatus(CommonConstants.UN_AVAILABLE_CODE); + if (monitors != null) { + for (Monitor monitor : monitors) { + HashMap tags = new HashMap<>(8); + tags.put(TAG_MONITOR_ID, String.valueOf(monitor.getId())); + tags.put(TAG_MONITOR_NAME, monitor.getName()); + tags.put(TAG_MONITOR_APP, monitor.getApp()); + this.notRecoveredAlertMap.put(monitor.getId() + CommonConstants.AVAILABILITY, + Alert.builder().tags(tags).target(CommonConstants.AVAILABILITY).status(ALERT_STATUS_CODE_PENDING).build()); + } + } + startCalculate(); + } + + private void startCalculate() { + Runnable runnable = () -> { + while (!Thread.currentThread().isInterrupted()) { + try { + CollectRep.MetricsData metricsData = dataQueue.pollMetricsDataToAlerter(); + if (metricsData != null) { + calculate(metricsData); + } + } catch (InterruptedException ignored) { + + } catch (Exception e) { + log.error("calculate alarm error: {}.", e.getMessage(), e); + } + } + }; + workerPool.executeJob(runnable); + workerPool.executeJob(runnable); + workerPool.executeJob(runnable); + } + + private void calculate(CollectRep.MetricsData metricsData) { + long currentTimeMilli = System.currentTimeMillis(); + long monitorId = metricsData.getId(); + String app = metricsData.getApp(); + if (app.startsWith(CommonConstants.PROMETHEUS_APP_PREFIX)) { + app = CommonConstants.PROMETHEUS; + } + String metrics = metricsData.getMetrics(); + // If the metrics whose scheduling priority is 0 has the status of collecting response data UN_REACHABLE/UN_CONNECTABLE, + // the highest severity alarm is generated to monitor the status change + if (metricsData.getPriority() == 0) { + handlerAvailableMetrics(monitorId, app, metricsData); + } + // Query the alarm definitions associated with the metrics of the monitoring type + // field - define[] + Map> defineMap = alertDefineService.getMonitorBindAlertDefines(monitorId, app, metrics); + if (defineMap.isEmpty()) { + return; + } + List fields = metricsData.getFieldsList(); + Map fieldValueMap = new HashMap<>(8); + int valueRowCount = metricsData.getValuesCount(); + for (Map.Entry> entry : defineMap.entrySet()) { + List defines = entry.getValue(); + for (AlertDefine define : defines) { + final String expr = define.getExpr(); + if (StringUtils.isBlank(expr)) { + continue; + } + if (expr.contains(SYSTEM_VALUE_ROW_COUNT) && metricsData.getValuesCount() == 0) { + fieldValueMap.put(SYSTEM_VALUE_ROW_COUNT, valueRowCount); + try { + boolean match = execAlertExpression(fieldValueMap, expr); + try { + if (match) { + // If the threshold rule matches, the number of times the threshold has been triggered is determined and an alarm is triggered + afterThresholdRuleMatch(currentTimeMilli, monitorId, app, metrics, "", fieldValueMap, define); + // if the threshold is triggered, ignore other data rows + continue; + } else { + String alarmKey = String.valueOf(monitorId) + define.getId(); + triggeredAlertMap.remove(alarmKey); + if (define.isRecoverNotice()) { + handleRecoveredAlert(currentTimeMilli, define, expr, alarmKey); + } + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } catch (Exception ignored) {} + } + for (CollectRep.ValueRow valueRow : metricsData.getValuesList()) { + + if (CollectionUtils.isEmpty(valueRow.getColumnsList())) { + continue; + } + fieldValueMap.clear(); + fieldValueMap.put(SYSTEM_VALUE_ROW_COUNT, valueRowCount); + StringBuilder tagBuilder = new StringBuilder(); + for (int index = 0; index < valueRow.getColumnsList().size(); index++) { + String valueStr = valueRow.getColumns(index); + if (CommonConstants.NULL_VALUE.equals(valueStr)) { + continue; + } + + final CollectRep.Field field = fields.get(index); + final int fieldType = field.getType(); + + if (fieldType == CommonConstants.TYPE_NUMBER) { + final Double doubleValue; + if ((doubleValue = CommonUtil.parseStrDouble(valueStr)) != null) { + fieldValueMap.put(field.getName(), doubleValue); + } + } else if (fieldType == CommonConstants.TYPE_TIME) { + final Integer integerValue; + if ((integerValue = CommonUtil.parseStrInteger(valueStr)) != null) { + fieldValueMap.put(field.getName(), integerValue); + } + } else { + if (StringUtils.isNotEmpty(valueStr)) { + fieldValueMap.put(field.getName(), valueStr); + } + } + + if (field.getLabel()) { + tagBuilder.append("-").append(valueStr); + } + } + try { + boolean match = execAlertExpression(fieldValueMap, expr); + try { + if (match) { + // If the threshold rule matches, the number of times the threshold has been triggered is determined and an alarm is triggered + afterThresholdRuleMatch(currentTimeMilli, monitorId, app, metrics, tagBuilder.toString(), fieldValueMap, define); + } else { + String alarmKey = String.valueOf(monitorId) + define.getId() + tagBuilder; + triggeredAlertMap.remove(alarmKey); + if (define.isRecoverNotice()) { + handleRecoveredAlert(currentTimeMilli, define, expr, alarmKey); + } + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } catch (Exception ignored) {} + } + } + } + } + + private void handleRecoveredAlert(long currentTimeMilli, AlertDefine define, String expr, String alarmKey) { + Alert notResolvedAlert = notRecoveredAlertMap.remove(alarmKey); + if (notResolvedAlert != null) { + // Sending an alarm Restore + Map tags = notResolvedAlert.getTags(); + String content = this.bundle.getString("alerter.alarm.recover") + " : " + expr; + Alert resumeAlert = Alert.builder() + .tags(tags) + .target(define.getApp() + "." + define.getMetric() + "." + define.getField()) + .content(content) + .priority(CommonConstants.ALERT_PRIORITY_CODE_WARNING) + .status(CommonConstants.ALERT_STATUS_CODE_RESTORED) + .firstAlarmTime(currentTimeMilli) + .lastAlarmTime(notResolvedAlert.getLastAlarmTime()) + .triggerTimes(1) + .build(); + alarmCommonReduce.reduceAndSendAlarm(resumeAlert); + } + } + + private void afterThresholdRuleMatch(long currentTimeMilli, long monitorId, String app, String metrics, String tagStr, + Map fieldValueMap, AlertDefine define) { + String alarmKey = String.valueOf(monitorId) + define.getId() + tagStr; + Alert triggeredAlert = triggeredAlertMap.get(alarmKey); + if (triggeredAlert != null) { + int times = triggeredAlert.getTriggerTimes() + 1; + triggeredAlert.setTriggerTimes(times); + triggeredAlert.setFirstAlarmTime(currentTimeMilli); + triggeredAlert.setLastAlarmTime(currentTimeMilli); + int defineTimes = define.getTimes() == null ? 1 : define.getTimes(); + if (times >= defineTimes) { + triggeredAlert.setStatus(ALERT_STATUS_CODE_PENDING); + triggeredAlertMap.remove(alarmKey); + notRecoveredAlertMap.put(alarmKey, triggeredAlert); + alarmCommonReduce.reduceAndSendAlarm(triggeredAlert.clone()); + } + } else { + fieldValueMap.put(TAG_MONITOR_APP, app); + fieldValueMap.put(TAG_METRICS, metrics); + fieldValueMap.put(TAG_METRIC, define.getField()); + Map tags = new HashMap<>(8); + tags.put(TAG_MONITOR_ID, String.valueOf(monitorId)); + tags.put(TAG_MONITOR_APP, app); + tags.put(CommonConstants.TAG_THRESHOLD_ID, String.valueOf(define.getId())); + if (!CollectionUtils.isEmpty(define.getTags())) { + for (TagItem tagItem : define.getTags()) { + fieldValueMap.put(tagItem.getName(), tagItem.getValue()); + tags.put(tagItem.getName(), tagItem.getValue()); + } + } + Alert alert = Alert.builder() + .tags(tags) + .priority(define.getPriority()) + .status(ALERT_STATUS_CODE_NOT_REACH) + .target(app + "." + metrics + "." + define.getField()) + .triggerTimes(1) + .firstAlarmTime(currentTimeMilli) + .lastAlarmTime(currentTimeMilli) + // Keyword matching and substitution in the template + .content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap)) + .build(); + int defineTimes = define.getTimes() == null ? 1 : define.getTimes(); + if (1 >= defineTimes) { + alert.setStatus(ALERT_STATUS_CODE_PENDING); + notRecoveredAlertMap.put(alarmKey, alert); + alarmCommonReduce.reduceAndSendAlarm(alert.clone()); + } else { + triggeredAlertMap.put(alarmKey, alert); + } + } + } + + private boolean execAlertExpression(Map fieldValueMap, String expr) { + Boolean match; + JexlExpression expression; + try { + expression = JexlExpressionRunner.compile(expr); + } catch (JexlException jexlException) { + log.error("Alarm Rule: {} Compile Error: {}.", expr, jexlException.getMessage()); + throw jexlException; + } catch (Exception e) { + log.error("Alarm Rule: {} Unknown Error: {}.", expr, e.getMessage()); + throw e; + } + + try { + match = (Boolean) JexlExpressionRunner.evaluate(expression, fieldValueMap); + } catch (JexlException jexlException) { + log.error("Alarm Rule: {} Run Error: {}.", expr, jexlException.getMessage()); + throw jexlException; + } catch (Exception e) { + log.error("Alarm Rule: {} Unknown Error: {}.", expr, e.getMessage()); + throw e; + } + return match != null && match; + } + + private void handlerAvailableMetrics(long monitorId, String app, CollectRep.MetricsData metricsData) { + if (metricsData.getCode() == CollectRep.Code.TIMEOUT) { + return; + } + // TODO CACHE getMonitorBindAlertAvaDefine + AlertDefine avaAlertDefine = alertDefineService.getMonitorBindAlertAvaDefine(monitorId, app, CommonConstants.AVAILABILITY); + if (avaAlertDefine == null) { + return; + } + long currentTimeMill = System.currentTimeMillis(); + if (metricsData.getCode() != CollectRep.Code.SUCCESS) { + Alert preAlert = triggeredAlertMap.get(String.valueOf(monitorId)); + Map tags = new HashMap<>(6); + tags.put(TAG_MONITOR_ID, String.valueOf(monitorId)); + tags.put(TAG_MONITOR_APP, app); + tags.put(CommonConstants.TAG_THRESHOLD_ID, String.valueOf(avaAlertDefine.getId())); + tags.put(TAG_METRICS, CommonConstants.AVAILABILITY); + tags.put(TAG_CODE, metricsData.getCode().name()); + Map valueMap = tags.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + if (!CollectionUtils.isEmpty(avaAlertDefine.getTags())) { + for (TagItem tagItem : avaAlertDefine.getTags()) { + valueMap.put(tagItem.getName(), tagItem.getValue()); + tags.put(tagItem.getName(), tagItem.getValue()); + } + } + if (preAlert == null) { + Alert.AlertBuilder alertBuilder = Alert.builder() + .tags(tags) + .priority(avaAlertDefine.getPriority()) + .status(ALERT_STATUS_CODE_NOT_REACH) + .target(CommonConstants.AVAILABILITY) + .content(AlertTemplateUtil.render(avaAlertDefine.getTemplate(), valueMap)) + .firstAlarmTime(currentTimeMill) + .lastAlarmTime(currentTimeMill) + .triggerTimes(1); + if (avaAlertDefine.getTimes() == null || avaAlertDefine.getTimes() <= 1) { + String notResolvedAlertKey = monitorId + CommonConstants.AVAILABILITY; + alertBuilder.status(ALERT_STATUS_CODE_PENDING); + notRecoveredAlertMap.put(notResolvedAlertKey, alertBuilder.build()); + alarmCommonReduce.reduceAndSendAlarm(alertBuilder.build()); + } else { + triggeredAlertMap.put(String.valueOf(monitorId), alertBuilder.build()); + } + } else { + int times = preAlert.getTriggerTimes() + 1; + preAlert.setTriggerTimes(times); + preAlert.setFirstAlarmTime(currentTimeMill); + preAlert.setLastAlarmTime(currentTimeMill); + int defineTimes = avaAlertDefine.getTimes() == null ? 1 : avaAlertDefine.getTimes(); + if (times >= defineTimes) { + preAlert.setStatus(ALERT_STATUS_CODE_PENDING); + String notResolvedAlertKey = monitorId + CommonConstants.AVAILABILITY; + notRecoveredAlertMap.put(notResolvedAlertKey, preAlert.clone()); + alarmCommonReduce.reduceAndSendAlarm(preAlert.clone()); + triggeredAlertMap.remove(String.valueOf(monitorId)); + } + } + } else { + // Check whether an availability or unreachable alarm is generated before the association monitoring + // and send a clear alarm to clear the monitoring status + triggeredAlertMap.remove(String.valueOf(monitorId)); + String notResolvedAlertKey = monitorId + CommonConstants.AVAILABILITY; + Alert notResolvedAlert = notRecoveredAlertMap.remove(notResolvedAlertKey); + if (notResolvedAlert != null) { + // Sending an alarm Restore + Map tags = notResolvedAlert.getTags(); + if (!avaAlertDefine.isRecoverNotice()) { + tags.put(CommonConstants.IGNORE, CommonConstants.IGNORE); + } + String content = this.bundle.getString("alerter.availability.recover"); + Alert resumeAlert = Alert.builder() + .tags(tags) + .target(CommonConstants.AVAILABILITY) + .content(content) + .priority(CommonConstants.ALERT_PRIORITY_CODE_WARNING) + .status(CommonConstants.ALERT_STATUS_CODE_RESTORED) + .firstAlarmTime(currentTimeMill) + .lastAlarmTime(notResolvedAlert.getLastAlarmTime()) + .triggerTimes(1) + .build(); + alarmCommonReduce.reduceAndSendAlarm(resumeAlert); + Runnable updateStatusJob = () -> { + // todo update pre all type alarm status + updateAvailabilityAlertStatus(monitorId, resumeAlert); + }; + workerPool.executeJob(updateStatusJob); + } + } + } + + private void updateAvailabilityAlertStatus(long monitorId, Alert restoreAlert) { + List availabilityAlerts = queryAvailabilityAlerts(monitorId, restoreAlert); + availabilityAlerts.stream().parallel().forEach(alert -> { + log.info("updating alert status solved id: {}", alert.getId()); + alertService.editAlertStatus(ALERT_STATUS_CODE_SOLVED, List.of(alert.getId())); + }); + } + + private List queryAvailabilityAlerts(long monitorId, Alert restoreAlert) { + //create query condition + Specification specification = (root, query, criteriaBuilder) -> { + List andList = new ArrayList<>(); + + Predicate predicateTags = criteriaBuilder.like(root.get("tags").as(String.class), "%" + monitorId + "%"); + andList.add(predicateTags); + + Predicate predicatePriority = criteriaBuilder.equal(root.get("priority"), CommonConstants.ALERT_PRIORITY_CODE_EMERGENCY); + andList.add(predicatePriority); + + Predicate predicateStatus = criteriaBuilder.equal(root.get("status"), ALERT_STATUS_CODE_PENDING); + andList.add(predicateStatus); + + Predicate predicateAlertTime = criteriaBuilder.lessThanOrEqualTo(root.get("lastAlarmTime"), restoreAlert.getLastAlarmTime()); + andList.add(predicateAlertTime); + + Predicate[] predicates = new Predicate[andList.size()]; + return criteriaBuilder.and(andList.toArray(predicates)); + }; + + //query results + return alertService.getAlerts(specification); + } + + @EventListener(SystemConfigChangeEvent.class) + public void onSystemConfigChangeEvent(SystemConfigChangeEvent event) { + log.info("calculate alarm receive system config change event: {}.", event.getSource()); + this.bundle = ResourceBundleUtil.getBundle("alerter"); + } + + @EventListener(MonitorDeletedEvent.class) + public void onMonitorDeletedEvent(MonitorDeletedEvent event) { + log.info("calculate alarm receive monitor {} has been deleted.", event.getMonitorId()); + this.triggeredAlertMap.remove(String.valueOf(event.getMonitorId())); + } + +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/config/AlerterAutoConfiguration.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/config/AlerterAutoConfiguration.java new file mode 100644 index 0000000..b0afa2c --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/config/AlerterAutoConfiguration.java @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.config; + +import org.springframework.context.annotation.ComponentScan; + +/** + * Alert auto configuration. + */ +@ComponentScan(basePackages = "org.apache.hertzbeat.alert") +public class AlerterAutoConfiguration { +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertConvergeController.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertConvergeController.java new file mode 100644 index 0000000..df38ea5 --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertConvergeController.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.controller; + +import static org.apache.hertzbeat.common.constants.CommonConstants.MONITOR_NOT_EXIST_CODE; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.apache.hertzbeat.alert.service.AlertConvergeService; +import org.apache.hertzbeat.common.entity.alerter.AlertConverge; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * Alarm Converge management API + */ +@Tag(name = "Alert Converge API") +@RestController +@RequestMapping(path = "/api/alert/converge", produces = {APPLICATION_JSON_VALUE}) +public class AlertConvergeController { + + @Autowired + private AlertConvergeService alertConvergeService; + + @PostMapping + @Operation(summary = "New Alarm Converge", description = "Added an alarm Converge") + public ResponseEntity> addNewAlertConverge(@Valid @RequestBody AlertConverge alertConverge) { + alertConvergeService.validate(alertConverge, false); + alertConvergeService.addAlertConverge(alertConverge); + return ResponseEntity.ok(Message.success("Add success")); + } + + @PutMapping + @Operation(summary = "Modifying an Alarm Converge", description = "Modify an existing alarm Converge") + public ResponseEntity> modifyAlertConverge(@Valid @RequestBody AlertConverge alertConverge) { + alertConvergeService.validate(alertConverge, true); + alertConvergeService.modifyAlertConverge(alertConverge); + return ResponseEntity.ok(Message.success("Modify success")); + } + + @GetMapping(path = "/{id}") + @Operation(summary = "Querying Alarm Converge", + description = "You can obtain alarm Converge information based on the alarm Converge ID") + public ResponseEntity> getAlertConverge( + @Parameter(description = "Alarm Converge ID", example = "6565463543") @PathVariable("id") long id) { + AlertConverge alertConverge = alertConvergeService.getAlertConverge(id); + if (alertConverge == null) { + return ResponseEntity.ok(Message.fail(MONITOR_NOT_EXIST_CODE, "AlertConverge not exist.")); + } else { + return ResponseEntity.ok(Message.success(alertConverge)); + } + } + +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertConvergesController.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertConvergesController.java new file mode 100644 index 0000000..3d6fb58 --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertConvergesController.java @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.controller; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.Predicate; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import org.apache.hertzbeat.alert.service.AlertConvergeService; +import org.apache.hertzbeat.common.entity.alerter.AlertConverge; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.http.ResponseEntity; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * Converge the batch API for alarms + */ +@Tag(name = "Alert Converge Batch API") +@RestController +@RequestMapping(path = "/api/alert/converges", produces = {APPLICATION_JSON_VALUE}) +public class AlertConvergesController { + + @Autowired + private AlertConvergeService alertConvergeService; + + @GetMapping + @Operation(summary = "Query the alarm converge list", + description = "You can obtain the list of alarm converge by querying filter items") + public ResponseEntity>> getAlertConverges( + @Parameter(description = "Alarm Converge ID", example = "6565463543") @RequestParam(required = false) List ids, + @Parameter(description = "Search Name", example = "x") @RequestParam(required = false) String search, + @Parameter(description = "Sort field, default id", example = "id") @RequestParam(defaultValue = "id") String sort, + @Parameter(description = "Sort mode: asc: ascending, desc: descending", example = "desc") @RequestParam(defaultValue = "desc") String order, + @Parameter(description = "List current page", example = "0") @RequestParam(defaultValue = "0") int pageIndex, + @Parameter(description = "Number of list pages", example = "8") @RequestParam(defaultValue = "8") int pageSize) { + + Specification specification = (root, query, criteriaBuilder) -> { + List andList = new ArrayList<>(); + if (ids != null && !ids.isEmpty()) { + CriteriaBuilder.In inPredicate = criteriaBuilder.in(root.get("id")); + for (long id : ids) { + inPredicate.value(id); + } + andList.add(inPredicate); + } + if (StringUtils.hasText(search)) { + Predicate predicate = criteriaBuilder.or( + criteriaBuilder.like( + criteriaBuilder.lower(root.get("name")), + "%" + search.toLowerCase() + "%" + ) + ); + andList.add(predicate); + } + Predicate[] predicates = new Predicate[andList.size()]; + return criteriaBuilder.and(andList.toArray(predicates)); + }; + Sort sortExp = Sort.by(new Sort.Order(Sort.Direction.fromString(order), sort)); + PageRequest pageRequest = PageRequest.of(pageIndex, pageSize, sortExp); + Page alertConvergePage = alertConvergeService.getAlertConverges(specification, pageRequest); + return ResponseEntity.ok(Message.success(alertConvergePage)); + } + + @DeleteMapping + @Operation(summary = "Delete alarm converge in batches", + description = "Delete alarm converge in batches based on the alarm converge ID list") + public ResponseEntity> deleteAlertDefines( + @Parameter(description = "Alarm Converge IDs", example = "6565463543") @RequestParam(required = false) List ids + ) { + if (ids != null && !ids.isEmpty()) { + alertConvergeService.deleteAlertConverges(new HashSet<>(ids)); + } + return ResponseEntity.ok(Message.success()); + } + +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertDefineController.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertDefineController.java new file mode 100644 index 0000000..33f4292 --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertDefineController.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.controller; + +import static org.apache.hertzbeat.common.constants.CommonConstants.MONITOR_NOT_EXIST_CODE; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import java.util.List; +import java.util.stream.Collectors; +import org.apache.hertzbeat.alert.service.AlertDefineService; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.apache.hertzbeat.common.entity.alerter.AlertDefineMonitorBind; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * Alarm definition management API + */ +@Tag(name = "Alert Define API") +@RestController +@RequestMapping(path = "/api/alert/define", produces = {APPLICATION_JSON_VALUE}) +public class AlertDefineController { + + @Autowired + private AlertDefineService alertDefineService; + + @PostMapping + @Operation(summary = "New Alarm Definition", description = "Added an alarm definition") + public ResponseEntity> addNewAlertDefine(@Valid @RequestBody AlertDefine alertDefine) { + // Verify request data + alertDefineService.validate(alertDefine, false); + alertDefineService.addAlertDefine(alertDefine); + return ResponseEntity.ok(Message.success("Add success")); + } + + @PutMapping + @Operation(summary = "Modifying an Alarm Definition", description = "Modify an existing alarm definition") + public ResponseEntity> modifyAlertDefine(@Valid @RequestBody AlertDefine alertDefine) { + // Verify request data + alertDefineService.validate(alertDefine, true); + alertDefineService.modifyAlertDefine(alertDefine); + return ResponseEntity.ok(Message.success("Modify success")); + } + + @GetMapping(path = "/{id}") + @Operation(summary = "Querying Alarm Definitions", + description = "You can obtain alarm definition information based on the alarm definition ID") + public ResponseEntity> getAlertDefine( + @Parameter(description = "Alarm Definition ID", example = "6565463543") @PathVariable("id") long id) { + // Obtaining Monitoring Information + AlertDefine alertDefine = alertDefineService.getAlertDefine(id); + if (alertDefine == null) { + return ResponseEntity.ok(Message.fail(MONITOR_NOT_EXIST_CODE, "AlertDefine not exist.")); + } else { + return ResponseEntity.ok(Message.success(alertDefine)); + } + } + + @DeleteMapping(path = "/{id}") + @Operation(summary = "Deleting an Alarm Definition", + description = "If the alarm definition does not exist, the alarm is deleted successfully") + public ResponseEntity> deleteAlertDefine( + @Parameter(description = "Alarm Definition ID", example = "6565463543") @PathVariable("id") long id) { + // If the alarm definition does not exist or is deleted successfully, the deletion succeeds + alertDefineService.deleteAlertDefine(id); + return ResponseEntity.ok(Message.success("Delete success")); + } + + @PostMapping(path = "/{alertDefineId}/monitors") + @Operation(summary = "Application alarm definition is associated with monitoring", + description = "Applies the association between specified alarm definitions and monitoring") + public ResponseEntity> applyAlertDefineMonitorsBind( + @Parameter(description = "Alarm Definition ID", example = "6565463543") @PathVariable("alertDefineId") long alertDefineId, + @RequestBody List alertDefineMonitorBinds) { + alertDefineService.applyBindAlertDefineMonitors(alertDefineId, alertDefineMonitorBinds); + return ResponseEntity.ok(Message.success("Apply success")); + } + + @GetMapping(path = "/{alertDefineId}/monitors") + @Operation(summary = "Application alarm definition is associated with monitoring", + description = "Applies the association between specified alarm definitions and monitoring") + public ResponseEntity>> getAlertDefineMonitorsBind( + @Parameter(description = "Alarm Definition ID", example = "6565463543") @PathVariable("alertDefineId") long alertDefineId) { + List defineBinds = alertDefineService.getBindAlertDefineMonitors(alertDefineId); + defineBinds = defineBinds.stream().filter(item -> item.getMonitor() != null).collect(Collectors.toList()); + return ResponseEntity.ok(Message.success(defineBinds)); + } + +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertDefinesController.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertDefinesController.java new file mode 100644 index 0000000..2150f06 --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertDefinesController.java @@ -0,0 +1,145 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.controller; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.Predicate; +import jakarta.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import org.apache.hertzbeat.alert.service.AlertDefineService; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.http.ResponseEntity; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +/** + * Define the batch API for alarms + */ +@Tag(name = "Alert Define Batch API") +@RestController +@RequestMapping(path = "/api/alert/defines", produces = {APPLICATION_JSON_VALUE}) +public class AlertDefinesController { + + @Autowired + private AlertDefineService alertDefineService; + + @GetMapping + @Operation(summary = "Example Query the alarm definition list", + description = "You can obtain the list of alarm definitions by querying filter items") + public ResponseEntity>> getAlertDefines( + @Parameter(description = "Alarm Definition ID", example = "6565463543") @RequestParam(required = false) List ids, + @Parameter(description = "Search-Target Expr Template", example = "x") @RequestParam(required = false) String search, + @Parameter(description = "Alarm Definition Severity", example = "6565463543") @RequestParam(required = false) Byte priority, + @Parameter(description = "Sort field, default id", example = "id") @RequestParam(defaultValue = "id") String sort, + @Parameter(description = "Sort mode: asc: ascending, desc: descending", example = "desc") @RequestParam(defaultValue = "desc") String order, + @Parameter(description = "List current page", example = "0") @RequestParam(defaultValue = "0") int pageIndex, + @Parameter(description = "Number of list pages", example = "8") @RequestParam(defaultValue = "8") int pageSize) { + + Specification specification = (root, query, criteriaBuilder) -> { + List andList = new ArrayList<>(); + if (ids != null && !ids.isEmpty()) { + CriteriaBuilder.In inPredicate = criteriaBuilder.in(root.get("id")); + for (long id : ids) { + inPredicate.value(id); + } + andList.add(inPredicate); + } + if (StringUtils.hasText(search)) { + Predicate predicate = criteriaBuilder.or( + criteriaBuilder.like( + criteriaBuilder.lower(root.get("app")), + "%" + search.toLowerCase() + "%" + ), + criteriaBuilder.like( + criteriaBuilder.lower(root.get("metric")), + "%" + search.toLowerCase() + "%" + ), + criteriaBuilder.like( + criteriaBuilder.lower(root.get("field")), + "%" + search.toLowerCase() + "%" + ), + criteriaBuilder.like( + criteriaBuilder.lower(root.get("expr")), + "%" + search.toLowerCase() + "%" + ), + criteriaBuilder.like( + criteriaBuilder.lower(root.get("template")), + "%" + search.toLowerCase() + "%" + ) + ); + andList.add(predicate); + } + if (priority != null) { + Predicate predicate = criteriaBuilder.equal(root.get("priority"), priority); + andList.add(predicate); + } + Predicate[] predicates = new Predicate[andList.size()]; + return criteriaBuilder.and(andList.toArray(predicates)); + }; + Sort sortExp = Sort.by(new Sort.Order(Sort.Direction.fromString(order), sort)); + PageRequest pageRequest = PageRequest.of(pageIndex, pageSize, sortExp); + Page alertDefinePage = alertDefineService.getAlertDefines(specification, pageRequest); + return ResponseEntity.ok(Message.success(alertDefinePage)); + } + + @DeleteMapping + @Operation(summary = "Delete alarm definitions in batches", + description = "Delete alarm definitions in batches based on the alarm definition ID list") + public ResponseEntity> deleteAlertDefines( + @Parameter(description = "Alarm Definition IDs", example = "6565463543") @RequestParam(required = false) List ids + ) { + if (ids != null && !ids.isEmpty()) { + alertDefineService.deleteAlertDefines(new HashSet<>(ids)); + } + return ResponseEntity.ok(Message.success()); + } + + @GetMapping("/export") + @Operation(summary = "export alertDefine config", description = "export alarm definition configuration") + public void export( + @Parameter(description = "AlertDefine ID List", example = "656937901") @RequestParam List ids, + @Parameter(description = "Export Type:JSON,EXCEL,YAML") @RequestParam(defaultValue = "JSON") String type, + HttpServletResponse res) throws Exception { + alertDefineService.export(ids, type, res); + } + + @PostMapping("/import") + @Operation(summary = "import alertDefine config", description = "import alarm definition configuration") + public ResponseEntity> importDefines(MultipartFile file) throws Exception { + alertDefineService.importConfig(file); + return ResponseEntity.ok(Message.success("Import success")); + } +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertReportController.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertReportController.java new file mode 100644 index 0000000..89b67fd --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertReportController.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.controller; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.Date; +import java.util.Optional; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.dto.CloudAlertReportAbstract; +import org.apache.hertzbeat.alert.dto.GeneralCloudAlertReport; +import org.apache.hertzbeat.alert.enums.CloudServiceAlarmInformationEnum; +import org.apache.hertzbeat.alert.service.AlertService; +import org.apache.hertzbeat.common.entity.dto.AlertReport; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * Extern Alarm Manage API + */ +@Tag(name = "Extern Alarm Manage API") +@RestController +@RequestMapping(path = "/api/alerts/report", produces = {APPLICATION_JSON_VALUE}) +@Slf4j +public class AlertReportController { + + @Autowired + private AlertService alertService; + + @PostMapping("/{cloud}") + @Operation(summary = "Interface for reporting external alarm information of cloud service") + public ResponseEntity> addNewAlertReportFromCloud(@PathVariable("cloud") String cloudServiceName, + @RequestBody String alertReport) { + CloudServiceAlarmInformationEnum cloudService = CloudServiceAlarmInformationEnum + .getEnumFromCloudServiceName(cloudServiceName); + + AlertReport alert = null; + if (cloudService != null) { + try { + CloudAlertReportAbstract cloudAlertReport = JsonUtil + .fromJson(alertReport, cloudService.getCloudServiceAlarmInformationEntity()); + assert cloudAlertReport != null; + alert = AlertReport.builder() + .content(cloudAlertReport.getContent()) + .alertName(cloudAlertReport.getAlertName()) + .alertTime(cloudAlertReport.getAlertTime()) + .alertDuration(cloudAlertReport.getAlertDuration()) + .priority(cloudAlertReport.getPriority()) + .reportType(cloudAlertReport.getReportType()) + .labels(cloudAlertReport.getLabels()) + .annotations(cloudAlertReport.getAnnotations()) + .build(); + } catch (Exception e) { + log.error("[alert report] parse cloud service alarm content failed! cloud service: {} conrent: {}", + cloudService.name(), alertReport); + } + } else { + alert = AlertReport.builder() + .content("error do not has cloud service api") + .alertName("/api/alerts/report/" + cloudServiceName) + .alertTime(new Date().getTime()) + .priority(1) + .reportType(1) + .build(); + } + Optional.ofNullable(alert).ifPresent(alertReportPresent -> + alertService.addNewAlertReport(alertReportPresent)); + return ResponseEntity.ok(Message.success("Add report success")); + } + + @PostMapping + @Operation(summary = "Interface for reporting external and general alarm information", + description = "The interface is used to report external and general alarm information") + public ResponseEntity> addNewAlertReport(@RequestBody GeneralCloudAlertReport alertReport) { + alertReport.refreshAlertTime(); + alertService.addNewAlertReport(alertReport); + return ResponseEntity.ok(Message.success("Add report success")); + } +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertSilenceController.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertSilenceController.java new file mode 100644 index 0000000..56e9af3 --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertSilenceController.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.controller; + +import static org.apache.hertzbeat.common.constants.CommonConstants.MONITOR_NOT_EXIST_CODE; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.apache.hertzbeat.alert.service.AlertSilenceService; +import org.apache.hertzbeat.common.entity.alerter.AlertSilence; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * Alarm Silence management API + */ +@Tag(name = "Alert Silence API") +@RestController +@RequestMapping(path = "/api/alert/silence", produces = {APPLICATION_JSON_VALUE}) +public class AlertSilenceController { + + @Autowired + private AlertSilenceService alertSilenceService; + + @PostMapping + @Operation(summary = "New Alarm Silence", description = "Added an alarm Silence") + public ResponseEntity> addNewAlertSilence(@Valid @RequestBody AlertSilence alertSilence) { + alertSilenceService.validate(alertSilence, false); + alertSilenceService.addAlertSilence(alertSilence); + return ResponseEntity.ok(Message.success("Add success")); + } + + @PutMapping + @Operation(summary = "Modifying an Alarm Silence", description = "Modify an existing alarm Silence") + public ResponseEntity> modifyAlertSilence(@Valid @RequestBody AlertSilence alertSilence) { + alertSilenceService.validate(alertSilence, true); + alertSilenceService.modifyAlertSilence(alertSilence); + return ResponseEntity.ok(Message.success("Modify success")); + } + + @GetMapping(path = "/{id}") + @Operation(summary = "Querying Alarm Silence", + description = "You can obtain alarm Silence information based on the alarm Silence ID") + public ResponseEntity> getAlertSilence( + @Parameter(description = "Alarm Silence ID", example = "6565463543") @PathVariable("id") long id) { + AlertSilence alertSilence = alertSilenceService.getAlertSilence(id); + if (alertSilence == null) { + return ResponseEntity.ok(Message.fail(MONITOR_NOT_EXIST_CODE, "AlertSilence not exist.")); + } else { + return ResponseEntity.ok(Message.success(alertSilence)); + } + } + +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertSilencesController.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertSilencesController.java new file mode 100644 index 0000000..60fa156 --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertSilencesController.java @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.controller; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.Predicate; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import org.apache.hertzbeat.alert.service.AlertSilenceService; +import org.apache.hertzbeat.common.entity.alerter.AlertSilence; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.http.ResponseEntity; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * Silence the batch API for alarms + */ +@Tag(name = "Alert Silence Batch API") +@RestController +@RequestMapping(path = "/api/alert/silences", produces = {APPLICATION_JSON_VALUE}) +public class AlertSilencesController { + + @Autowired + private AlertSilenceService alertSilenceService; + + @GetMapping + @Operation(summary = "Query the alarm silence list", + description = "You can obtain the list of alarm silence by querying filter items") + public ResponseEntity>> getAlertSilences( + @Parameter(description = "Alarm Silence ID", example = "6565463543") @RequestParam(required = false) List ids, + @Parameter(description = "Search Name", example = "x") @RequestParam(required = false) String search, + @Parameter(description = "Sort field, default id", example = "id") @RequestParam(defaultValue = "id") String sort, + @Parameter(description = "Sort mode: asc: ascending, desc: descending", example = "desc") @RequestParam(defaultValue = "desc") String order, + @Parameter(description = "List current page", example = "0") @RequestParam(defaultValue = "0") int pageIndex, + @Parameter(description = "Number of list pages", example = "8") @RequestParam(defaultValue = "8") int pageSize) { + + Specification specification = (root, query, criteriaBuilder) -> { + List andList = new ArrayList<>(); + if (ids != null && !ids.isEmpty()) { + CriteriaBuilder.In inPredicate = criteriaBuilder.in(root.get("id")); + for (long id : ids) { + inPredicate.value(id); + } + andList.add(inPredicate); + } + if (StringUtils.hasText(search)) { + Predicate predicate = criteriaBuilder.or( + criteriaBuilder.like( + criteriaBuilder.lower(root.get("name")), + "%" + search.toLowerCase() + "%" + ) + ); + andList.add(predicate); + } + Predicate[] predicates = new Predicate[andList.size()]; + return criteriaBuilder.and(andList.toArray(predicates)); + }; + Sort sortExp = Sort.by(new Sort.Order(Sort.Direction.fromString(order), sort)); + PageRequest pageRequest = PageRequest.of(pageIndex, pageSize, sortExp); + Page alertSilencePage = alertSilenceService.getAlertSilences(specification, pageRequest); + Message> message = Message.success(alertSilencePage); + return ResponseEntity.ok(message); + } + + @DeleteMapping + @Operation(summary = "Delete alarm silence in batches", + description = "Delete alarm silence in batches based on the alarm silence ID list") + public ResponseEntity> deleteAlertDefines( + @Parameter(description = "Alarm Silence IDs", example = "6565463543") @RequestParam(required = false) List ids + ) { + if (ids != null && !ids.isEmpty()) { + alertSilenceService.deleteAlertSilences(new HashSet<>(ids)); + } + Message message = Message.success(); + return ResponseEntity.ok(message); + } + +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertsController.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertsController.java new file mode 100644 index 0000000..65aac1e --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/controller/AlertsController.java @@ -0,0 +1,146 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.controller; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.Predicate; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import org.apache.hertzbeat.alert.dto.AlertSummary; +import org.apache.hertzbeat.alert.service.AlertService; +import org.apache.hertzbeat.common.entity.alerter.Alert; +import org.apache.hertzbeat.common.entity.dto.Message; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * Alarm Management API + */ +@Tag(name = "Alarm Manage Batch API") +@RestController +@RequestMapping(path = "/api/alerts", produces = {APPLICATION_JSON_VALUE}) +public class AlertsController { + + @Autowired + private AlertService alertService; + + @GetMapping + @Operation(summary = "Get a list of alarm information based on query filter items", description = "according to the query filter items to obtain a list of alarm information") + public ResponseEntity>> getAlerts( + @Parameter(description = "Alarm ID List", example = "6565466456") @RequestParam(required = false) List ids, + @Parameter(description = "Alarm monitor object ID", example = "6565463543") @RequestParam(required = false) Long monitorId, + @Parameter(description = "Alarm level", example = "6565463543") @RequestParam(required = false) Byte priority, + @Parameter(description = "Alarm Status", example = "6565463543") @RequestParam(required = false) Byte status, + @Parameter(description = "Alarm content fuzzy query", example = "linux") @RequestParam(required = false) String content, + @Parameter(description = "Sort field, default id", example = "name") @RequestParam(defaultValue = "id") String sort, + @Parameter(description = "Sort Type", example = "desc") @RequestParam(defaultValue = "desc") String order, + @Parameter(description = "List current page", example = "0") @RequestParam(defaultValue = "0") int pageIndex, + @Parameter(description = "Number of list pagination", example = "8") @RequestParam(defaultValue = "8") int pageSize) { + + Specification specification = (root, query, criteriaBuilder) -> { + List andList = new ArrayList<>(); + + if (ids != null && !ids.isEmpty()) { + CriteriaBuilder.In inPredicate = criteriaBuilder.in(root.get("id")); + for (long id : ids) { + inPredicate.value(id); + } + andList.add(inPredicate); + } + if (monitorId != null) { + Predicate predicate = criteriaBuilder.like(root.get("tags").as(String.class), "%" + monitorId + "%"); + andList.add(predicate); + } + if (priority != null) { + Predicate predicate = criteriaBuilder.equal(root.get("priority"), priority); + andList.add(predicate); + } + if (status != null) { + Predicate predicate = criteriaBuilder.equal(root.get("status"), status); + andList.add(predicate); + } + if (content != null && !content.isEmpty()) { + Predicate predicateContent = criteriaBuilder.like(root.get("content"), "%" + content + "%"); + andList.add(predicateContent); + } + Predicate[] predicates = new Predicate[andList.size()]; + return criteriaBuilder.and(andList.toArray(predicates)); + }; + Sort sortExp = Sort.by(new Sort.Order(Sort.Direction.fromString(order), sort)); + PageRequest pageRequest = PageRequest.of(pageIndex, pageSize, sortExp); + Page alertPage = alertService.getAlerts(specification, pageRequest); + Message> message = Message.success(alertPage); + return ResponseEntity.ok(message); + } + + @DeleteMapping + @Operation(summary = "Delete alarms in batches", description = "according to the alarm ID list to delete the alarm information in batches") + public ResponseEntity> deleteAlerts( + @Parameter(description = "Alarm List ID", example = "6565463543") @RequestParam(required = false) List ids) { + if (ids != null && !ids.isEmpty()) { + alertService.deleteAlerts(new HashSet<>(ids)); + } + Message message = Message.success(); + return ResponseEntity.ok(message); + } + + @DeleteMapping("/clear") + @Operation(summary = "Delete alarms in batches", description = "delete all alarm information") + public ResponseEntity> clearAllAlerts() { + alertService.clearAlerts(); + Message message = Message.success(); + return ResponseEntity.ok(message); + } + + @PutMapping(path = "/status/{status}") + @Operation(summary = "Batch modify alarm status, set read and unread", description = "Batch modify alarm status, set read and unread") + public ResponseEntity> applyAlertDefinesStatus( + @Parameter(description = "Alarm status value", example = "0") @PathVariable Byte status, + @Parameter(description = "Alarm List IDS", example = "6565463543") @RequestParam(required = false) List ids) { + if (ids != null && status != null && !ids.isEmpty()) { + alertService.editAlertStatus(status, ids); + } + Message message = Message.success(); + return ResponseEntity.ok(message); + } + + @GetMapping(path = "/summary") + @Operation(summary = "Get alarm statistics", description = "Get alarm statistics information") + public ResponseEntity> getAlertsSummary() { + AlertSummary alertSummary = alertService.getAlertsSummary(); + Message message = Message.success(alertSummary); + return ResponseEntity.ok(message); + } + +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertConvergeDao.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertConvergeDao.java new file mode 100644 index 0000000..b638f0d --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertConvergeDao.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.dao; + +import java.util.Set; +import org.apache.hertzbeat.common.entity.alerter.AlertConverge; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; + +/** + * AlertConverge Dao + */ +public interface AlertConvergeDao extends JpaRepository, JpaSpecificationExecutor { + + /** + * Delete alarm converge based on the ID list + * @param convergeIds alert converge id list + */ + @Modifying + void deleteAlertConvergesByIdIn(Set convergeIds); +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertDao.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertDao.java new file mode 100644 index 0000000..80603d6 --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertDao.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.dao; + +import java.util.List; +import java.util.Set; +import org.apache.hertzbeat.alert.dto.AlertPriorityNum; +import org.apache.hertzbeat.common.entity.alerter.Alert; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +/** + * Alert Database Operations + */ +public interface AlertDao extends JpaRepository, JpaSpecificationExecutor { + + /** + * Delete alerts based on ID list + * @param alertIds Alert ID List + */ + void deleteAlertsByIdIn(Set alertIds); + + /** + * Updates the alarm status based on the alarm ID-status value + * @param status status value + * @param ids alarm ids + */ + @Modifying + @Query("update Alert set status = :status where id in :ids") + void updateAlertsStatus(@Param(value = "status") Byte status, @Param(value = "ids") List ids); + + /** + * Query the number of unhandled alarms of each alarm severity + * @return List of alerts num + */ + @Query("select new org.apache.hertzbeat.alert.dto.AlertPriorityNum(mo.priority, count(mo.id)) from Alert mo where mo.status = 0 group by mo.priority") + List findAlertPriorityNum(); +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertDefineBindDao.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertDefineBindDao.java new file mode 100644 index 0000000..76bb187 --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertDefineBindDao.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.dao; + +import java.util.List; +import java.util.Set; +import org.apache.hertzbeat.common.entity.alerter.AlertDefineMonitorBind; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +/** + * AlertDefineBind database operations + */ +public interface AlertDefineBindDao extends JpaRepository, JpaSpecificationExecutor { + + /** + * Delete the alarm definition and monitor association based on the alarm definition ID + * @param alertDefineId Alarm Definition ID + */ + void deleteAlertDefineBindsByAlertDefineIdEquals(Long alertDefineId); + + /** + * Deleting alarms based on monitoring IDs defines monitoring associations + * @param monitorId Monitor Id + */ + void deleteAlertDefineMonitorBindsByMonitorIdEquals(Long monitorId); + + /** + * Delete alarm definition monitoring association based on monitoring ID list + * @param monitorIds Monitoring ID List + */ + void deleteAlertDefineMonitorBindsByMonitorIdIn(Set monitorIds); + + /** + * Query monitoring related information based on alarm definition ID + * @param alertDefineId Alarm Definition ID + * @return Associated monitoring information + */ + List getAlertDefineBindsByAlertDefineIdEquals(Long alertDefineId); +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertDefineDao.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertDefineDao.java new file mode 100644 index 0000000..8a13bee --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertDefineDao.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.dao; + +import java.util.List; +import java.util.Set; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +/** + * AlertDefine Dao + */ +public interface AlertDefineDao extends JpaRepository, JpaSpecificationExecutor { + + /** + * Delete alarm definitions based on the ID list + * @param alertDefineIds alarm define ids + */ + void deleteAlertDefinesByIdIn(Set alertDefineIds); + + /** + * Query the default alarm thresholds based on the monitoring metrics type + * @param app monitoring type + * @param metric metrics + * @return alarm defines + */ + List queryAlertDefinesByAppAndMetricAndPresetTrueAndEnableTrue(String app, String metric); + + /** + * Query app metric alert define + * @param app app + * @param metric metric + * @return alert define + */ + List queryAlertDefineByAppAndMetric(String app, String metric); + + /** + * Query the alarm definition list associated with the monitoring ID + * @param monitorId monitor id + * @param app monitor type + * @param metrics metrics + * @return Alarm Definition List + */ + @Query("select define from AlertDefine define join AlertDefineMonitorBind bind on bind.alertDefineId = define.id " + + "where bind.monitorId = :monitorId and define.app = :app and define.metric = :metrics and define.enable = true and define.preset = false") + List queryAlertDefinesByMonitor(@Param(value = "monitorId") Long monitorId, + @Param(value = "app") String app, + @Param(value = "metrics") String metrics); +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertMonitorDao.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertMonitorDao.java new file mode 100644 index 0000000..8fd7614 --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertMonitorDao.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.dao; + +import java.util.List; +import org.apache.hertzbeat.common.entity.manager.Monitor; +import org.apache.hertzbeat.common.entity.manager.Tag; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +/** + * Alert Monitor Dao + */ +public interface AlertMonitorDao extends JpaRepository, JpaSpecificationExecutor { + + /** + * Query the monitoring status of a specified monitoring state + * @param status status value + * @return Monitor the list + */ + List findMonitorsByStatus(Byte status); + + /** + * find monitor bind tags by monitorId + * @param monitorId monitorId + * @return bind tags + */ + @Query("select tag from Tag tag join TagMonitorBind bind on bind.tagId = tag.id where bind.monitorId = :monitorId") + List findMonitorIdBindTags(@Param(value = "monitorId") Long monitorId); +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertSilenceDao.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertSilenceDao.java new file mode 100644 index 0000000..4a409b8 --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/dao/AlertSilenceDao.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.dao; + +import java.util.Set; +import org.apache.hertzbeat.common.entity.alerter.AlertSilence; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; + +/** + * AlertSilence Dao + */ +public interface AlertSilenceDao extends JpaRepository, JpaSpecificationExecutor { + + /** + * Delete alarm silence based on the ID list + * + * @param silenceIds alert silence id list + */ + @Modifying + void deleteAlertSilencesByIdIn(Set silenceIds); +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/dto/AlertPriorityNum.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/dto/AlertPriorityNum.java new file mode 100644 index 0000000..8e85eff --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/dto/AlertPriorityNum.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; + +/** + * Number of monitoring level alarms + */ +@Data +@AllArgsConstructor +public class AlertPriorityNum { + + /** + * Alarm level + */ + private byte priority; + + /** + * Alarm count + */ + private long num; +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/dto/AlertSummary.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/dto/AlertSummary.java new file mode 100644 index 0000000..f75a680 --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/dto/AlertSummary.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.dto; + +import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + + +/** + * Alarm Statistics Information + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Alarm Statistics Information") +public class AlertSummary { + + @Schema(title = "Total number of alerts (including processed and unprocessed alerts)", + example = "134", accessMode = READ_ONLY) + private long total; + + @Schema(title = "Number of alerts handled", + example = "34", accessMode = READ_ONLY) + private long dealNum; + + @Schema(title = "Alarm handling rate", + example = "39.34", accessMode = READ_ONLY) + private float rate; + + @Schema(title = "Number of alarms whose alarm severity is warning alarms (referring to unhandled alarms)", + example = "43", accessMode = READ_ONLY) + private long priorityWarningNum; + + @Schema(title = "Number of alarms whose alarm severity is critical alarms (referring to unhandled alarms)", + example = "56", accessMode = READ_ONLY) + private long priorityCriticalNum; + + @Schema(title = "Number of alarms whose alarm severity is urgent alarms (referring to unhandled alarms)", + example = "23", accessMode = READ_ONLY) + private long priorityEmergencyNum; +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/dto/CloudAlertReportAbstract.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/dto/CloudAlertReportAbstract.java new file mode 100644 index 0000000..eeecd5a --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/dto/CloudAlertReportAbstract.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.dto; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.Map; +import org.apache.hertzbeat.common.entity.dto.AlertReport; + +/** + * Abstract parent of cloud service alarm information + * {@link AlertReport} - The method corresponds to the parameter of this class one by one. For details about the method, see this class + */ +public abstract class CloudAlertReportAbstract { + + /** + * This parameter specifies the alarm name when a cloud alarm is converted to an internal alarm + */ + @JsonIgnore + public abstract String getAlertName(); + + /** + * Interval when a cloud alarm is converted to an internal alarm + */ + @JsonIgnore + public abstract Integer getAlertDuration(); + + /** + * Specifies the alarm time when a cloud alarm is converted to an internal alarm + */ + @JsonIgnore + public abstract long getAlertTime(); + + /** + * Severity of an alarm when a cloud alarm is converted to an internal alarm + */ + @JsonIgnore + public abstract Integer getPriority(); + + /** + * This parameter specifies the alarm type when a cloud alarm is converted to an internal alarm + */ + @JsonIgnore + public abstract Integer getReportType(); + + /** + * Alarm label information about a cloud alarm that is converted to an internal alarm + */ + @JsonIgnore + public abstract Map getLabels(); + + /** + * Alarm labels when a cloud alarm is converted to an internal alarm + */ + @JsonIgnore + public abstract Map getAnnotations(); + + /** + * This topic describes the alarm content when a cloud alarm is converted to an internal alarm + */ + @JsonIgnore + public abstract String getContent(); + +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/dto/GeneralCloudAlertReport.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/dto/GeneralCloudAlertReport.java new file mode 100644 index 0000000..8d71953 --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/dto/GeneralCloudAlertReport.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.dto; + +import java.util.Optional; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.alert.util.DateUtil; +import org.apache.hertzbeat.common.entity.dto.AlertReport; + +/** + * Generic cloud alarm entity class + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class GeneralCloudAlertReport extends AlertReport { + + /** + * Alarm date and time + */ + private String alertDateTime; + + /** + * DATE TIME FORMAT + */ + private String dateTimeFormat; + + /** + * You can refresh the timestamp of the alarm time with enhanced properties + */ + public void refreshAlertTime() { + if (getAlertTime() != 0L) { + return; + } + if (StringUtils.isNotBlank(alertDateTime)) { + Long timeStamp = null; + if (StringUtils.isNotBlank(dateTimeFormat)) { + Optional tsf = DateUtil.getTimeStampFromFormat(alertDateTime, dateTimeFormat); + boolean present = tsf.isPresent(); + if (present) { + timeStamp = tsf.get(); + } + } + if (timeStamp == null) { + Optional tsf = DateUtil.getTimeStampFromSomeFormats(alertDateTime); + boolean present = tsf.isPresent(); + if (present) { + timeStamp = tsf.get(); + } + } + if (timeStamp != null) { + setAlertTime(timeStamp); + return; + } + } + throw new RuntimeException("parse alarm time error"); + } + +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/dto/TenCloudAlertReport.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/dto/TenCloudAlertReport.java new file mode 100644 index 0000000..6ea51c1 --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/dto/TenCloudAlertReport.java @@ -0,0 +1,229 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.dto; + + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.io.Serializable; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.apache.hertzbeat.alert.util.DateUtil; + +/** + * + * Tencent cloud alarm entity class + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +@Builder +public class TenCloudAlertReport extends CloudAlertReportAbstract implements Serializable { + @JsonProperty("sessionID") + private String sessionId; + private String alarmStatus; + private String alarmType; + private AlarmObjInfo alarmObjInfo; + private AlarmPolicyInfo alarmPolicyInfo; + private String firstOccurTime; + private int durationTime; + private String recoverTime; + + /** + * Alarm Object information + */ + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class AlarmObjInfo { + private String region; + private String namespace; + @JsonProperty("appID") + private String appId; + private String uin; + private Dimensions dimensions; + } + + /** + * Uniform Resource ID information + */ + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class Dimensions { + @JsonProperty("unInstanceID") + private String unInstanceId; + @JsonProperty("objID") + private String objId; + } + + /** + * Alarm policy information + */ + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class AlarmPolicyInfo { + @JsonProperty("policyID") + private String policyId; + private String policyType; + private String policyName; + @JsonProperty("policyTypeCName") + private String policyTypeCname; + private Conditions conditions; + } + + /** + * Parameters of indicator alarms + */ + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class Conditions { + // alarm metrics parameters + private String metricName; + private String metricShowName; + private String calcType; + private String calcValue; + private String calcUnit; + private String currentValue; + private String historyValue; + private String unit; + private String period; + private String periodNum; + private String alarmNotifyType; + private long alarmNotifyPeriod; + + // alarm event parameters + private String productName; + private String productShowName; + private String eventName; + private String eventShowName; + } + + @Override + public String getAlertName() { + return "TenCloud|腾讯云"; + } + + @Override + public Integer getAlertDuration() { + return this.durationTime; + } + + @Override + public long getAlertTime() { + + return DateUtil.getTimeStampFromFormat(getFirstOccurTime(), "yyyy-MM-dd HH:mm:ss") + .orElse(0L); + } + + @Override + public Integer getPriority() { + return 1; + } + + @Override + public Integer getReportType() { + return 1; + } + + @Override + public Map getLabels() { + return Map.of("app", "TenCloud"); + } + + @Override + public Map getAnnotations() { + return Map.of("app", "TenCloud"); + } + + /** + * Transaction alarm + */ + private static final String EVENT = "event"; + + /** + * Indicator alarm + */ + private static final String METRIC = "metric"; + + /** + * If the following alarm types increase, the entity class can be divided into a parent class and multiple subclasses, and then the method can be implemented in the subclass + * Since there are only two, it will not be split for the time being + */ + @Override + public String getContent() { + StringBuilder contentBuilder = new StringBuilder(); + // 判断类型 + if (EVENT.equals(getAlarmType())) { + contentBuilder + .append("[") + .append("告警状态 | ") + .append("0".equals(alarmStatus) ? "恢复" : "告警") + .append("]\n") + .append("[") + .append("告警对象信息 | ") + .append(getAlarmObjInfo().getRegion() == null ? "" : "region:" + getAlarmObjInfo().getRegion()) + .append(";").append("appId:").append(getAlarmObjInfo().getAppId()) + .append(";").append("uni:").append(getAlarmObjInfo().getUin()) + .append(";").append("unInstanceId:").append(getAlarmObjInfo().getDimensions().getUnInstanceId()) + .append("]\n") + .append("[") + .append("告警策略组信息 | ") + .append("名称:").append(getAlarmPolicyInfo().getPolicyName()) + .append(";") + .append("策略类型展示名称:").append(getAlarmPolicyInfo().getConditions().getProductName()) + .append(",").append(getAlarmPolicyInfo().getConditions().getProductShowName()) + .append(";") + .append("事件告警名称:").append(getAlarmPolicyInfo().getConditions().getEventName()) + .append(",").append(getAlarmPolicyInfo().getConditions().getEventShowName()) + .append("]"); + } else if (METRIC.equals(getAlarmType())) { + contentBuilder + .append("[") + .append("告警对象:") + .append(getAlarmObjInfo().getRegion() == null ? "" : getAlarmObjInfo().getRegion()) + .append(getAlarmObjInfo().getRegion() == null ? "" : "|") + .append(getAlarmObjInfo().getNamespace()) + .append("]") + .append("[") + .append("告警内容:") + .append(getAlarmPolicyInfo().getPolicyTypeCname()).append("|") + .append(getAlarmPolicyInfo().getConditions().getMetricShowName()).append("|") + .append(getAlarmPolicyInfo().getConditions().getMetricName()) + .append(getAlarmPolicyInfo().getConditions().getCalcType()) + .append(getAlarmPolicyInfo().getConditions().getCalcValue()) + .append(getAlarmPolicyInfo().getConditions().getCalcUnit()) + .append("]") + .append("[") + .append("当前数据") + .append(getAlarmPolicyInfo().getConditions().getCurrentValue()) + .append(getAlarmPolicyInfo().getConditions().getCalcUnit()) + .append("]"); + } + return contentBuilder.toString(); + } + +} + + diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/enums/CloudServiceAlarmInformationEnum.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/enums/CloudServiceAlarmInformationEnum.java new file mode 100644 index 0000000..ebaeb0a --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/enums/CloudServiceAlarmInformationEnum.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.enums; + +import java.util.Arrays; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.apache.hertzbeat.alert.dto.CloudAlertReportAbstract; +import org.apache.hertzbeat.alert.dto.TenCloudAlertReport; + +/** + * Cloud server alarm enum + */ +@AllArgsConstructor +@Getter +public enum CloudServiceAlarmInformationEnum { + + TencentCloud("tencloud", TenCloudAlertReport.class); + + /** + * cloud service name + */ + private final String cloudServiceName; + + /** + * cloud service body + */ + private final Class cloudServiceAlarmInformationEntity; + + public static CloudServiceAlarmInformationEnum getEnumFromCloudServiceName(String name) { + return Arrays.stream(CloudServiceAlarmInformationEnum.values()) + .filter(cloudService -> cloudService.cloudServiceName.equals(name)) + .findFirst() + .orElse(null); + } + +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmCommonReduce.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmCommonReduce.java new file mode 100644 index 0000000..a2f7b8d --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmCommonReduce.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.reduce; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.dao.AlertMonitorDao; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.Alert; +import org.apache.hertzbeat.common.entity.manager.Tag; +import org.apache.hertzbeat.common.queue.CommonDataQueue; +import org.springframework.stereotype.Service; + +/** + * reduce alarm and send alert data + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class AlarmCommonReduce { + + private final AlarmSilenceReduce alarmSilenceReduce; + + private final AlarmConvergeReduce alarmConvergeReduce; + + private final CommonDataQueue dataQueue; + + private final AlertMonitorDao alertMonitorDao; + + public void reduceAndSendAlarm(Alert alert) { + alert.setTimes(1); + Map tags = alert.getTags(); + if (tags == null) { + tags = new HashMap<>(8); + alert.setTags(tags); + } + String monitorIdStr = tags.get(CommonConstants.TAG_MONITOR_ID); + if (monitorIdStr == null) { + log.debug("receiver extern alarm message: {}", alert); + } else { + long monitorId = Long.parseLong(monitorIdStr); + List tagList = alertMonitorDao.findMonitorIdBindTags(monitorId); + for (Tag tag : tagList) { + if (!tags.containsKey(tag.getName())) { + tags.put(tag.getName(), tag.getTagValue()); + } + } + } + // converge -> silence + if (alarmConvergeReduce.filterConverge(alert) && alarmSilenceReduce.filterSilence(alert)) { + dataQueue.sendAlertsData(alert); + } + } + +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmConvergeReduce.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmConvergeReduce.java new file mode 100644 index 0000000..b2c1350 --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmConvergeReduce.java @@ -0,0 +1,160 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.reduce; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.hertzbeat.alert.dao.AlertConvergeDao; +import org.apache.hertzbeat.common.cache.CacheFactory; +import org.apache.hertzbeat.common.cache.CommonCacheService; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.Alert; +import org.apache.hertzbeat.common.entity.alerter.AlertConverge; +import org.apache.hertzbeat.common.entity.manager.TagItem; +import org.springframework.stereotype.Service; + +/** + * alarm converge + */ +@Service +public class AlarmConvergeReduce { + + private final AlertConvergeDao alertConvergeDao; + + private final Map converageAlertMap; + + public AlarmConvergeReduce(AlertConvergeDao alertConvergeDao) { + this.alertConvergeDao = alertConvergeDao; + this.converageAlertMap = new ConcurrentHashMap<>(16); + } + + /** + * currentAlert converge filter data + * + * @param currentAlert currentAlert + * @return true when not filter + */ + @SuppressWarnings("unchecked") + public boolean filterConverge(Alert currentAlert) { + // ignore monitor status auto recover notice + if (currentAlert.getTags() != null && currentAlert.getTags().containsKey(CommonConstants.IGNORE)) { + return true; + } + if (currentAlert.getStatus() == CommonConstants.ALERT_STATUS_CODE_RESTORED) { + // restored alert + int alertHash = Objects.hash(CommonConstants.ALERT_PRIORITY_CODE_CRITICAL) + + Arrays.hashCode(currentAlert.getTags().keySet().toArray(new String[0])) + + Arrays.hashCode(currentAlert.getTags().values().toArray(new String[0])); + converageAlertMap.remove(alertHash); + alertHash = Objects.hash(CommonConstants.ALERT_PRIORITY_CODE_EMERGENCY) + + Arrays.hashCode(currentAlert.getTags().keySet().toArray(new String[0])) + + Arrays.hashCode(currentAlert.getTags().values().toArray(new String[0])); + converageAlertMap.remove(alertHash); + alertHash = Objects.hash(CommonConstants.ALERT_PRIORITY_CODE_WARNING) + + Arrays.hashCode(currentAlert.getTags().keySet().toArray(new String[0])) + + Arrays.hashCode(currentAlert.getTags().values().toArray(new String[0])); + converageAlertMap.remove(alertHash); + return true; + } + CommonCacheService convergeCache = CacheFactory.getAlertConvergeCache(); + List alertConvergeList = (List) convergeCache.get(CommonConstants.CACHE_ALERT_CONVERGE); + if (alertConvergeList == null) { + alertConvergeList = alertConvergeDao.findAll(); + // matchAll is in the last + alertConvergeList.sort((item1, item2) -> { + if (item1.isMatchAll()) { + return 1; + } else if (item2.isMatchAll()) { + return -1; + } else { + return 0; + } + }); + convergeCache.put(CommonConstants.CACHE_ALERT_CONVERGE, alertConvergeList); + } + for (AlertConverge alertConverge : alertConvergeList) { + if (!alertConverge.isEnable()) { + continue; + } + boolean match = alertConverge.isMatchAll(); + if (!match) { + List tags = alertConverge.getTags(); + if (currentAlert.getTags() != null && !currentAlert.getTags().isEmpty()) { + Map alertTagMap = currentAlert.getTags(); + match = tags.stream().anyMatch(item -> { + if (alertTagMap.containsKey(item.getName())) { + String tagValue = alertTagMap.get(item.getName()); + if (tagValue == null && item.getValue() == null) { + return true; + } else { + return tagValue != null && tagValue.equals(item.getValue()); + } + } else { + return false; + } + }); + } else { + match = true; + } + if (match && alertConverge.getPriorities() != null && !alertConverge.getPriorities().isEmpty()) { + match = alertConverge.getPriorities().stream().anyMatch(item -> item != null && item == currentAlert.getPriority()); + } + } + if (match) { + long evalInterval = alertConverge.getEvalInterval() * 1000; + long now = System.currentTimeMillis(); + if (evalInterval <= 0) { + return true; + } + int alertHash = Objects.hash(currentAlert.getPriority()) + + Arrays.hashCode(currentAlert.getTags().keySet().toArray(new String[0])) + + Arrays.hashCode(currentAlert.getTags().values().toArray(new String[0])); + Alert preAlert = converageAlertMap.get(alertHash); + if (preAlert == null) { + currentAlert.setTimes(1); + currentAlert.setFirstAlarmTime(now); + currentAlert.setLastAlarmTime(now); + converageAlertMap.put(alertHash, currentAlert.clone()); + return true; + } else { + if (now - preAlert.getFirstAlarmTime() < evalInterval) { + preAlert.setTimes(preAlert.getTimes() + 1); + preAlert.setLastAlarmTime(now); + return false; + } else { + currentAlert.setTimes(preAlert.getTimes()); + if (preAlert.getTimes() == 1) { + currentAlert.setFirstAlarmTime(now); + } else { + currentAlert.setFirstAlarmTime(preAlert.getFirstAlarmTime()); + } + currentAlert.setLastAlarmTime(now); + preAlert.setFirstAlarmTime(now); + preAlert.setLastAlarmTime(now); + preAlert.setTimes(1); + return true; + } + } + } + } + return true; + } +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmSilenceReduce.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmSilenceReduce.java new file mode 100644 index 0000000..8a807a0 --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/reduce/AlarmSilenceReduce.java @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.reduce; + +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.apache.hertzbeat.alert.dao.AlertSilenceDao; +import org.apache.hertzbeat.common.cache.CacheFactory; +import org.apache.hertzbeat.common.cache.CommonCacheService; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.Alert; +import org.apache.hertzbeat.common.entity.alerter.AlertSilence; +import org.apache.hertzbeat.common.entity.manager.TagItem; +import org.springframework.stereotype.Service; + +/** + * silence alarm + */ +@Service +@RequiredArgsConstructor +public class AlarmSilenceReduce { + + private final AlertSilenceDao alertSilenceDao; + + /** + * alert silence filter data + * @param alert alert + * @return true when not filter + */ + @SuppressWarnings("unchecked") + public boolean filterSilence(Alert alert) { + CommonCacheService silenceCache = CacheFactory.getAlertSilenceCache(); + List alertSilenceList = (List) silenceCache.get(CommonConstants.CACHE_ALERT_SILENCE); + if (alertSilenceList == null) { + alertSilenceList = alertSilenceDao.findAll(); + silenceCache.put(CommonConstants.CACHE_ALERT_SILENCE, alertSilenceList); + } + for (AlertSilence alertSilence : alertSilenceList) { + if (!alertSilence.isEnable()) { + continue; + } + // if match the silence rule, return + boolean match = alertSilence.isMatchAll(); + if (!match) { + List tags = alertSilence.getTags(); + if (alert.getTags() != null && !alert.getTags().isEmpty()) { + Map alertTagMap = alert.getTags(); + match = tags.stream().anyMatch(item -> { + if (alertTagMap.containsKey(item.getName())) { + String tagValue = alertTagMap.get(item.getName()); + if (tagValue == null && item.getValue() == null) { + return true; + } else { + return tagValue != null && tagValue.equals(item.getValue()); + } + } else { + return false; + } + }); + } else { + match = true; + } + if (match && alertSilence.getPriorities() != null && !alertSilence.getPriorities().isEmpty()) { + match = alertSilence.getPriorities().stream().anyMatch(item -> item != null && item == alert.getPriority()); + } + } + if (match) { + LocalDateTime nowDate = LocalDateTime.now(); + if (alertSilence.getType() == 0) { + // once time + boolean startMatch = alertSilence.getPeriodStart() == null || nowDate.isAfter(alertSilence.getPeriodStart().toLocalDateTime()); + boolean endMatch = alertSilence.getPeriodEnd() == null || nowDate.isBefore(alertSilence.getPeriodEnd().toLocalDateTime()); + if (startMatch && endMatch) { + int times = Optional.ofNullable(alertSilence.getTimes()).orElse(0); + alertSilence.setTimes(times + 1); + alertSilenceDao.save(alertSilence); + return false; + } + } else if (alertSilence.getType() == 1) { + // cyc time + int currentDayOfWeek = nowDate.toLocalDate().getDayOfWeek().getValue(); + if (alertSilence.getDays() != null && !alertSilence.getDays().isEmpty()) { + boolean dayMatch = alertSilence.getDays().stream().anyMatch(item -> item == currentDayOfWeek); + if (dayMatch) { + LocalTime nowTime = nowDate.toLocalTime(); + boolean startMatch = alertSilence.getPeriodStart() == null || nowTime.isAfter(alertSilence.getPeriodStart().toLocalTime()); + boolean endMatch = alertSilence.getPeriodEnd() == null || nowTime.isBefore(alertSilence.getPeriodEnd().toLocalTime()); + if (startMatch && endMatch) { + int times = Optional.ofNullable(alertSilence.getTimes()).orElse(0); + alertSilence.setTimes(times + 1); + alertSilenceDao.save(alertSilence); + return false; + } + } + } + } + } + } + return true; + } +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertConvergeService.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertConvergeService.java new file mode 100644 index 0000000..b41fdc5 --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertConvergeService.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.service; + +import java.util.Set; +import org.apache.hertzbeat.common.entity.alerter.AlertConverge; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.domain.Specification; + +/** + * management interface service for alert converge + */ +public interface AlertConvergeService { + /** + * Verify the correctness of the request data parameters + * @param alertConverge AlertConverge + * @param isModify whether modify + * @throws IllegalArgumentException A checksum parameter error is thrown + */ + void validate(AlertConverge alertConverge, boolean isModify) throws IllegalArgumentException; + + /** + * New AlertConverge + * @param alertConverge AlertConverge Entity + * @throws RuntimeException Added procedure exception throwing + */ + void addAlertConverge(AlertConverge alertConverge) throws RuntimeException; + + /** + * Modifying an AlertConverge + * @param alertConverge Alarm definition Entity + * @throws RuntimeException Exception thrown during modification + */ + void modifyAlertConverge(AlertConverge alertConverge) throws RuntimeException; + + /** + * Obtain AlertConverge information + * @param convergeId AlertConverge ID + * @return AlertConverge + * @throws RuntimeException An exception was thrown during the query + */ + AlertConverge getAlertConverge(long convergeId) throws RuntimeException; + + + /** + * Delete AlertConverge in batches + * @param convergeIds AlertConverge IDs + * @throws RuntimeException Exception thrown during deletion + */ + void deleteAlertConverges(Set convergeIds) throws RuntimeException; + + /** + * Dynamic conditional query + * @param specification Query conditions + * @param pageRequest Paging parameters + * @return The query results + */ + Page getAlertConverges(Specification specification, PageRequest pageRequest); +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertDefineImExportService.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertDefineImExportService.java new file mode 100644 index 0000000..dfcbb19 --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertDefineImExportService.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.service; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; + +/** + * Configuration Import Export + */ +public interface AlertDefineImExportService { + /** + * Import Configuration + * @param is input stream + */ + void importConfig(InputStream is); + + /** + * Export Configuration + * @param os output stream + * @param configList configuration list + */ + void exportConfig(OutputStream os, List configList); + + /** + * Export file type + * @return file type + */ + String type(); + + /** + * Get Export File Name + * @return file name + */ + String getFileName(); +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertDefineService.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertDefineService.java new file mode 100644 index 0000000..7431916 --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertDefineService.java @@ -0,0 +1,144 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.service; + +import jakarta.servlet.http.HttpServletResponse; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.apache.hertzbeat.common.entity.alerter.AlertDefineMonitorBind; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.web.multipart.MultipartFile; + +/** + * Alarm define manager service + */ +public interface AlertDefineService { + + /** + * Verify the correctness of the request data parameters + * @param alertDefine alertDefine + * @param isModify whether modify + * @throws IllegalArgumentException A checksum parameter error is thrown + */ + void validate(AlertDefine alertDefine, boolean isModify) throws IllegalArgumentException; + + /** + * New Alarm Definition + * @param alertDefine Alarm definition Entity + * @throws RuntimeException Added procedure exception throwing + */ + void addAlertDefine(AlertDefine alertDefine) throws RuntimeException; + + /** + * Modifying an Alarm Definition + * @param alertDefine Alarm definition Entity + * @throws RuntimeException Exception thrown during modification + */ + void modifyAlertDefine(AlertDefine alertDefine) throws RuntimeException; + + /** + * Deleting an Alarm Definition + * @param alertId Alarm Definition ID + * @throws RuntimeException Exception thrown during deletion + */ + void deleteAlertDefine(long alertId) throws RuntimeException; + + /** + * Obtain alarm definition information + * @param alertId Monitor the ID + * @return AlertDefine + * @throws RuntimeException An exception was thrown during the query + */ + AlertDefine getAlertDefine(long alertId) throws RuntimeException; + + + /** + * Delete alarm definitions in batches + * @param alertIds Alarm Definition IDs + * @throws RuntimeException Exception thrown during deletion + */ + void deleteAlertDefines(Set alertIds) throws RuntimeException; + + /** + * Dynamic conditional query + * @param specification Query conditions + * @param pageRequest Paging parameters + * @return The query results + */ + Page getMonitorBindAlertDefines(Specification specification, PageRequest pageRequest); + + /** + * Association between application alarm schedule and monitoring + * @param alertId Alarm Definition ID + * @param alertDefineBinds correlation + */ + void applyBindAlertDefineMonitors(Long alertId, List alertDefineBinds); + + /** + * Query the alarm definitions that match the specified metrics associated with the monitoring ID + * @param monitorId Monitor the ID + * @param app Monitoring type + * @param metrics metrics + * @return field - define[] + */ + Map> getMonitorBindAlertDefines(long monitorId, String app, String metrics); + + /** + * Query the alarm definitions that match the specified metrics associated with the monitoring ID + * @param monitorId Monitor the ID + * @param app Monitoring type + * @param metrics metrics + * @return field - define[] + */ + AlertDefine getMonitorBindAlertAvaDefine(long monitorId, String app, String metrics); + + /** + * Dynamic conditional query + * @param specification Query conditions + * @param pageRequest Paging parameters + * @return The query results + */ + Page getAlertDefines(Specification specification, PageRequest pageRequest); + + /** + * Query the associated monitoring list information based on the alarm definition ID + * @param alertDefineId Alarm Definition ID + * @return Associated information about the monitoring list + */ + List getBindAlertDefineMonitors(long alertDefineId); + + /** + * Export file configuration of specified type based on ID list and export file type + * @param ids AlertDefine ID + * @param type File Type + * @param res Response + * @throws Exception An exception was thrown during the export + */ + void export(List ids, String type, HttpServletResponse res) throws Exception; + + /** + * Add alarm threshold rules based on the uploaded alarm threshold file + * @param file Upload File + * @throws Exception An exception was thrown during the importConfig + */ + void importConfig(MultipartFile file) throws Exception; +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertService.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertService.java new file mode 100644 index 0000000..2bc6b18 --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertService.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.service; + +import java.util.HashSet; +import java.util.List; +import org.apache.hertzbeat.alert.dto.AlertSummary; +import org.apache.hertzbeat.common.entity.alerter.Alert; +import org.apache.hertzbeat.common.entity.dto.AlertReport; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.domain.Specification; + +/** + * Alarm information management interface + */ +public interface AlertService { + + /** + * Add alarm record + * @param alert Alert entity + * @throws RuntimeException Add process exception throw + */ + void addAlert(Alert alert) throws RuntimeException; + + /** + * Dynamic conditional query + * @param specification Query conditions + * @param pageRequest pagination parameters + * @return search result + */ + Page getAlerts(Specification specification, PageRequest pageRequest); + + /** + * Delete alarms in batches according to the alarm ID list + * @param ids Alarm ID List + */ + void deleteAlerts(HashSet ids); + + /** + * Clear all alerts + */ + void clearAlerts(); + + /** + * Update the alarm status according to the alarm ID-status value + * @param status Alarm status to be modified + * @param ids Alarm ID List to be modified + */ + void editAlertStatus(Byte status, List ids); + + /** + * Get alarm statistics information + * @return Alarm statistics information + */ + AlertSummary getAlertsSummary(); + + /** + * A third party reports an alarm + * @param alertReport The alarm information + */ + void addNewAlertReport(AlertReport alertReport); + + /** + * Dynamic conditional query + * @param specification Query conditions + * @return search result + */ + List getAlerts(Specification specification); +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertSilenceService.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertSilenceService.java new file mode 100644 index 0000000..3e7e114 --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/AlertSilenceService.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.service; + +import java.util.Set; +import org.apache.hertzbeat.common.entity.alerter.AlertSilence; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.domain.Specification; + +/** + * management interface service for alert silence + */ +public interface AlertSilenceService { + /** + * Verify the correctness of the request data parameters + * @param alertSilence AlertSilence + * @param isModify whether modify + * @throws IllegalArgumentException A checksum parameter error is thrown + */ + void validate(AlertSilence alertSilence, boolean isModify) throws IllegalArgumentException; + + /** + * New AlertSilence + * @param alertSilence AlertSilence Entity + * @throws RuntimeException Added procedure exception throwing + */ + void addAlertSilence(AlertSilence alertSilence) throws RuntimeException; + + /** + * Modifying an AlertSilence + * @param alertSilence Alarm definition Entity + * @throws RuntimeException Exception thrown during modification + */ + void modifyAlertSilence(AlertSilence alertSilence) throws RuntimeException; + + /** + * Obtain AlertSilence information + * @param silenceId AlertSilence ID + * @return AlertSilence + * @throws RuntimeException An exception was thrown during the query + */ + AlertSilence getAlertSilence(long silenceId) throws RuntimeException; + + + /** + * Delete AlertSilence in batches + * @param silenceIds AlertSilence IDs + * @throws RuntimeException Exception thrown during deletion + */ + void deleteAlertSilences(Set silenceIds) throws RuntimeException; + + /** + * Dynamic conditional query + * @param specification Query conditions + * @param pageRequest Paging parameters + * @return The query results + */ + Page getAlertSilences(Specification specification, PageRequest pageRequest); +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertConvergeServiceImpl.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertConvergeServiceImpl.java new file mode 100644 index 0000000..a4d05b4 --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertConvergeServiceImpl.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.service.impl; + +import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.dao.AlertConvergeDao; +import org.apache.hertzbeat.alert.service.AlertConvergeService; +import org.apache.hertzbeat.common.cache.CacheFactory; +import org.apache.hertzbeat.common.cache.CommonCacheService; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.AlertConverge; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * implement for alert converge service + */ +@Service +@Transactional(rollbackFor = Exception.class) +@Slf4j +public class AlertConvergeServiceImpl implements AlertConvergeService { + + @Autowired + private AlertConvergeDao alertConvergeDao; + + @Override + public void validate(AlertConverge alertConverge, boolean isModify) throws IllegalArgumentException { + // todo + } + + @Override + public void addAlertConverge(AlertConverge alertConverge) throws RuntimeException { + alertConvergeDao.save(alertConverge); + clearAlertConvergesCache(); + } + + @Override + public void modifyAlertConverge(AlertConverge alertConverge) throws RuntimeException { + alertConvergeDao.save(alertConverge); + clearAlertConvergesCache(); + } + + @Override + public AlertConverge getAlertConverge(long convergeId) throws RuntimeException { + return alertConvergeDao.findById(convergeId).orElse(null); + } + + @Override + public void deleteAlertConverges(Set convergeIds) throws RuntimeException { + alertConvergeDao.deleteAlertConvergesByIdIn(convergeIds); + clearAlertConvergesCache(); + } + + @Override + public Page getAlertConverges(Specification specification, PageRequest pageRequest) { + return alertConvergeDao.findAll(specification, pageRequest); + } + + private void clearAlertConvergesCache() { + CommonCacheService convergeCache = CacheFactory.getAlertConvergeCache(); + convergeCache.remove(CommonConstants.CACHE_ALERT_CONVERGE); + } +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineAbstractImExportServiceImpl.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineAbstractImExportServiceImpl.java new file mode 100644 index 0000000..2900b0f --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineAbstractImExportServiceImpl.java @@ -0,0 +1,151 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.service.impl; + +import cn.afterturn.easypoi.excel.annotation.Excel; +import cn.afterturn.easypoi.excel.annotation.ExcelTarget; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import jakarta.annotation.Resource; +import java.io.InputStream; +import java.io.OutputStream; +import java.time.LocalDate; +import java.util.List; +import java.util.stream.Collectors; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.service.AlertDefineImExportService; +import org.apache.hertzbeat.alert.service.AlertDefineService; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.apache.hertzbeat.common.entity.manager.TagItem; +import org.springframework.beans.BeanUtils; +import org.springframework.context.annotation.Lazy; +import org.springframework.util.CollectionUtils; + +/** + * Configuration Import Export + */ +@Slf4j +public abstract class AlertDefineAbstractImExportServiceImpl implements AlertDefineImExportService { + @Resource + @Lazy + private AlertDefineService alertDefineService; + + @Override + public void importConfig(InputStream is) { + var formList = parseImport(is) + .stream() + .map(this::convert) + .collect(Collectors.toUnmodifiableList()); + if (!CollectionUtils.isEmpty(formList)) { + formList.forEach(alertDefine -> { + alertDefineService.validate(alertDefine, false); + alertDefineService.addAlertDefine(alertDefine); + }); + } + } + + @Override + public void exportConfig(OutputStream os, List configList) { + var monitorList = configList.stream() + .map(it -> alertDefineService.getAlertDefine(it)) + .map(this::convert) + .collect(Collectors.toUnmodifiableList()); + writeOs(monitorList, os); + } + + + /** + * Parsing an input stream into a form + * + * @param is input stream + * @return form list + */ + abstract List parseImport(InputStream is); + + /** + * Export Configuration to Output Stream + * @param exportAlertDefineList configuration list + * @param os output stream + */ + abstract void writeOs(List exportAlertDefineList, OutputStream os); + + + private ExportAlertDefineDTO convert(AlertDefine alertDefine) { + var exportAlertDefine = new ExportAlertDefineDTO(); + var alertDefineDTO = new AlertDefineDTO(); + BeanUtils.copyProperties(alertDefine, alertDefineDTO); + exportAlertDefine.setAlertDefine(alertDefineDTO); + return exportAlertDefine; + } + + private AlertDefine convert(ExportAlertDefineDTO exportAlertDefineDTO) { + var alertDefine = new AlertDefine(); + var alertDefineDTO = exportAlertDefineDTO.getAlertDefine(); + BeanUtils.copyProperties(alertDefineDTO, alertDefine); + return alertDefine; + } + + protected String fileNamePrefix() { + return "hertzbeat_alertDefine_" + LocalDate.now(); + } + + /** + * Export data transfer objects for alert configurations + */ + @Data + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + @ExcelTarget(value = "ExportAlertDefineDTO") + protected static class ExportAlertDefineDTO { + @Excel(name = "AlertDefine") + private AlertDefineDTO alertDefine; + } + + /** + * Data transfer object for alert configuration + */ + @Data + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + @ExcelTarget(value = "AlertDefineDTO") + protected static class AlertDefineDTO { + @Excel(name = "App") + private String app; + @Excel(name = "Metric") + private String metric; + @Excel(name = "Field") + private String field; + @Excel(name = "Preset") + private Boolean preset; + @Excel(name = "Expr") + private String expr; + @Excel(name = "Priority") + private Byte priority; + @Excel(name = "Times") + private Integer times; + @Excel(name = "Tags") + private List tags; + @Excel(name = "Enable") + private Boolean enable; + @Excel(name = "RecoverNotice") + private Boolean recoverNotice; + @Excel(name = "Template") + private String template; + } +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineExcelImExportServiceImpl.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineExcelImExportServiceImpl.java new file mode 100644 index 0000000..88fa777 --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineExcelImExportServiceImpl.java @@ -0,0 +1,265 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.service.impl; + +import com.fasterxml.jackson.core.type.TypeReference; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.entity.manager.TagItem; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +/** + * Configure the import and export EXCEL format + */ +@Slf4j +@Service +public class AlertDefineExcelImExportServiceImpl extends AlertDefineAbstractImExportServiceImpl { + public static final String TYPE = "EXCEL"; + public static final String FILE_SUFFIX = ".xlsx"; + + /** + * Export file type + * @return file type + */ + @Override + public String type() { + return TYPE; + } + + /** + * Get Export File Name + * @return file name + */ + @Override + public String getFileName() { + return fileNamePrefix() + FILE_SUFFIX; + } + + + /** + * Parsing an input stream into a form + * @param is input stream + * @return form list + */ + @Override + List parseImport(InputStream is) { + try (Workbook workbook = WorkbookFactory.create(is)) { + Sheet sheet = workbook.getSheetAt(0); + List alertDefines = new ArrayList<>(); + for (Row row : sheet) { + if (row.getRowNum() == 0) { + continue; + } + String app = getCellValueAsString(row.getCell(0)); + if (StringUtils.hasText(app)) { + AlertDefineDTO alertDefineDTO = extractAlertDefineDataFromRow(row); + ExportAlertDefineDTO exportAlertDefineDTO = new ExportAlertDefineDTO(); + exportAlertDefineDTO.setAlertDefine(alertDefineDTO); + alertDefines.add(exportAlertDefineDTO); + } + } + return alertDefines; + } catch (IOException e) { + throw new RuntimeException("Failed to parse alertDefine data", e); + } + } + + private TagItem extractTagDataFromRow(Row row) { + String name = getCellValueAsString(row.getCell(7)); + if (StringUtils.hasText(name)) { + TagItem tagItem = new TagItem(); + tagItem.setName(name); + tagItem.setValue(getCellValueAsString(row.getCell(8))); + return tagItem; + } + return null; + } + + private List extractTagDataFromRow(Cell cell) { + String jsonStr = getCellValueAsString(cell); + if (StringUtils.hasText(jsonStr)) { + return JsonUtil.fromJson(jsonStr, new TypeReference<>() { + }); + } + return null; + } + + + private String getCellValueAsString(Cell cell) { + if (cell == null) { + return null; + } + return switch (cell.getCellType()) { + case STRING -> cell.getStringCellValue(); + case NUMERIC -> String.valueOf(cell.getNumericCellValue()); + default -> null; + }; + } + + + private boolean getCellValueAsBoolean(Cell cell) { + if (cell == null) { + return false; + } + if (Objects.requireNonNull(cell.getCellType()) == CellType.BOOLEAN) { + return cell.getBooleanCellValue(); + } + return false; + } + + + private Integer getCellValueAsInteger(Cell cell) { + if (cell == null) { + return null; + } + if (Objects.requireNonNull(cell.getCellType()) == CellType.NUMERIC) { + return (int) cell.getNumericCellValue(); + } + return null; + } + + + private Byte getCellValueAsByte(Cell cell) { + if (cell == null) { + return null; + } + if (Objects.requireNonNull(cell.getCellType()) == CellType.NUMERIC) { + return (byte) cell.getNumericCellValue(); + } + return null; + } + + + private AlertDefineDTO extractAlertDefineDataFromRow(Row row) { + AlertDefineDTO alertDefineDTO = new AlertDefineDTO(); + alertDefineDTO.setApp(getCellValueAsString(row.getCell(0))); + alertDefineDTO.setMetric(getCellValueAsString(row.getCell(1))); + alertDefineDTO.setField(getCellValueAsString(row.getCell(2))); + alertDefineDTO.setPreset(getCellValueAsBoolean(row.getCell(3))); + alertDefineDTO.setExpr(getCellValueAsString(row.getCell(4))); + alertDefineDTO.setPriority(getCellValueAsByte(row.getCell(5))); + alertDefineDTO.setTimes(getCellValueAsInteger(row.getCell(6))); + alertDefineDTO.setTags(extractTagDataFromRow(row.getCell(7))); + alertDefineDTO.setEnable(getCellValueAsBoolean(row.getCell(8))); + alertDefineDTO.setRecoverNotice(getCellValueAsBoolean(row.getCell(9))); + alertDefineDTO.setTemplate(getCellValueAsString(row.getCell(10))); + return alertDefineDTO; + } + + + /** + * Export Configuration to Output Stream + * @param exportAlertDefineList exportAlertDefineList + * @param os output stream + */ + @Override + void writeOs(List exportAlertDefineList, OutputStream os) { + try { + Workbook workbook = WorkbookFactory.create(true); + String sheetName = "Export AlertDefine"; + Sheet sheet = workbook.createSheet(sheetName); + sheet.setDefaultColumnWidth(20); + sheet.setColumnWidth(9, 40 * 256); + sheet.setColumnWidth(10, 40 * 256); + // set header style + CellStyle headerCellStyle = workbook.createCellStyle(); + Font headerFont = workbook.createFont(); + headerFont.setBold(true); + headerCellStyle.setFont(headerFont); + headerCellStyle.setAlignment(HorizontalAlignment.CENTER); + // set cell style + CellStyle cellStyle = workbook.createCellStyle(); + cellStyle.setAlignment(HorizontalAlignment.CENTER); + // set header + String[] headers = {"app", "metric", "field", "preset", "expr", "priority", "times", "tags", + "enable", "recoverNotice", "template"}; + Row headerRow = sheet.createRow(0); + for (int i = 0; i < headers.length; i++) { + Cell cell = headerRow.createCell(i); + cell.setCellValue(headers[i]); + cell.setCellStyle(headerCellStyle); + } + + // Traverse the threshold rule list, each threshold rule object corresponds to a row of data + int rowIndex = 1; + for (ExportAlertDefineDTO alertDefine : exportAlertDefineList) { + AlertDefineDTO alertDefineDTO = alertDefine.getAlertDefine(); + Row row = sheet.createRow(rowIndex++); + // Threshold rule information only needs to be written once + Cell appCell = row.createCell(0); + appCell.setCellValue(alertDefineDTO.getApp()); + appCell.setCellStyle(cellStyle); + Cell metricCell = row.createCell(1); + metricCell.setCellValue(alertDefineDTO.getMetric()); + metricCell.setCellStyle(cellStyle); + Cell fieldCell = row.createCell(2); + fieldCell.setCellValue(alertDefineDTO.getField()); + fieldCell.setCellStyle(cellStyle); + Cell presetCell = row.createCell(3); + presetCell.setCellValue(alertDefineDTO.getPreset() != null + && alertDefineDTO.getPreset()); + presetCell.setCellStyle(cellStyle); + Cell exprCell = row.createCell(4); + exprCell.setCellValue(alertDefineDTO.getExpr()); + exprCell.setCellStyle(cellStyle); + Cell priorityCell = row.createCell(5); + priorityCell.setCellValue(alertDefineDTO.getPriority()); + priorityCell.setCellStyle(cellStyle); + Cell timesCell = row.createCell(6); + timesCell.setCellValue(alertDefineDTO.getTimes()); + Cell tagCell = row.createCell(7); + // get tags + List tagList = alertDefineDTO.getTags(); + String tagValue = tagList == null || tagList.isEmpty() ? "" : JsonUtil.toJson(tagList); + tagCell.setCellValue(tagValue); + tagCell.setCellStyle(cellStyle); + Cell enableCell = row.createCell(8); + enableCell.setCellValue(alertDefineDTO.getEnable() != null + && alertDefineDTO.getEnable()); + enableCell.setCellStyle(cellStyle); + Cell recoverNoticeCell = row.createCell(9); + recoverNoticeCell.setCellValue(alertDefineDTO.getRecoverNotice() != null + && alertDefineDTO.getRecoverNotice()); + recoverNoticeCell.setCellStyle(cellStyle); + Cell templateCell = row.createCell(10); + templateCell.setCellValue(alertDefineDTO.getTemplate()); + recoverNoticeCell.setCellStyle(cellStyle); + } + workbook.write(os); + os.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineJsonImExportServiceImpl.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineJsonImExportServiceImpl.java new file mode 100644 index 0000000..75bf8b0 --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineJsonImExportServiceImpl.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.service.impl; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * Configure the import and export JSON format + */ +@Slf4j +@RequiredArgsConstructor +@Service +public class AlertDefineJsonImExportServiceImpl extends AlertDefineAbstractImExportServiceImpl { + public static final String TYPE = "JSON"; + public static final String FILE_SUFFIX = ".json"; + + private final ObjectMapper objectMapper; + + @Override + public String type() { + return TYPE; + } + + @Override + public String getFileName() { + return fileNamePrefix() + FILE_SUFFIX; + } + + @Override + List parseImport(InputStream is) { + try { + return objectMapper.readValue(is, new TypeReference<>() { + }); + } catch (IOException ex) { + log.error("import alertDefine failed.", ex); + throw new RuntimeException("import alertDefine failed"); + } + } + + @Override + void writeOs(List exportAlertDefineList, OutputStream os) { + try { + objectMapper.writeValue(os, exportAlertDefineList); + } catch (IOException ex) { + log.error("export alertDefine failed.", ex); + throw new RuntimeException("export alertDefine failed"); + } + } +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineServiceImpl.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineServiceImpl.java new file mode 100644 index 0000000..21d07b2 --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineServiceImpl.java @@ -0,0 +1,189 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.service.impl; + +import jakarta.servlet.http.HttpServletResponse; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.dao.AlertDefineBindDao; +import org.apache.hertzbeat.alert.dao.AlertDefineDao; +import org.apache.hertzbeat.alert.service.AlertDefineImExportService; +import org.apache.hertzbeat.alert.service.AlertDefineService; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.apache.hertzbeat.common.entity.alerter.AlertDefineMonitorBind; +import org.apache.hertzbeat.common.util.JexlExpressionRunner; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +/** + * Alarm definition management interface implementation + */ +@Service +@Transactional(rollbackFor = Exception.class) +@Slf4j +public class AlertDefineServiceImpl implements AlertDefineService { + + @Autowired + private AlertDefineDao alertDefineDao; + + @Autowired + private AlertDefineBindDao alertDefineBindDao; + + private final Map alertDefineImExportServiceMap = new HashMap<>(); + + public AlertDefineServiceImpl(List alertDefineImExportServiceList) { + alertDefineImExportServiceList.forEach(it -> alertDefineImExportServiceMap.put(it.type(), it)); + } + + @Override + public void validate(AlertDefine alertDefine, boolean isModify) throws IllegalArgumentException { + // todo + if (StringUtils.hasText(alertDefine.getExpr())) { + try { + JexlExpressionRunner.compile(alertDefine.getExpr()); + } catch (Exception e) { + throw new IllegalArgumentException("alert expr error: " + e.getMessage()); + } + } + } + + @Override + public void addAlertDefine(AlertDefine alertDefine) throws RuntimeException { + alertDefineDao.save(alertDefine); + } + + @Override + public void modifyAlertDefine(AlertDefine alertDefine) throws RuntimeException { + alertDefineDao.save(alertDefine); + } + + @Override + public void deleteAlertDefine(long alertId) throws RuntimeException { + alertDefineDao.deleteById(alertId); + } + + @Override + public AlertDefine getAlertDefine(long alertId) throws RuntimeException { + Optional optional = alertDefineDao.findById(alertId); + return optional.orElse(null); + } + + @Override + public void deleteAlertDefines(Set alertIds) throws RuntimeException { + alertDefineDao.deleteAlertDefinesByIdIn(alertIds); + } + + @Override + public Page getMonitorBindAlertDefines(Specification specification, PageRequest pageRequest) { + return alertDefineDao.findAll(specification, pageRequest); + } + + @Override + public void applyBindAlertDefineMonitors(Long alertId, List alertDefineBinds) { + // todo checks whether the alarm definition and monitoring exist + if (!alertDefineBindDao.existsById(alertId)){ + alertDefineBindDao.deleteAlertDefineBindsByAlertDefineIdEquals(alertId); + } + // Delete all associations of this alarm + alertDefineBindDao.deleteAlertDefineBindsByAlertDefineIdEquals(alertId); + // Save the associated + alertDefineBindDao.saveAll(alertDefineBinds); + } + + @Override + public Map> getMonitorBindAlertDefines(long monitorId, String app, String metrics) { + List defines = alertDefineDao.queryAlertDefinesByMonitor(monitorId, app, metrics); + List defaultDefines = alertDefineDao.queryAlertDefinesByAppAndMetricAndPresetTrueAndEnableTrue(app, metrics); + defines.addAll(defaultDefines); + Set defineSet = defines.stream().filter(item -> item.getField() != null).collect(Collectors.toSet()); + // The alarm thresholds are defined in ascending order of the alarm severity from 0 to 3. + // The lower the number, the higher the alarm is. That is, the alarm is calculated from the highest alarm threshold + return defineSet.stream().sorted(Comparator.comparing(AlertDefine::getPriority)) + .collect(Collectors.groupingBy(AlertDefine::getField)); + } + + @Override + public AlertDefine getMonitorBindAlertAvaDefine(long monitorId, String app, String metrics) { + List defines = alertDefineDao.queryAlertDefinesByMonitor(monitorId, app, metrics); + List defaultDefines = alertDefineDao.queryAlertDefinesByAppAndMetricAndPresetTrueAndEnableTrue(app, metrics); + defines.addAll(defaultDefines); + return defines.stream().findFirst().orElse(null); + } + + @Override + public Page getAlertDefines(Specification specification, PageRequest pageRequest) { + return alertDefineDao.findAll(specification, pageRequest); + } + + @Override + public List getBindAlertDefineMonitors(long alertDefineId) { + return alertDefineBindDao.getAlertDefineBindsByAlertDefineIdEquals(alertDefineId); + } + + @Override + public void export(List ids, String type, HttpServletResponse res) throws Exception { + var imExportService = alertDefineImExportServiceMap.get(type); + if (imExportService == null) { + throw new IllegalArgumentException("not support export type: " + type); + } + var fileName = imExportService.getFileName(); + res.setHeader("content-type", "application/octet-stream;charset=UTF-8"); + res.setContentType("application/octet-stream;charset=UTF-8"); + res.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8)); + res.setHeader("Access-Control-Expose-Headers", "Content-Disposition"); + imExportService.exportConfig(res.getOutputStream(), ids); + } + + @Override + public void importConfig(MultipartFile file) throws Exception { + var fileName = file.getOriginalFilename(); + if (!StringUtils.hasText(fileName)) { + return; + } + var type = ""; + if (fileName.toLowerCase().endsWith(AlertDefineJsonImExportServiceImpl.FILE_SUFFIX)) { + type = AlertDefineJsonImExportServiceImpl.TYPE; + } + if (fileName.toLowerCase().endsWith(AlertDefineExcelImExportServiceImpl.FILE_SUFFIX)) { + type = AlertDefineExcelImExportServiceImpl.TYPE; + } + if (fileName.toLowerCase().endsWith(AlertDefineYamlImExportServiceImpl.FILE_SUFFIX)) { + type = AlertDefineYamlImExportServiceImpl.TYPE; + } + if (!alertDefineImExportServiceMap.containsKey(type)) { + throw new RuntimeException("file " + fileName + " is not supported."); + } + var imExportService = alertDefineImExportServiceMap.get(type); + imExportService.importConfig(file.getInputStream()); + } +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineYamlImExportServiceImpl.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineYamlImExportServiceImpl.java new file mode 100644 index 0000000..2dacffe --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineYamlImExportServiceImpl.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.service.impl; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; + +/** + * Configure the import and export Yaml format + */ +@Slf4j +@Service +public class AlertDefineYamlImExportServiceImpl extends AlertDefineAbstractImExportServiceImpl { + + public static final String TYPE = "YAML"; + public static final String FILE_SUFFIX = ".yaml"; + + @Override + public String type() { + return TYPE; + } + + @Override + public String getFileName() { + return fileNamePrefix() + FILE_SUFFIX; + } + + @Override + List parseImport(InputStream is) { + Yaml yaml = new Yaml(); + return yaml.load(is); + } + + @Override + void writeOs(List exportAlertDefineList, OutputStream os) { + DumperOptions options = new DumperOptions(); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + options.setIndent(2); + options.setPrettyFlow(true); + Yaml yaml = new Yaml(options); + yaml.dump(exportAlertDefineList, new OutputStreamWriter(os, StandardCharsets.UTF_8)); + } +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertServiceImpl.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertServiceImpl.java new file mode 100644 index 0000000..ffdca29 --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertServiceImpl.java @@ -0,0 +1,161 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.service.impl; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.dao.AlertDao; +import org.apache.hertzbeat.alert.dto.AlertPriorityNum; +import org.apache.hertzbeat.alert.dto.AlertSummary; +import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce; +import org.apache.hertzbeat.alert.service.AlertService; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.Alert; +import org.apache.hertzbeat.common.entity.dto.AlertReport; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * Realization of Alarm Information Service + */ +@Service +@Transactional(rollbackFor = Exception.class) +@Slf4j +public class AlertServiceImpl implements AlertService { + + @Autowired + private AlertDao alertDao; + + @Autowired + private AlarmCommonReduce alarmCommonReduce; + + @Override + public void addAlert(Alert alert) throws RuntimeException { + alertDao.save(alert); + } + + @Override + public Page getAlerts(Specification specification, PageRequest pageRequest) { + return alertDao.findAll(specification, pageRequest); + } + + @Override + public void deleteAlerts(HashSet ids) { + alertDao.deleteAlertsByIdIn(ids); + } + + @Override + public void clearAlerts() { + alertDao.deleteAll(); + } + + @Override + public void editAlertStatus(Byte status, List ids) { + alertDao.updateAlertsStatus(status, ids); + } + + @Override + public AlertSummary getAlertsSummary() { + AlertSummary alertSummary = new AlertSummary(); + // Statistics on the alarm information in the alarm state + List priorityNums = alertDao.findAlertPriorityNum(); + if (priorityNums != null) { + for (AlertPriorityNum priorityNum : priorityNums) { + switch (priorityNum.getPriority()) { + case CommonConstants + .ALERT_PRIORITY_CODE_WARNING -> alertSummary.setPriorityWarningNum(priorityNum.getNum()); + case CommonConstants.ALERT_PRIORITY_CODE_CRITICAL -> alertSummary.setPriorityCriticalNum(priorityNum.getNum()); + case CommonConstants.ALERT_PRIORITY_CODE_EMERGENCY -> alertSummary.setPriorityEmergencyNum(priorityNum.getNum()); + default -> {} + } + } + } + long total = alertDao.count(); + alertSummary.setTotal(total); + long dealNum = total - alertSummary.getPriorityCriticalNum() + - alertSummary.getPriorityEmergencyNum() - alertSummary.getPriorityWarningNum(); + alertSummary.setDealNum(dealNum); + try { + if (total == 0) { + alertSummary.setRate(100); + } else { + float rate = BigDecimal.valueOf(100 * (float) dealNum / total) + .setScale(2, RoundingMode.HALF_UP) + .floatValue(); + alertSummary.setRate(rate); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return alertSummary; + } + + @Override + public void addNewAlertReport(AlertReport alertReport) { + alarmCommonReduce.reduceAndSendAlarm(buildAlertData(alertReport)); + } + + @Override + public List getAlerts(Specification specification) { + + return alertDao.findAll(specification); + } + + /** + * The external alarm information is converted to Alert + * @param alertReport alarm body + * @return Alert entity + */ + private Alert buildAlertData(AlertReport alertReport){ + Map annotations = alertReport.getAnnotations(); + StringBuilder sb = new StringBuilder(); + if (alertReport.getContent() == null || alertReport.getContent().length() <= 0){ + StringBuilder finalSb = sb; + annotations.forEach((k, v) -> { + finalSb.append(k).append(":").append(v).append("\n"); + }); + } else { + sb = new StringBuilder(alertReport.getContent()); + } + LocalDateTime dateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(alertReport.getAlertTime()), + ZoneId.systemDefault()); + return Alert.builder() + .content("Alert Center\n" + sb) + .priority(alertReport.getPriority().byteValue()) + .status(CommonConstants.ALERT_STATUS_CODE_PENDING) + .tags(alertReport.getLabels()) + .target(alertReport.getAlertName()) + .triggerTimes(1) + .firstAlarmTime(alertReport.getAlertTime()) + .lastAlarmTime(alertReport.getAlertTime()) + .gmtCreate(dateTime) + .gmtUpdate(dateTime) + .build(); + } +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertSilenceServiceImpl.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertSilenceServiceImpl.java new file mode 100644 index 0000000..a8a347a --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertSilenceServiceImpl.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.service.impl; + +import java.util.Arrays; +import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.alert.dao.AlertSilenceDao; +import org.apache.hertzbeat.alert.service.AlertSilenceService; +import org.apache.hertzbeat.common.cache.CacheFactory; +import org.apache.hertzbeat.common.cache.CommonCacheService; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.AlertSilence; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * management interface service implement for alert silence + */ +@Service +@Transactional(rollbackFor = Exception.class) +@Slf4j +public class AlertSilenceServiceImpl implements AlertSilenceService { + + @Autowired + private AlertSilenceDao alertSilenceDao; + + @Override + public void validate(AlertSilence alertSilence, boolean isModify) throws IllegalArgumentException { + // todo + // if the alarm silent selection date set in periodic situations is empty, it will be deemed to be all checked. + if (alertSilence.getType() == 1 && alertSilence.getDays() == null) { + alertSilence.setDays(Arrays.asList((byte) 7, (byte) 1, (byte) 2, (byte) 3, (byte) 4, (byte) 5, (byte) 6)); + } + } + + @Override + public void addAlertSilence(AlertSilence alertSilence) throws RuntimeException { + alertSilenceDao.save(alertSilence); + clearAlertSilencesCache(); + } + + @Override + public void modifyAlertSilence(AlertSilence alertSilence) throws RuntimeException { + alertSilenceDao.save(alertSilence); + clearAlertSilencesCache(); + } + + @Override + public AlertSilence getAlertSilence(long silenceId) throws RuntimeException { + return alertSilenceDao.findById(silenceId).orElse(null); + } + + @Override + public void deleteAlertSilences(Set silenceIds) throws RuntimeException { + alertSilenceDao.deleteAlertSilencesByIdIn(silenceIds); + clearAlertSilencesCache(); + } + + @Override + public Page getAlertSilences(Specification specification, PageRequest pageRequest) { + return alertSilenceDao.findAll(specification, pageRequest); + } + + private void clearAlertSilencesCache() { + CommonCacheService silenceCache = CacheFactory.getAlertSilenceCache(); + silenceCache.remove(CommonConstants.CACHE_ALERT_SILENCE); + } +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/util/AlertTemplateUtil.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/util/AlertTemplateUtil.java new file mode 100644 index 0000000..8ae100b --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/util/AlertTemplateUtil.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.util; + +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import lombok.extern.slf4j.Slf4j; + +/** + * Alarm template keyword matching replacement engine tool + */ +@Slf4j +public final class AlertTemplateUtil { + + private AlertTemplateUtil() { + } + + /** + * Match the variable ${key} + * eg: Alert, the instance: ${instance} metrics: ${metrics} is over flow. + */ + private static final Pattern PATTERN = Pattern.compile("\\$\\{(\\w+)\\}"); + + public static String render(String template, Map replaceData) { + if (template == null) { + return null; + } + if (replaceData == null) { + log.warn("The replaceData is null."); + return template; + } + try { + Matcher matcher = PATTERN.matcher(template); + StringBuilder builder = new StringBuilder(); + while (matcher.find()) { + Object objectValue = replaceData.getOrDefault(matcher.group(1), "NullValue"); + String value = objectValue.toString(); + matcher.appendReplacement(builder, Matcher.quoteReplacement(value)); + } + matcher.appendTail(builder); + return builder.toString(); + } catch (Exception e) { + log.error(e.getMessage(), e); + return template; + } + } +} diff --git a/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/util/DateUtil.java b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/util/DateUtil.java new file mode 100644 index 0000000..55b0104 --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/java/org/apache/hertzbeat/alert/util/DateUtil.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.util; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.util.Optional; +import lombok.extern.slf4j.Slf4j; + +/** + * date time common util + */ +@Slf4j +public final class DateUtil { + + private DateUtil() { + } + + private static final String[] DATE_FORMATS = { + "yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'", + "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", + "yyyy-MM-dd HH:mm:ss" + }; + + /** + * convert date to timestamp + * @param date date + * @return timestamp + */ + public static Optional getTimeStampFromSomeFormats(String date) { + for (String dateFormat : DATE_FORMATS) { + try { + DateTimeFormatter dateTimeFormatter = new DateTimeFormatterBuilder() + .appendPattern(dateFormat) + // enable string conversion in strict mode. + .parseStrict() + .toFormatter(); + LocalDateTime time = LocalDateTime.parse(date, dateTimeFormatter); + return Optional.of(time.toInstant(ZoneOffset.UTC).toEpochMilli()); + } catch (Exception e) { + log.warn("Error parsing date '{}' with format '{}': {}", + date, dateFormat, e.getMessage()); + } + } + + log.error("Error parsing date '{}', no corresponding date format", date); + return Optional.empty(); + } + + /** + * convert format data to timestamp + */ + public static Optional getTimeStampFromFormat(String date, String format) { + try { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format); + LocalDateTime dateTime = LocalDateTime.parse(date, formatter); + return Optional.of(dateTime.toInstant(java.time.ZoneOffset.UTC).toEpochMilli()); + } catch (Exception e) { + log.error("Error parsing date '{}' with format '{}': {}", + date, format, e.getMessage()); + } + + return Optional.empty(); + } + +} diff --git a/applications/hertzbeat/alerter/src/main/resources/META-INF/spring.factories b/applications/hertzbeat/alerter/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..13843a7 --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/resources/META-INF/spring.factories @@ -0,0 +1,17 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.apache.hertzbeat.alert.config.AlerterAutoConfiguration diff --git a/applications/hertzbeat/alerter/src/main/resources/alerter_en_US.properties b/applications/hertzbeat/alerter/src/main/resources/alerter_en_US.properties new file mode 100644 index 0000000..938da98 --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/resources/alerter_en_US.properties @@ -0,0 +1,32 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +alerter.availability.recover = Availability Alert Resolved, Monitor Status Normal Now +alerter.alarm.recover = Alert Resolved Notice +alerter.notify.title = HertzBeat Alert Notify +alerter.notify.target = Monitor Target +alerter.notify.monitorId = Monitor ID +alerter.notify.monitorName = Monitor Name +alerter.notify.monitorHost = Monitor Host +alerter.notify.priority = Alert Priority +alerter.notify.triggerTime = Alert Trigger Time +alerter.notify.restoreTime = Alert Restore Time +alerter.notify.times = Alert Trigger Times +alerter.notify.tags = Alert Labels +alerter.notify.content = Alert Content +alerter.notify.console = Console Login +alerter.priority.0 = Emergency Alert +alerter.priority.1 = Critical Alert +alerter.priority.2 = Warning Alert diff --git a/applications/hertzbeat/alerter/src/main/resources/alerter_zh_CN.properties b/applications/hertzbeat/alerter/src/main/resources/alerter_zh_CN.properties new file mode 100644 index 0000000..b1f11e2 --- /dev/null +++ b/applications/hertzbeat/alerter/src/main/resources/alerter_zh_CN.properties @@ -0,0 +1,32 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +alerter.availability.recover = 可用性告警恢复通知, 任务状态已恢复正常 +alerter.alarm.recover = 告警恢复通知 +alerter.notify.title = HertzBeat告警通知 +alerter.notify.target = 告警目标对象 +alerter.notify.monitorId = 所属监控任务ID +alerter.notify.monitorName = 所属任务名称 +alerter.notify.monitorHost = 所属任务主机 +alerter.notify.priority = 告警级别 +alerter.notify.triggerTime = 告警触发时间 +alerter.notify.restoreTime = 告警恢复时间 +alerter.notify.times = 告警触发次数 +alerter.notify.tags = 告警标签 +alerter.notify.content = 内容详情 +alerter.notify.console = 登入控制台 +alerter.priority.0 = 紧急告警 +alerter.priority.1 = 严重告警 +alerter.priority.2 = 警告告警 diff --git a/applications/hertzbeat/alerter/src/test/java/org/apache/hertzbeat/alert/AlerterWorkerPoolTest.java b/applications/hertzbeat/alerter/src/test/java/org/apache/hertzbeat/alert/AlerterWorkerPoolTest.java new file mode 100644 index 0000000..4b7a61a --- /dev/null +++ b/applications/hertzbeat/alerter/src/test/java/org/apache/hertzbeat/alert/AlerterWorkerPoolTest.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link AlerterWorkerPool} + */ +class AlerterWorkerPoolTest { + + private static final int NUMBER_OF_THREADS = 10; + private AlerterWorkerPool pool; + private AtomicInteger counter; + private CountDownLatch latch; + + @BeforeEach + void setUp() { + pool = new AlerterWorkerPool(); + counter = new AtomicInteger(); + latch = new CountDownLatch(NUMBER_OF_THREADS); + } + + @Test + void executeJob() throws InterruptedException { + for (int i = 0; i < NUMBER_OF_THREADS; i++) { + pool.executeJob(() -> { + counter.incrementAndGet(); + latch.countDown(); + }); + } + latch.await(); + + assertEquals(NUMBER_OF_THREADS, counter.get()); + } +} diff --git a/applications/hertzbeat/alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertDefineControllerTest.java b/applications/hertzbeat/alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertDefineControllerTest.java new file mode 100644 index 0000000..fa2fe11 --- /dev/null +++ b/applications/hertzbeat/alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertDefineControllerTest.java @@ -0,0 +1,171 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.controller; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.util.Collections; +import java.util.List; +import org.apache.hertzbeat.alert.service.impl.AlertDefineServiceImpl; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.apache.hertzbeat.common.entity.alerter.AlertDefineMonitorBind; +import org.apache.hertzbeat.common.entity.manager.Monitor; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +/** + * Test case for {@link AlertDefineController} + */ +@ExtendWith(MockitoExtension.class) +class AlertDefineControllerTest { + + private MockMvc mockMvc; + + private AlertDefine alertDefine; + + private List alertDefineMonitorBinds; + + @Mock + private AlertDefineServiceImpl alertDefineService; + + @InjectMocks + private AlertDefineController alertDefineController; + + @BeforeEach + void setUp() { + // standaloneSetup: Standalone setup, not integrated with a web environment for testing + this.mockMvc = MockMvcBuilders.standaloneSetup(alertDefineController).build(); + + this.alertDefine = AlertDefine.builder() + .id(1L) + .app("app") + .metric("test") + .field("test") + .preset(false) + .expr("1 > 0") + .priority((byte) 1) + .times(1) + .template("template") + .creator("tom") + .modifier("tom") + .build(); + + this.alertDefineMonitorBinds = Collections.singletonList( + AlertDefineMonitorBind.builder() + .id(1L) + .alertDefineId(this.alertDefine.getId()) + .monitorId(1L) + .monitor( + Monitor.builder() + .id(1L) + .app("app") + .host("localhost") + .name("monitor") + .build() + ) + .build() + ); + } + + @Test + void addNewAlertDefine() throws Exception { + // Simulate the client sending a request to the server + mockMvc.perform(MockMvcRequestBuilders.post("/api/alert/define") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(this.alertDefine))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andReturn(); + } + + @Test + void modifyAlertDefine() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.put("/api/alert/define") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(this.alertDefine))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andReturn(); + } + + @Test + void getAlertDefine() throws Exception { + // Simulate returning data from getAlertDefine + Mockito.when(alertDefineService.getAlertDefine(this.alertDefine.getId())) + .thenReturn(this.alertDefine); + + mockMvc.perform(MockMvcRequestBuilders.get("/api/alert/define/" + this.alertDefine.getId()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.data.id").value(alertDefine.getId())) + .andExpect(jsonPath("$.data.app").value(alertDefine.getApp())) + .andExpect(jsonPath("$.data.metric").value(alertDefine.getMetric())) + .andExpect(jsonPath("$.data.field").value(alertDefine.getField())) + .andExpect(jsonPath("$.data.expr").value(alertDefine.getExpr())) + .andExpect(jsonPath("$.data.template").value(alertDefine.getTemplate())) + .andExpect(jsonPath("$.data.gmtCreate").value(alertDefine.getGmtCreate())) + .andExpect(jsonPath("$.data.gmtUpdate").value(alertDefine.getGmtUpdate())) + .andReturn(); + } + + @Test + void deleteAlertDefine() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.delete("/api/alert/define/" + this.alertDefine.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(this.alertDefine))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andReturn(); + } + + @Test + void applyAlertDefineMonitorsBind() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.post("/api/alert/define/" + this.alertDefine.getId() + "/monitors") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(this.alertDefineMonitorBinds))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andReturn(); + } + + @Test + void getAlertDefineMonitorsBind() throws Exception { + Mockito.when(alertDefineService.getBindAlertDefineMonitors(this.alertDefine.getId())) + .thenReturn(this.alertDefineMonitorBinds); + + mockMvc.perform(MockMvcRequestBuilders.get("/api/alert/define/" + this.alertDefine.getId() + "/monitors") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.data[0].id").value(alertDefineMonitorBinds.get(0).getId())) + .andExpect(jsonPath("$.data[0].monitor.id").value(alertDefineMonitorBinds.get(0).getMonitor().getId())) + .andReturn(); + } +} diff --git a/applications/hertzbeat/alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertDefinesControllerTest.java b/applications/hertzbeat/alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertDefinesControllerTest.java new file mode 100644 index 0000000..62ce3fb --- /dev/null +++ b/applications/hertzbeat/alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertDefinesControllerTest.java @@ -0,0 +1,151 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.controller; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.hertzbeat.alert.service.AlertDefineService; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +/** + * Test case for {@link AlertDefinesController} + * Test whether the data mocked at the mock is correct, and test whether the format of the returned data is correct + */ +@ExtendWith(MockitoExtension.class) +class AlertDefinesControllerTest { + + private MockMvc mockMvc; + + @InjectMocks + private AlertDefinesController alertDefinesController; + + @Mock + AlertDefineService alertDefineService; + + // Parameters to avoid default values interference, default values have been replaced + List ids = Stream.of(6565463543L, 6565463544L).collect(Collectors.toList()); + Byte priority = Byte.parseByte("1"); + String sort = "gmtCreate"; + String order = "asc"; + Integer pageIndex = 1; + Integer pageSize = 7; + + // Parameter collection + Map content = new HashMap<>(); + + // Object for mock + PageRequest pageRequest; + + // Since the specification is used in dynamic proxy, it cannot be mocked + // Missing debugging parameters are ids, priority + // The missing part has been manually output for testing + + @BeforeEach + void setUp() { + this.mockMvc = MockMvcBuilders.standaloneSetup(alertDefinesController).build(); + content.put("ids", ids); + content.put("priority", priority); + content.put("sort", sort); + content.put("order", order); + content.put("pageIndex", pageIndex); + content.put("pageSize", pageSize); + Sort sortExp = Sort.by(new Sort.Order(Sort.Direction.fromString(content.get("order").toString()), content.get("sort").toString())); + pageRequest = PageRequest.of((Integer) content.get("pageIndex"), (Integer) content.get("pageSize"), sortExp); + } + + // @Test + // todo: fix this test + void getAlertDefines() throws Exception { + + // Test the correctness of the mock + // Although objects cannot be mocked, stubs can be stored using class files +// Mockito.when(alertDefineService.getAlertDefines(Mockito.any(Specification.class), Mockito.argThat(new ArgumentMatcher() { +// @Override +// public boolean matches(PageRequest pageRequestMidden) { +// // There are three methods in the source code that need to be compared, namely getPageNumber(), getPageSize(), getSort() +// if(pageRequestMidden.getPageSize() == pageRequest.getPageSize() && +// pageRequestMidden.getPageNumber() == pageRequest.getPageNumber() && +// pageRequestMidden.getSort().equals(pageRequest.getSort())) { +// return true; +// } +// return false; +// } +// }))).thenReturn(new PageImpl(new ArrayList())); + AlertDefine define = AlertDefine.builder().id(9L).app("linux").metric("disk").field("usage").expr("x").times(1).tags(new LinkedList<>()).build(); + Mockito.when(alertDefineService.getAlertDefines(Mockito.any(), Mockito.any())).thenReturn(new PageImpl<>(Collections.singletonList(define))); + + mockMvc.perform(MockMvcRequestBuilders.get( + "/api/alert/defines") + .param("ids", ids.toString().substring(1, ids.toString().length() - 1)) + .param("priority", priority.toString()) + .param("sort", sort) + .param("order", order) + .param("pageIndex", pageIndex.toString()) + .param("pageSize", pageSize.toString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.data.content").value(new ArrayList<>())) + .andExpect(jsonPath("$.data.pageable").value("INSTANCE")) + .andExpect(jsonPath("$.data.totalPages").value(1)) + .andExpect(jsonPath("$.data.totalElements").value(0)) + .andExpect(jsonPath("$.data.last").value(true)) + .andExpect(jsonPath("$.data.number").value(0)) + .andExpect(jsonPath("$.data.size").value(0)) + .andExpect(jsonPath("$.data.first").value(true)) + .andExpect(jsonPath("$.data.numberOfElements").value(0)) + .andExpect(jsonPath("$.data.empty").value(true)) + .andExpect(jsonPath("$.data.sort.empty").value(true)) + .andExpect(jsonPath("$.data.sort.sorted").value(false)) + .andExpect(jsonPath("$.data.sort.unsorted").value(true)) + .andReturn(); + } + + @Test + void deleteAlertDefines() throws Exception { + this.mockMvc.perform(MockMvcRequestBuilders.delete("/api/alert/defines") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(ids))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andReturn(); + } +} diff --git a/applications/hertzbeat/alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertReportControllerTest.java b/applications/hertzbeat/alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertReportControllerTest.java new file mode 100644 index 0000000..cb7f4e0 --- /dev/null +++ b/applications/hertzbeat/alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertReportControllerTest.java @@ -0,0 +1,124 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.controller; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import org.apache.hertzbeat.alert.dto.GeneralCloudAlertReport; +import org.apache.hertzbeat.alert.dto.TenCloudAlertReport; +import org.apache.hertzbeat.alert.service.AlertService; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.util.JsonUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +/** + * unit test for {@link AlertReportController } + */ +@ExtendWith(MockitoExtension.class) +class AlertReportControllerTest { + + private MockMvc mockMvc; + + @Mock + private AlertService alertService; + + @InjectMocks + private AlertReportController alertReportController; + + @BeforeEach + void setUp() { + this.mockMvc = MockMvcBuilders.standaloneSetup(alertReportController).build(); + } + + @Test + void addNewAlertReportTencent() throws Exception { + TenCloudAlertReport.Dimensions dimensions = new TenCloudAlertReport.Dimensions(); + dimensions.setUnInstanceId("3333"); + + TenCloudAlertReport.AlarmObjInfo alarmObjInfo = new TenCloudAlertReport.AlarmObjInfo(); + alarmObjInfo.setRegion("Guangzhou"); + alarmObjInfo.setNamespace("Guangzhou1"); + alarmObjInfo.setAppId("1111"); + alarmObjInfo.setUin("2222"); + alarmObjInfo.setDimensions(dimensions); + + TenCloudAlertReport.Conditions conditions = new TenCloudAlertReport.Conditions(); + conditions.setMetricName("xx"); + conditions.setMetricShowName("xxx"); + conditions.setCalcType("a"); + conditions.setCalcValue("aa"); + conditions.setCalcUnit("aaa"); + conditions.setCurrentValue("b"); + conditions.setCalcUnit("bb"); + conditions.setProductName("guangzhou"); + conditions.setProductShowName("Guangzhou1"); + conditions.setEventName("CVS"); + conditions.setEventShowName("Core error"); + + TenCloudAlertReport.AlarmPolicyInfo alarmPolicyInfo = new TenCloudAlertReport.AlarmPolicyInfo(); + alarmPolicyInfo.setPolicyTypeCname("x"); + alarmPolicyInfo.setPolicyName("Test1"); + alarmPolicyInfo.setConditions(conditions); + + TenCloudAlertReport report = TenCloudAlertReport.builder() + .sessionId("123") + .alarmStatus("1") + .alarmType("event") + .durationTime(2) + .firstOccurTime("2023-08-14 11:11:11") + .alarmObjInfo(alarmObjInfo) + .alarmPolicyInfo(alarmPolicyInfo) + .build(); + mockMvc.perform( + MockMvcRequestBuilders + .post("/api/alerts/report/tencloud") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(report)) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(content().json("{\"data\":null,\"msg\":\"Add report success\",\"code\":0}")) + .andReturn(); + } + + @Test + void addNewAlertReport() throws Exception { + GeneralCloudAlertReport generalCloudAlertReport = new GeneralCloudAlertReport(); + generalCloudAlertReport.setAlertDateTime("2023-02-22T07:27:15.404000000Z"); + + mockMvc.perform(MockMvcRequestBuilders + .post("/api/alerts/report") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.toJson(generalCloudAlertReport)) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(content().json("{\"data\":null,\"msg\":\"Add report success\",\"code\":0}")) + .andReturn(); + } +} diff --git a/applications/hertzbeat/alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertsControllerTest.java b/applications/hertzbeat/alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertsControllerTest.java new file mode 100644 index 0000000..c35ee78 --- /dev/null +++ b/applications/hertzbeat/alerter/src/test/java/org/apache/hertzbeat/alert/controller/AlertsControllerTest.java @@ -0,0 +1,161 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.controller; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.LongStream; +import org.apache.hertzbeat.alert.dto.AlertSummary; +import org.apache.hertzbeat.alert.service.AlertService; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.alerter.Alert; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +/** + * Test case for {@link AlertsController} + */ +@ExtendWith(MockitoExtension.class) +class AlertsControllerTest { + + private MockMvc mockMvc; + + @InjectMocks + private AlertsController alertsController; + + @Mock + private AlertService alertService; + + private List ids; + + + @BeforeEach + void setUp() { + this.mockMvc = MockMvcBuilders.standaloneSetup(alertsController).build(); + ids = LongStream.rangeClosed(1, 10).boxed().collect(Collectors.toList()); + } + + // todo: fix this test + void getAlerts() throws Exception { + String sortField = "id"; + String orderType = "asc"; + int pageIndex = 0; + int pageSize = 10; + PageRequest pageRequest = PageRequest.of(pageIndex, pageSize, Sort.by(new Sort.Order(Sort.Direction.fromString(orderType), sortField))); + Page alertPage = new PageImpl<>(Collections.singletonList(Alert.builder().build())); + Mockito.when( + alertService.getAlerts( + Mockito.any(Specification.class) + , Mockito.argThat( + argument -> + argument.getPageNumber() == pageRequest.getPageNumber() + && argument.getPageSize() == pageRequest.getPageSize() + && argument.getSort().equals(pageRequest.getSort()) + ) + ) + ) + .thenReturn(alertPage); + + mockMvc.perform( + MockMvcRequestBuilders + .get("/api/alerts") + .param("ids", ids.stream().map(String::valueOf).collect(Collectors.joining(","))) + .param("monitorId", "1") + .param("priority", "1") + .param("status", "1") + .param("content", "test") + .param("sort", sortField) + .param("order", orderType) + .param("pageIndex", String.valueOf(pageIndex)) + .param("pageSize", String.valueOf(pageSize)) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(jsonPath("$.data.content.length()").value(1)) + .andReturn(); + } + + @Test + void deleteAlerts() throws Exception { + mockMvc.perform( + MockMvcRequestBuilders + .delete("/api/alerts") + .param("ids", ids.stream().map(String::valueOf).collect(Collectors.joining(","))) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(content().json("{\"data\":null,\"msg\":null,\"code\":0}")) + .andReturn(); + } + + @Test + void clearAllAlerts() throws Exception { + mockMvc.perform( + MockMvcRequestBuilders + .delete("/api/alerts/clear") + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(content().json("{\"data\":null,\"msg\":null,\"code\":0}")) + .andReturn(); + } + + @Test + void applyAlertDefinesStatus() throws Exception { + mockMvc.perform( + MockMvcRequestBuilders + .put("/api/alerts/status/1") + .param("ids", ids.stream().map(String::valueOf).collect(Collectors.joining(","))) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(content().json("{\"data\":null,\"msg\":null,\"code\":0}")) + .andReturn(); + } + + @Test + void getAlertsSummary() throws Exception { + Mockito.when(alertService.getAlertsSummary()).thenReturn(new AlertSummary()); + + mockMvc.perform( + MockMvcRequestBuilders + .get("/api/alerts/summary") + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE)) + .andExpect(content().json("{\"data\":{\"total\":0,\"dealNum\":0,\"rate\":0.0,\"priorityWarningNum\":0,\"priorityCriticalNum\":0,\"priorityEmergencyNum\":0},\"msg\":null,\"code\":0}")) + .andReturn(); + } +} diff --git a/applications/hertzbeat/alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertDefineServiceTest.java b/applications/hertzbeat/alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertDefineServiceTest.java new file mode 100644 index 0000000..b3b317d --- /dev/null +++ b/applications/hertzbeat/alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertDefineServiceTest.java @@ -0,0 +1,190 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.service; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.anySet; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.when; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import org.apache.hertzbeat.alert.dao.AlertDefineBindDao; +import org.apache.hertzbeat.alert.dao.AlertDefineDao; +import org.apache.hertzbeat.alert.service.impl.AlertDefineServiceImpl; +import org.apache.hertzbeat.common.entity.alerter.AlertDefine; +import org.apache.hertzbeat.common.entity.alerter.AlertDefineMonitorBind; +import org.apache.hertzbeat.common.entity.manager.Monitor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.test.util.ReflectionTestUtils; + +/** + * Test case for {@link AlertDefineService} + */ +@ExtendWith(MockitoExtension.class) +class AlertDefineServiceTest { + + private AlertDefine alertDefine; + + private List alertDefineMonitorBinds; + + @Mock + private AlertDefineDao alertDefineDao; + + @Mock + private AlertDefineBindDao alertDefineBindDao; + + @Mock + private List alertDefineImExportServiceList; + + @InjectMocks + private AlertDefineServiceImpl alertDefineService; + + @BeforeEach + void setUp() { + ReflectionTestUtils.setField(this.alertDefineService, "alertDefineDao", alertDefineDao); + ReflectionTestUtils.setField(this.alertDefineService, "alertDefineBindDao", alertDefineBindDao); + + this.alertDefine = AlertDefine.builder() + .id(1L) + .app("app") + .metric("test") + .field("test") + .preset(false) + .expr("1 > 0") + .priority((byte) 1) + .times(1) + .template("template") + .creator("tom") + .modifier("tom") + .build(); + + this.alertDefineMonitorBinds = Collections.singletonList( + AlertDefineMonitorBind.builder() + .id(1L) + .alertDefineId(this.alertDefine.getId()) + .monitorId(1L) + .monitor( + Monitor.builder() + .id(1L) + .app("app") + .host("localhost") + .name("monitor") + .build() + ) + .build() + ); + } + + @Test + void validate() { + assertDoesNotThrow(() -> alertDefineService.validate(alertDefine, true)); + assertDoesNotThrow(() -> alertDefineService.validate(alertDefine, false)); + } + + @Test + void addAlertDefine() { + assertDoesNotThrow(() -> alertDefineService.addAlertDefine(alertDefine)); + when(alertDefineDao.save(alertDefine)).thenThrow(new RuntimeException()); + assertThrows(RuntimeException.class, () -> alertDefineService.addAlertDefine(alertDefine)); + } + + @Test + void modifyAlertDefine() { + AlertDefine alertDefine = AlertDefine.builder().id(1L).build(); + when(alertDefineDao.save(alertDefine)).thenReturn(alertDefine); + assertDoesNotThrow(() -> alertDefineService.modifyAlertDefine(alertDefine)); + reset(); + when(alertDefineDao.save(alertDefine)).thenThrow(new RuntimeException()); + assertThrows(RuntimeException.class, () -> alertDefineService.modifyAlertDefine(alertDefine)); + } + + @Test + void deleteAlertDefine() { + long id = 1L; + doNothing().doThrow(new RuntimeException()).when(alertDefineDao).deleteById(id); + assertDoesNotThrow(() -> alertDefineService.deleteAlertDefine(id)); + assertThrows(RuntimeException.class, () -> alertDefineService.deleteAlertDefine(id)); + } + + @Test + void getAlertDefine() { + long id = 1L; + AlertDefine alertDefine = AlertDefine.builder().id(id).build(); + when(alertDefineDao.findById(id)).thenReturn(Optional.of(alertDefine)); + assertDoesNotThrow(() -> alertDefineService.getAlertDefine(id)); + } + + @Test + void deleteAlertDefines() { + doNothing().when(alertDefineDao).deleteAlertDefinesByIdIn(anySet()); + assertDoesNotThrow(() -> alertDefineService.deleteAlertDefines(new HashSet<>(1))); + } + + @Test + void getMonitorBindAlertDefines() { + Specification specification = mock(Specification.class); + when(alertDefineDao.findAll(specification, PageRequest.of(1, 1))).thenReturn(Page.empty()); + assertNotNull(alertDefineService.getMonitorBindAlertDefines(specification, PageRequest.of(1, 1))); + } + + @Test + void applyBindAlertDefineMonitors() { + long id = 1L; + doNothing().when(alertDefineBindDao).deleteAlertDefineBindsByAlertDefineIdEquals(id); + when(alertDefineBindDao.saveAll(alertDefineMonitorBinds)).thenReturn(alertDefineMonitorBinds); + assertDoesNotThrow(() -> alertDefineService.applyBindAlertDefineMonitors(id, alertDefineMonitorBinds)); + } + + @Test + void testGetMonitorBindAlertDefines() { + List alertDefineList = new ArrayList<>(); + alertDefineList.add(this.alertDefine); + when(alertDefineDao.queryAlertDefinesByMonitor(1L, "app", "test")).thenReturn(alertDefineList); + when(alertDefineDao.queryAlertDefinesByAppAndMetricAndPresetTrueAndEnableTrue("app", "test")).thenReturn(alertDefineList); + assertNotNull(alertDefineService.getMonitorBindAlertDefines(1L, "app", "test")); + } + + @Test + void getAlertDefines() { + Specification specification = mock(Specification.class); + when(alertDefineDao.findAll(specification, PageRequest.of(1, 1))).thenReturn(Page.empty()); + assertNotNull(alertDefineService.getAlertDefines(specification, PageRequest.of(1, 1))); + } + + @Test + void getBindAlertDefineMonitors() { + long id = 1L; + when(alertDefineBindDao.getAlertDefineBindsByAlertDefineIdEquals(id)).thenReturn(alertDefineMonitorBinds); + assertDoesNotThrow(() -> alertDefineService.getBindAlertDefineMonitors(id)); + } +} diff --git a/applications/hertzbeat/alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertServiceTest.java b/applications/hertzbeat/alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertServiceTest.java new file mode 100644 index 0000000..9a76063 --- /dev/null +++ b/applications/hertzbeat/alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertServiceTest.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link AlertService} + */ +class AlertServiceTest { + + @BeforeEach + void setUp() { + } + + @Test + void addAlert() { + } + + @Test + void getAlerts() { + } + + @Test + void deleteAlerts() { + } + + @Test + void clearAlerts() { + } + + @Test + void editAlertStatus() { + } + + @Test + void getAlertsSummary() { + } + + @Test + void addNewAlertReport() { + } +} diff --git a/applications/hertzbeat/alerter/src/test/java/org/apache/hertzbeat/alert/util/AlertTemplateUtilTest.java b/applications/hertzbeat/alerter/src/test/java/org/apache/hertzbeat/alert/util/AlertTemplateUtilTest.java new file mode 100644 index 0000000..096a76e --- /dev/null +++ b/applications/hertzbeat/alerter/src/test/java/org/apache/hertzbeat/alert/util/AlertTemplateUtilTest.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link AlertTemplateUtil} + */ +class AlertTemplateUtilTest { + + @BeforeEach + void setUp() { + } + + @Test + void render() { + // test null template case + Map param = new HashMap<>(); + String template = null; + assertNull(AlertTemplateUtil.render(null, param)); + // test null map case + template = "${key} for testing"; + assertEquals(AlertTemplateUtil.render(template, null), template); + // test null template and null map case + assertNull(AlertTemplateUtil.render(null, null)); + // test illegal template case + template = ""; + param.put("key", "Just"); + assertEquals(AlertTemplateUtil.render(template, param), template); + template = "key for testing!"; + assertEquals(AlertTemplateUtil.render(template, param), template); + // test empty map case + param.clear(); + template = "${key} for testing"; + assertEquals(AlertTemplateUtil.render(template, param), "NullValue for testing"); + // test illegal template and empty map case + param.clear(); + template = "key for testing"; + assertEquals(AlertTemplateUtil.render(template, param), template); + // test one param + param.put("key", "Just"); + template = "${key} for testing"; + assertEquals(AlertTemplateUtil.render(template, param), "Just for testing"); + // test two param + param.put("key1", "Just"); + param.put("key2", "testing"); + template = "${key1} for ${key2}"; + assertEquals(AlertTemplateUtil.render(template, param), "Just for testing"); + // test all param + param.put("key1", "Just"); + param.put("key2", "for"); + param.put("key3", "testing"); + template = "${key1} ${key2} ${key3}"; + assertEquals(AlertTemplateUtil.render(template, param), "Just for testing"); + } + + @Test + void renderSpecialCharacters() { + Map param = new HashMap<>(); + param.put("valueWithDollar", "$100"); + param.put("valueWithBackslash", "C:\\Users"); + + String template = "The price is ${valueWithDollar} and the path is ${valueWithBackslash}"; + + // Expected to handle the dollar sign and backslash correctly without throwing an exception + String expectedResult = "The price is $100 and the path is C:\\Users"; + assertEquals(expectedResult, AlertTemplateUtil.render(template, param)); + } +} diff --git a/applications/hertzbeat/alerter/src/test/java/org/apache/hertzbeat/alert/util/DateUtilTest.java b/applications/hertzbeat/alerter/src/test/java/org/apache/hertzbeat/alert/util/DateUtilTest.java new file mode 100644 index 0000000..df4cff2 --- /dev/null +++ b/applications/hertzbeat/alerter/src/test/java/org/apache/hertzbeat/alert/util/DateUtilTest.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.alert.util; + +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test case for {@link DateUtil} + */ +class DateUtilTest { + + @Test + void getTimeStampFromSomeFormats() { + String date = "2024-05-13"; + Optional actualTimestamp = DateUtil.getTimeStampFromSomeFormats(date); + assertFalse(actualTimestamp.isPresent()); + + date = "2024-05-13T12:34:56.789Z"; + actualTimestamp = DateUtil.getTimeStampFromSomeFormats(date); + assertTrue(actualTimestamp.isPresent()); + assertEquals(1715603696789L, actualTimestamp.get()); + + date = "2023-02-22T07:27:15.404000000Z"; + actualTimestamp = DateUtil.getTimeStampFromSomeFormats(date); + assertTrue(actualTimestamp.isPresent()); + assertEquals(1677050835404L, actualTimestamp.get()); + } + + @Test + void getTimeStampFromFormat() { + String date = "2024-05-13 10:30:00"; + String format = "yyyy-MM-dd HH:mm:ss"; + Optional actualTimestamp = DateUtil.getTimeStampFromFormat(date, format); + assertTrue(actualTimestamp.isPresent()); + assertEquals(1715596200000L, actualTimestamp.get()); + + date = "2024-05-13"; + format = "yyyy-MM-dd HH:mm:ss.SSS"; + actualTimestamp = DateUtil.getTimeStampFromFormat(date, format); + assertFalse(actualTimestamp.isPresent()); + } +} diff --git a/applications/hertzbeat/collector/pom.xml b/applications/hertzbeat/collector/pom.xml new file mode 100644 index 0000000..4edd3af --- /dev/null +++ b/applications/hertzbeat/collector/pom.xml @@ -0,0 +1,454 @@ + + + + + hertzbeat + org.apache.hertzbeat + 2.0-SNAPSHOT + + 4.0.0 + + hertzbeat-collector + ${project.artifactId} + + 17 + ${java.version} + ${java.version} + 3.2.0 + 3.3.0 + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.boot + spring-boot-autoconfigure + + + + org.apache.hertzbeat + hertzbeat-common + + + + org.apache.hertzbeat + hertzbeat-remoting + + + + org.springframework + spring-jdbc + + + + org.apache.kafka + kafka-clients + + + + org.apache.httpcomponents + httpclient + + + + commons-net + commons-net + 3.8.0 + + + + com.jayway.jsonpath + json-path + + + + com.googlecode.concurrentlinkedhashmap + concurrentlinkedhashmap-lru + 1.4.2 + + + com.google.guava + guava + + + com.google.code.gson + gson + + + + + mysql + mysql-connector-java + provided + + + + com.clickhouse + clickhouse-jdbc + + + + + com.dameng + DmJdbcDriver18 + + + + org.postgresql + postgresql + + + + org.apache.sshd + sshd-core + 2.8.0 + + + net.i2p.crypto + eddsa + 0.3.0 + + + + com.microsoft.sqlserver + mssql-jdbc + + + + com.oracle.database.jdbc + ojdbc8 + provided + + + com.oracle.database.nls + orai18n + provided + + + + io.lettuce + lettuce-core + + + + org.mongodb + mongodb-driver-sync + + + + org.snmp4j + snmp4j + 3.6.7 + + + + + org.apache.rocketmq + rocketmq-tools + 4.9.4 + + + + + dnsjava + dnsjava + 3.5.2 + + + + + com.ecwid.consul + consul-api + 1.4.5 + + + + com.alibaba.nacos + nacos-client + 2.2.1 + + + + com.vesoft + client + 3.6.0 + + + + + apache-hertzbeat-collector-${hzb.version} + + + + + inner + + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin.version} + + + package + + + + + + + + cluster + + + + src/main/resources + true + + *.yml + *.properties + *.xml + banner.txt + META-INF/** + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 17 + 17 + + + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin.version} + + target/classes/ + + + false + + + org.apache.hertzbeat.collector.Collector + false + + true + + lib/ + + + . config + + + + + + org.apache.maven.plugins + maven-assembly-plugin + ${maven-assembly-plugin.version} + + + without-jdk + + package + + + single + + + + ../script/assembly/collector/assembly.xml + + ../dist + + + + + + + + + runtime + + + + src/main/resources + true + + *.yml + *.properties + *.xml + banner.txt + META-INF/** + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 17 + 17 + + + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin.version} + + target/classes/ + + + false + + + org.apache.hertzbeat.collector.Collector + false + + true + + lib/ + + + . config + + + + + + org.apache.maven.plugins + maven-assembly-plugin + ${maven-assembly-plugin.version} + + + without-jdk + + package + + + single + + + + ../script/assembly/collector/assembly.xml + + ../dist + + + + make-macos-arm64 + + package + + + single + + + + ../script/assembly/collector/assembly-macos-arm64.xml + + ../dist + + + + make-macos-amd64 + + package + + + single + + + + ../script/assembly/collector/assembly-macos-amd64.xml + + ../dist + + + + make-linux-arm64 + + package + + + single + + + + ../script/assembly/collector/assembly-linux-arm64.xml + + ../dist + + + + make-linux-amd64 + + package + + + single + + + + ../script/assembly/collector/assembly-linux-amd64.xml + + ../dist + + + + make-windows-64 + + package + + + single + + + + ../script/assembly/collector/assembly-windows-64.xml + + ../dist + + + + + + + + + + diff --git a/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/Collector.java b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/Collector.java new file mode 100644 index 0000000..3e41f22 --- /dev/null +++ b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/Collector.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector; + +import jakarta.annotation.PostConstruct; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; +import org.springframework.context.annotation.ComponentScan; + +/** + * collector startup + */ +@ComponentScan(basePackages = {"org.apache.hertzbeat"}) +@ConfigurationPropertiesScan(basePackages = {"org.apache.hertzbeat"}) +@SpringBootApplication +public class Collector { + public static void main(String[] args) { + SpringApplication.run(Collector.class, args); + } + + @PostConstruct + public void init() { + System.setProperty("jdk.jndi.object.factoriesFilter", "!com.zaxxer.hikari.HikariJNDIFactory"); + } +} diff --git a/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/AbstractCollect.java b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/AbstractCollect.java new file mode 100644 index 0000000..82caa00 --- /dev/null +++ b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/AbstractCollect.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect; + + +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.message.CollectRep; + +/** + * Specific metrics collection implementation abstract class + */ +public abstract class AbstractCollect { + + /** + * Pre-check metrics + * @param metrics metric configuration + * @throws IllegalArgumentException when validation failed + */ + public abstract void preCheck(Metrics metrics) throws IllegalArgumentException; + + + /** + * Real acquisition implementation interface + * @param builder response builder + * @param monitorId monitor id + * @param app monitor type + * @param metrics metric configuration + */ + public abstract void collect(CollectRep.MetricsData.Builder builder, long monitorId, String app, Metrics metrics); + + /** + * the protocol this collect instance support + * @return protocol str + */ + public abstract String supportProtocol(); +} diff --git a/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/AbstractConnection.java b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/AbstractConnection.java new file mode 100644 index 0000000..7b3ce9f --- /dev/null +++ b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/AbstractConnection.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.common.cache; + +import lombok.extern.slf4j.Slf4j; + +/** + * AbstractConnection + */ +@Slf4j +public abstract class AbstractConnection implements AutoCloseable { + + /** + * @return Returns the connection. + */ + public abstract T getConnection(); + + /** + * Close connection + */ + public abstract void closeConnection() throws Exception; + + @Override + public void close() throws Exception{ + closeConnection(); + } +} diff --git a/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/CacheIdentifier.java b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/CacheIdentifier.java new file mode 100644 index 0000000..603ae17 --- /dev/null +++ b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/CacheIdentifier.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.common.cache; + +import java.util.Objects; +import lombok.Builder; +import lombok.Data; + +/** + * resource identifier in cache + */ +@Data +@Builder +public class CacheIdentifier { + + private String ip; + + private String port; + + private String username; + + private String password; + + private String customArg; + + @Override + public String toString() { + return "CacheIdentifier {" + + "ip='" + ip + '\'' + + ", port='" + port + '\'' + + ", username+password=>hash='" + Objects.hash(username, password) + '\'' + + ", customArg='" + customArg + '\'' + + '}'; + } +} diff --git a/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/ConnectionCommonCache.java b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/ConnectionCommonCache.java new file mode 100644 index 0000000..8bd6321 --- /dev/null +++ b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/ConnectionCommonCache.java @@ -0,0 +1,230 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.common.cache; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; + +/** + * lru common resource cache for client-server connection + */ +@Slf4j +public class ConnectionCommonCache> { + + /** + * default cache time 200s + */ + private static final long DEFAULT_CACHE_TIMEOUT = 200 * 1000L; + + /** + * default max cache num + */ + private static final int DEFAULT_MAX_CAPACITY = 10000; + + /** + * cacheTime length + */ + private static final int CACHE_TIME_LENGTH = 2; + + /** + * cache timeout map + */ + private Map timeoutMap; + + /** + * object cache + */ + private ConcurrentLinkedHashMap cacheMap; + + /** + * the executor who clean cache when timeout + */ + private ThreadPoolExecutor timeoutCleanerExecutor; + + public ConnectionCommonCache() { + init(); + } + + private void init() { + cacheMap = new ConcurrentLinkedHashMap + .Builder() + .maximumWeightedCapacity(DEFAULT_MAX_CAPACITY) + .listener((key, value) -> { + timeoutMap.remove(key); + try { + value.close(); + } catch (Exception e) { + log.error("connection close error: {}.", e.getMessage(), e); + } + log.info("connection common cache discard key: {}, value: {}.", key, value); + }).build(); + timeoutMap = new ConcurrentHashMap<>(DEFAULT_MAX_CAPACITY >> 6); + // last-first-coverage algorithm, run the first and last thread, discard mid + timeoutCleanerExecutor = new ThreadPoolExecutor(1, 1, 1, TimeUnit.SECONDS, + new ArrayBlockingQueue<>(1), + r -> new Thread(r, "connection-cache-timeout-cleaner"), + new ThreadPoolExecutor.DiscardOldestPolicy()); + // init monitor available detector cyc task + ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setNameFormat("connection-cache-ava-detector-%d") + .setDaemon(true) + .build(); + ScheduledThreadPoolExecutor scheduledExecutor = new ScheduledThreadPoolExecutor(1, threadFactory); + scheduledExecutor.scheduleWithFixedDelay(this::detectCacheAvailable, 2, 20, TimeUnit.MINUTES); + } + + /** + * detect all cache available, cleanup not ava connection + */ + private void detectCacheAvailable() { + try { + cacheMap.forEach((key, value) -> { + Long[] cacheTime = timeoutMap.get(key); + long currentTime = System.currentTimeMillis(); + if (cacheTime == null || cacheTime.length != CACHE_TIME_LENGTH + || cacheTime[0] + cacheTime[1] < currentTime) { + cacheMap.remove(key); + timeoutMap.remove(key); + try { + value.close(); + } catch (Exception e) { + log.error("connection close error: {}.", e.getMessage(), e); + } + + } + }); + } catch (Exception e) { + log.error("connection common cache detect cache available error: {}.", e.getMessage(), e); + } + } + + /** + * clean timeout cache + */ + private void cleanTimeoutCache() { + try { + cacheMap.forEach((key, value) -> { + // index 0 is startTime, 1 is timeDiff + Long[] cacheTime = timeoutMap.get(key); + long currentTime = System.currentTimeMillis(); + if (cacheTime == null || cacheTime.length != CACHE_TIME_LENGTH) { + timeoutMap.put(key, new Long[]{currentTime, DEFAULT_CACHE_TIMEOUT}); + } else if (cacheTime[0] + cacheTime[1] < currentTime) { + // timeout, remove this object cache + log.warn("[connection common cache] clean the timeout cache, key {}", key); + timeoutMap.remove(key); + cacheMap.remove(key); + try { + value.close(); + } catch (Exception e) { + log.error("connection close error: {}.", e.getMessage(), e); + } + } + }); + Thread.sleep(20 * 1000); + } catch (Exception e) { + log.error("[connection common cache] clean timeout cache error: {}.", e.getMessage(), e); + } + } + + /** + * add update cache + * + * @param key cache key + * @param value cache value + * @param timeDiff cache time millis + */ + public void addCache(T key, C value, Long timeDiff) { + removeCache(key); + if (timeDiff == null) { + timeDiff = DEFAULT_CACHE_TIMEOUT; + } + cacheMap.put(key, value); + timeoutMap.put(key, new Long[]{System.currentTimeMillis(), timeDiff}); + timeoutCleanerExecutor.execute(this::cleanTimeoutCache); + } + + /** + * add update cache + * + * @param key cache key + * @param value cache value + */ + public void addCache(T key, C value) { + addCache(key, value, DEFAULT_CACHE_TIMEOUT); + } + + /** + * get cache by key + * + * @param key cache key + * @param refreshCache is refresh cache + * @return cache object + */ + public Optional getCache(T key, boolean refreshCache) { + Long[] cacheTime = timeoutMap.get(key); + if (cacheTime == null || cacheTime.length != CACHE_TIME_LENGTH) { + log.info("[connection common cache] not hit the cache, key {}.", key); + return Optional.empty(); + } + if (cacheTime[0] + cacheTime[1] < System.currentTimeMillis()) { + log.warn("[connection common cache] is timeout, remove it, key {}.", key); + timeoutMap.remove(key); + cacheMap.remove(key); + return Optional.empty(); + } + C value = cacheMap.get(key); + if (value == null) { + log.error("[connection common cache] value is null, remove it, key {}.", key); + cacheMap.remove(key); + timeoutMap.remove(key); + } else if (refreshCache) { + cacheTime[0] = System.currentTimeMillis(); + timeoutMap.put(key, cacheTime); + } + return Optional.ofNullable(value); + } + + /** + * remove cache by key + * + * @param key key + */ + public void removeCache(T key) { + timeoutMap.remove(key); + C value = cacheMap.remove(key); + try { + if (value == null) { + return; + } + value.close(); + } catch (Exception e) { + log.error("connection close error: {}.", e.getMessage(), e); + } + } + +} diff --git a/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/JdbcConnect.java b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/JdbcConnect.java new file mode 100644 index 0000000..ad82887 --- /dev/null +++ b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/JdbcConnect.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.common.cache; + +import java.sql.Connection; +import lombok.extern.slf4j.Slf4j; + +/** + * jdbc common connection + */ +@Slf4j +public class JdbcConnect extends AbstractConnection { + + private final Connection connection; + + public JdbcConnect(Connection connection) { + this.connection = connection; + } + + @Override + public void closeConnection() throws Exception { + if (connection != null) { + connection.close(); + } + } + + @Override + public Connection getConnection() { + return connection; + } +} diff --git a/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/JmxConnect.java b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/JmxConnect.java new file mode 100644 index 0000000..f0e0360 --- /dev/null +++ b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/JmxConnect.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.common.cache; + +import javax.management.remote.JMXConnector; +import lombok.extern.slf4j.Slf4j; + +/** + * jmx connect object + **/ +@Slf4j +public class JmxConnect extends AbstractConnection { + + private final JMXConnector connection; + + public JmxConnect(JMXConnector connection) { + this.connection = connection; + } + + + @Override + public void closeConnection() throws Exception { + if (connection != null) { + connection.close(); + } + } + + @Override + public JMXConnector getConnection() { + return connection; + } +} diff --git a/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/MongodbConnect.java b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/MongodbConnect.java new file mode 100644 index 0000000..73e1e92 --- /dev/null +++ b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/MongodbConnect.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.common.cache; + +import com.mongodb.client.MongoClient; +import lombok.extern.slf4j.Slf4j; + +/** + * mongodb connect client + */ +@Slf4j +public class MongodbConnect extends AbstractConnection { + private final MongoClient mongoClient; + + public MongodbConnect(MongoClient mongoClient) { + this.mongoClient = mongoClient; + } + + @Override + public void closeConnection() throws Exception { + if (this.mongoClient != null) { + this.mongoClient.close(); + } + } + + @Override + public MongoClient getConnection() { + return mongoClient; + } +} diff --git a/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/RedfishConnect.java b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/RedfishConnect.java new file mode 100644 index 0000000..b0750f7 --- /dev/null +++ b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/RedfishConnect.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.common.cache; + +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.redfish.ConnectSession; + +/** + * redfish connect session + */ +@Slf4j +public class RedfishConnect extends AbstractConnection { + private final ConnectSession reddishConnectSession; + + public RedfishConnect(ConnectSession reddishConnectSession) { + this.reddishConnectSession = reddishConnectSession; + } + + @Override + public void closeConnection() throws Exception { + if (reddishConnectSession != null) { + reddishConnectSession.close(); + } + } + + @Override + public ConnectSession getConnection() { + return reddishConnectSession; + } +} diff --git a/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/RedisConnect.java b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/RedisConnect.java new file mode 100644 index 0000000..e24be18 --- /dev/null +++ b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/RedisConnect.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.common.cache; + +import io.lettuce.core.api.StatefulConnection; +import lombok.extern.slf4j.Slf4j; + +/** + * redis connection + */ +@Slf4j +public class RedisConnect extends AbstractConnection> { + + private final StatefulConnection connection; + + public RedisConnect(StatefulConnection connection) { + this.connection = connection; + } + + @Override + public void closeConnection() throws Exception { + if (connection != null) { + connection.closeAsync(); + } + } + + @Override + public StatefulConnection getConnection() { + return connection; + } +} diff --git a/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/SshConnect.java b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/SshConnect.java new file mode 100644 index 0000000..5e05aad --- /dev/null +++ b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/common/cache/SshConnect.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.common.cache; + +import lombok.extern.slf4j.Slf4j; +import org.apache.sshd.client.session.ClientSession; + +/** + * ssh connection holder + */ +@Slf4j +public class SshConnect extends AbstractConnection { + private final ClientSession clientSession; + + public SshConnect(ClientSession clientSession) { + this.clientSession = clientSession; + } + + @Override + public void closeConnection() throws Exception { + if (clientSession != null) { + clientSession.close(); + } + } + + public ClientSession getConnection() { + return clientSession; + } +} diff --git a/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/common/http/CommonHttpClient.java b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/common/http/CommonHttpClient.java new file mode 100644 index 0000000..5212e71 --- /dev/null +++ b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/common/http/CommonHttpClient.java @@ -0,0 +1,155 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.common.http; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import java.security.cert.CertificateException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import lombok.extern.slf4j.Slf4j; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.socket.PlainConnectionSocketFactory; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.ssl.SSLContexts; + +/** + * common http client + */ +@Slf4j +public class CommonHttpClient { + + private static CloseableHttpClient httpClient; + + private static PoolingHttpClientConnectionManager connectionManager; + + /** + * all max total connection + */ + private static final int MAX_TOTAL_CONNECTIONS = 50000; + + /** + * peer route max total connection + */ + private static final int MAX_PER_ROUTE_CONNECTIONS = 80; + + /** + * timeout for get connect from pool(ms) + */ + private static final int REQUIRE_CONNECT_TIMEOUT = 4000; + + /** + * tcp connect timeout(ms) + */ + private static final int CONNECT_TIMEOUT = 4000; + + /** + * socket read timeout(ms) + */ + private static final int SOCKET_TIMEOUT = 60000; + + /** + * validated time for idle connection. if when reuse this connection after this time, we will check it available. + */ + private static final int INACTIVITY_VALIDATED_TIME = 10000; + + /** + * ssl supported version + */ + private static final String[] SUPPORTED_SSL = {"TLSv1", "TLSv1.1", "TLSv1.2", "SSLv3"}; + + static { + try { + SSLContext sslContext = SSLContexts.createDefault(); + X509TrustManager x509TrustManager = new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] x509Certificates, String s) { } + + @Override + public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { + // check server ssl certificate expired + Date now = new Date(); + if (x509Certificates != null) { + for (X509Certificate certificate : x509Certificates) { + Date deadline = certificate.getNotAfter(); + if (deadline != null && now.after(deadline)) { + throw new CertificateExpiredException(); + } + } + } + } + + @Override + public X509Certificate[] getAcceptedIssuers() { return null; } + }; + sslContext.init(null, new TrustManager[]{x509TrustManager}, null); + SSLConnectionSocketFactory sslFactory = new SSLConnectionSocketFactory(sslContext, SUPPORTED_SSL, null, new NoopHostnameVerifier()); + Registry registry = RegistryBuilder.create() + .register("http", PlainConnectionSocketFactory.INSTANCE) + .register("https", sslFactory) + .build(); + RequestConfig requestConfig = RequestConfig.custom() + .setConnectionRequestTimeout(REQUIRE_CONNECT_TIMEOUT) + .setConnectTimeout(CONNECT_TIMEOUT) + .setSocketTimeout(SOCKET_TIMEOUT) + // auto redirect when 301 302 response status + .setRedirectsEnabled(true) + .build(); + // connection pool + connectionManager = new PoolingHttpClientConnectionManager(registry); + connectionManager.setMaxTotal(MAX_TOTAL_CONNECTIONS); + connectionManager.setDefaultMaxPerRoute(MAX_PER_ROUTE_CONNECTIONS); + connectionManager.setValidateAfterInactivity(INACTIVITY_VALIDATED_TIME); + httpClient = HttpClients.custom() + .setConnectionManager(connectionManager) + .setDefaultRequestConfig(requestConfig) + // clean up unavailable expired connections + .evictExpiredConnections() + // clean up available but idle connections + .evictIdleConnections(100, TimeUnit.SECONDS) + .build(); + ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setNameFormat("http-connection-pool-cleaner-%d") + .setDaemon(true) + .build(); + ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(1, threadFactory); + scheduledExecutor.scheduleWithFixedDelay(() -> { + connectionManager.closeExpiredConnections(); + connectionManager.closeIdleConnections(100, TimeUnit.SECONDS); + }, 40L, 40L, TimeUnit.SECONDS); + } catch (Exception ignored) {} + } + + public static CloseableHttpClient getHttpClient() { + return httpClient; + } +} diff --git a/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/common/ssh/CommonSshClient.java b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/common/ssh/CommonSshClient.java new file mode 100644 index 0000000..639ca24 --- /dev/null +++ b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/common/ssh/CommonSshClient.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.common.ssh; + +import lombok.extern.slf4j.Slf4j; +import org.apache.sshd.client.ClientBuilder; +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.keyverifier.AcceptAllServerKeyVerifier; +import org.apache.sshd.common.NamedFactory; +import org.apache.sshd.common.PropertyResolverUtils; +import org.apache.sshd.common.kex.BuiltinDHFactories; +import org.apache.sshd.core.CoreModuleProperties; + +/** + * common ssh pool client + */ +@Slf4j +public class CommonSshClient { + + private static final SshClient SSH_CLIENT; + + static { + SSH_CLIENT = SshClient.setUpDefaultClient(); + // accept all server key verifier, will print warn log : Server at {} presented unverified {} key: {} + AcceptAllServerKeyVerifier verifier = AcceptAllServerKeyVerifier.INSTANCE; + SSH_CLIENT.setServerKeyVerifier(verifier); + // set connection heartbeat interval time 2000ms, wait for heartbeat response timeout 300_000ms + PropertyResolverUtils.updateProperty( + SSH_CLIENT, CoreModuleProperties.HEARTBEAT_INTERVAL.getName(), 2000); + PropertyResolverUtils.updateProperty( + SSH_CLIENT, CoreModuleProperties.HEARTBEAT_REPLY_WAIT.getName(), 300_000); + PropertyResolverUtils.updateProperty( + SSH_CLIENT, CoreModuleProperties.SOCKET_KEEPALIVE.getName(), true); + // set support all KeyExchange + SSH_CLIENT.setKeyExchangeFactories(NamedFactory.setUpTransformedFactories( + false, + BuiltinDHFactories.VALUES, + ClientBuilder.DH2KEX + )); + // todo when connect AlibabaCloud ubuntu server, custom signature factories will cause error, why? + // SSH_CLIENT.setSignatureFactories(new ArrayList<>(BuiltinSignatures.VALUES)); + SSH_CLIENT.start(); + } + + public static SshClient getSshClient() { + return SSH_CLIENT; + } +} diff --git a/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/database/JdbcCommonCollect.java b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/database/JdbcCommonCollect.java new file mode 100644 index 0000000..2294369 --- /dev/null +++ b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/database/JdbcCommonCollect.java @@ -0,0 +1,320 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.database; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.collect.common.cache.CacheIdentifier; +import org.apache.hertzbeat.collector.collect.common.cache.ConnectionCommonCache; +import org.apache.hertzbeat.collector.collect.common.cache.JdbcConnect; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.collector.util.CollectUtil; +import org.apache.hertzbeat.common.constants.CollectorConstants; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.JdbcProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.postgresql.util.PSQLException; +import org.springframework.core.io.FileSystemResource; +import org.springframework.jdbc.datasource.init.ScriptUtils; +import org.springframework.util.StringUtils; + +/** + * common query for database query + */ +@Slf4j +public class JdbcCommonCollect extends AbstractCollect { + + private static final String QUERY_TYPE_ONE_ROW = "oneRow"; + private static final String QUERY_TYPE_MULTI_ROW = "multiRow"; + private static final String QUERY_TYPE_COLUMNS = "columns"; + private static final String RUN_SCRIPT = "runScript"; + + private static final String[] VULNERABLE_KEYWORDS = {"allowLoadLocalInfile", "allowLoadLocalInfileInPath", "useLocalInfile"}; + + private final ConnectionCommonCache connectionCommonCache; + + public JdbcCommonCollect(){ + connectionCommonCache = new ConnectionCommonCache<>(); + } + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + if (metrics == null || metrics.getJdbc() == null) { + throw new IllegalArgumentException("Database collect must has jdbc params"); + } + if (StringUtils.hasText(metrics.getJdbc().getUrl())) { + for (String keyword : VULNERABLE_KEYWORDS) { + if (metrics.getJdbc().getUrl().contains(keyword)) { + throw new IllegalArgumentException("Jdbc url prohibit contains vulnerable param " + keyword); + } + } + } + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, long monitorId, String app, Metrics metrics) { + long startTime = System.currentTimeMillis(); + JdbcProtocol jdbcProtocol = metrics.getJdbc(); + String databaseUrl = constructDatabaseUrl(jdbcProtocol); + int timeout = CollectUtil.getTimeout(jdbcProtocol.getTimeout()); + Statement statement = null; + try { + statement = getConnection(jdbcProtocol.getUsername(), + jdbcProtocol.getPassword(), databaseUrl, timeout); + switch (jdbcProtocol.getQueryType()) { + case QUERY_TYPE_ONE_ROW -> queryOneRow(statement, jdbcProtocol.getSql(), metrics.getAliasFields(), builder, startTime); + case QUERY_TYPE_MULTI_ROW -> queryMultiRow(statement, jdbcProtocol.getSql(), metrics.getAliasFields(), builder, startTime); + case QUERY_TYPE_COLUMNS -> queryOneRowByMatchTwoColumns(statement, jdbcProtocol.getSql(), metrics.getAliasFields(), builder, startTime); + case RUN_SCRIPT -> { + Connection connection = statement.getConnection(); + FileSystemResource rc = new FileSystemResource(jdbcProtocol.getSql()); + ScriptUtils.executeSqlScript(connection, rc); + } + default -> { + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("Not support database query type: " + jdbcProtocol.getQueryType()); + } + } + } catch (PSQLException psqlException) { + // for PostgreSQL 08001 + if (CollectorConstants.POSTGRESQL_UN_REACHABLE_CODE.equals(psqlException.getSQLState())) { + // Peer connection failed, unreachable + builder.setCode(CollectRep.Code.UN_REACHABLE); + } else { + builder.setCode(CollectRep.Code.FAIL); + } + builder.setMsg("Error: " + psqlException.getMessage() + " Code: " + psqlException.getSQLState()); + } catch (SQLException sqlException) { + log.warn("Jdbc sql error: {}, code: {}.", sqlException.getMessage(), sqlException.getErrorCode()); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("Query Error: " + sqlException.getMessage() + " Code: " + sqlException.getErrorCode()); + } catch (Exception e) { + String errorMessage = CommonUtil.getMessageFromThrowable(e); + log.error("Jdbc error: {}.", errorMessage, e); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("Query Error: " + errorMessage); + } finally { + if (statement != null) { + try { + statement.close(); + } catch (Exception e) { + log.error("Jdbc close statement error: {}", e.getMessage()); + } + } + } + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_JDBC; + } + + + private Statement getConnection(String username, String password, String url, Integer timeout) throws Exception { + CacheIdentifier identifier = CacheIdentifier.builder() + .ip(url) + .username(username).password(password).build(); + Optional cacheOption = connectionCommonCache.getCache(identifier, true); + Statement statement = null; + if (cacheOption.isPresent()) { + JdbcConnect jdbcConnect = cacheOption.get(); + try { + statement = jdbcConnect.getConnection().createStatement(); + // set query timeout + int timeoutSecond = timeout / 1000; + timeoutSecond = timeoutSecond <= 0 ? 1 : timeoutSecond; + statement.setQueryTimeout(timeoutSecond); + // set query max row number + statement.setMaxRows(1000); + } catch (Exception e) { + log.info("The jdbc connect from cache, create statement error: {}", e.getMessage()); + try { + if (statement != null) { + statement.close(); + } + jdbcConnect.close(); + } catch (Exception e2) { + log.error(e2.getMessage()); + } + statement = null; + connectionCommonCache.removeCache(identifier); + } + } + if (statement != null) { + return statement; + } + // renew connection when failed + Connection connection = DriverManager.getConnection(url, username, password); + statement = connection.createStatement(); + int timeoutSecond = timeout / 1000; + timeoutSecond = timeoutSecond <= 0 ? 1 : timeoutSecond; + statement.setQueryTimeout(timeoutSecond); + statement.setMaxRows(1000); + JdbcConnect jdbcConnect = new JdbcConnect(connection); + connectionCommonCache.addCache(identifier, jdbcConnect); + return statement; + } + + /** + * query one row record, response metrics header and one value row + * eg: + * query metrics:one tow three four + * query sql:select one, tow, three, four from book limit 1; + * @param statement statement + * @param sql sql + * @param columns query metrics field list + * @throws Exception when error happen + */ + private void queryOneRow(Statement statement, String sql, List columns, + CollectRep.MetricsData.Builder builder, long startTime) throws Exception { + statement.setMaxRows(1); + try (ResultSet resultSet = statement.executeQuery(sql)) { + if (resultSet.next()) { + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String column : columns) { + if (CollectorConstants.RESPONSE_TIME.equals(column)) { + long time = System.currentTimeMillis() - startTime; + valueRowBuilder.addColumns(String.valueOf(time)); + } else { + String value = resultSet.getString(column); + value = value == null ? CommonConstants.NULL_VALUE : value; + valueRowBuilder.addColumns(value); + } + } + builder.addValues(valueRowBuilder.build()); + } + } + } + + /** + * query two columns to mapping one row + * eg: + * query metrics:one two three four + * query sql:select key, value from book; the key is the query metrics fields + * select key, value from book; + * one - value1 + * two - value2 + * three - value3 + * four - value4 + * @param statement statement + * @param sql sql + * @param columns query metrics field list + * @throws Exception when error happen + */ + private void queryOneRowByMatchTwoColumns(Statement statement, String sql, List columns, + CollectRep.MetricsData.Builder builder, long startTime) throws Exception { + try (ResultSet resultSet = statement.executeQuery(sql)) { + HashMap values = new HashMap<>(columns.size()); + while (resultSet.next()) { + if (resultSet.getString(1) != null) { + values.put(resultSet.getString(1).toLowerCase().trim(), resultSet.getString(2)); + } + } + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String column : columns) { + if (CollectorConstants.RESPONSE_TIME.equals(column)) { + long time = System.currentTimeMillis() - startTime; + valueRowBuilder.addColumns(String.valueOf(time)); + } else { + String value = values.get(column.toLowerCase()); + value = value == null ? CommonConstants.NULL_VALUE : value; + valueRowBuilder.addColumns(value); + } + } + builder.addValues(valueRowBuilder.build()); + } + } + + /** + * query multi row record, response metrics header and multi value row + * eg: + * query metrics:one tow three four + * query sql:select one, tow, three, four from book; + * and return multi row record mapping with the metrics + * @param statement statement + * @param sql sql + * @param columns query metrics field list + * @throws Exception when error happen + */ + private void queryMultiRow(Statement statement, String sql, List columns, + CollectRep.MetricsData.Builder builder, long startTime) throws Exception { + try (ResultSet resultSet = statement.executeQuery(sql)) { + while (resultSet.next()) { + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String column : columns) { + if (CollectorConstants.RESPONSE_TIME.equals(column)) { + long time = System.currentTimeMillis() - startTime; + valueRowBuilder.addColumns(String.valueOf(time)); + } else { + String value = resultSet.getString(column); + value = value == null ? CommonConstants.NULL_VALUE : value; + valueRowBuilder.addColumns(value); + } + } + builder.addValues(valueRowBuilder.build()); + } + } + } + + /** + * construct jdbc url due the jdbc protocol + * @param jdbcProtocol jdbc + * @return URL + */ + private String constructDatabaseUrl(JdbcProtocol jdbcProtocol) { + if (Objects.nonNull(jdbcProtocol.getUrl()) + && !Objects.equals("", jdbcProtocol.getUrl()) + && jdbcProtocol.getUrl().startsWith("jdbc")) { + // when has config jdbc url, use it + return jdbcProtocol.getUrl(); + } + return switch (jdbcProtocol.getPlatform()) { + case "mysql", "mariadb" -> + "jdbc:mysql://" + jdbcProtocol.getHost() + ":" + jdbcProtocol.getPort() + + "/" + (jdbcProtocol.getDatabase() == null ? "" : jdbcProtocol.getDatabase()) + + "?useUnicode=true&characterEncoding=utf-8&useSSL=false"; + case "postgresql" -> + "jdbc:postgresql://" + jdbcProtocol.getHost() + ":" + jdbcProtocol.getPort() + + "/" + (jdbcProtocol.getDatabase() == null ? "" : jdbcProtocol.getDatabase()); + case "clickhouse" -> + "jdbc:clickhouse://" + jdbcProtocol.getHost() + ":" + jdbcProtocol.getPort() + + "/" + (jdbcProtocol.getDatabase() == null ? "" : jdbcProtocol.getDatabase()); + case "sqlserver" -> + "jdbc:sqlserver://" + jdbcProtocol.getHost() + ":" + jdbcProtocol.getPort() + + ";" + (jdbcProtocol.getDatabase() == null ? "" : "DatabaseName=" + jdbcProtocol.getDatabase()) + + ";trustServerCertificate=true;"; + case "oracle" -> + "jdbc:oracle:thin:@" + jdbcProtocol.getHost() + ":" + jdbcProtocol.getPort() + + "/" + (jdbcProtocol.getDatabase() == null ? "" : jdbcProtocol.getDatabase()); + case "dm" -> + "jdbc:dm://" + jdbcProtocol.getHost() + ":" + jdbcProtocol.getPort(); + default -> throw new IllegalArgumentException("Not support database platform: " + jdbcProtocol.getPlatform()); + }; + } +} diff --git a/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/database/JdbcSpiLoader.java b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/database/JdbcSpiLoader.java new file mode 100644 index 0000000..a52edb0 --- /dev/null +++ b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/database/JdbcSpiLoader.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.database; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.CommandLineRunner; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Service; + +/** + * load the jdbc driver first to avoid spi concurrent deadlock + */ +@Service +@Slf4j +@Order(value = Ordered.HIGHEST_PRECEDENCE) +public class JdbcSpiLoader implements CommandLineRunner { + + @Override + public void run(String... args) throws Exception { + log.info("start load jdbc drivers"); + try { + Class.forName("org.postgresql.Driver"); + Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver"); + Class.forName("dm.jdbc.driver.DmDriver"); + Class.forName("com.clickhouse.jdbc.ClickHouseDriver"); + } catch (Exception e) { + log.error("load jdbc error: {}", e.getMessage()); + } + log.info("end load jdbc drivers"); + } +} diff --git a/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/dns/DnsCollectImpl.java b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/dns/DnsCollectImpl.java new file mode 100644 index 0000000..0d81a07 --- /dev/null +++ b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/dns/DnsCollectImpl.java @@ -0,0 +1,231 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.dns; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import java.io.IOException; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.DnsProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.springframework.util.StopWatch; +import org.xbill.DNS.DClass; +import org.xbill.DNS.Message; +import org.xbill.DNS.Name; +import org.xbill.DNS.Opcode; +import org.xbill.DNS.RRset; +import org.xbill.DNS.Rcode; +import org.xbill.DNS.Record; +import org.xbill.DNS.Resolver; +import org.xbill.DNS.Section; +import org.xbill.DNS.SimpleResolver; +import org.xbill.DNS.Type; + +/** + * dns protocol collection implementation + */ +@Slf4j +public class DnsCollectImpl extends AbstractCollect { + /* + each part of dig command output + */ + private static final String HEADER = "header"; + private static final String QUESTION = "question"; + private static final String ANSWER = "answer"; + private static final String AUTHORITY = "authority"; + private static final String ADDITIONAL = "additional"; + /* + * used for header key + * example: + * ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 3221 + * ;; flags: qr rd ra ; qd: 1 an: 1 au: 0 ad: 0 + * + * + * opcode -> opcode + * status -> status + * flags -> flags + * qd -> questionRowCount + * an -> answerRowCount + * au -> authorityRowCount + * ad -> additionalRowCount + */ + private static final String RESPONSE_TIME = "responseTime"; + private static final String OP_CODE = "opcode"; + private static final String STATUS = "status"; + private static final String FLAGS = "flags"; + private static final String QUESTION_ROW_COUNT = "questionRowCount"; + private static final String ANSWER_ROW_COUNT = "answerRowCount"; + private static final String AUTHORITY_ROW_COUNT = "authorityRowCount"; + private static final String ADDITIONAL_ROW_COUNT = "additionalRowCount"; + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + // compatible with monitoring template configurations of older versions + if (StringUtils.isBlank(metrics.getDns().getQueryClass())) { + metrics.getDns().setQueryClass(DClass.string(DClass.IN)); + } + // check params + if (checkDnsProtocolFailed(metrics.getDns())) { + throw new IllegalArgumentException("DNS collect must have a valid DNS protocol param! "); + } + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, long monitorId, String app, Metrics metrics) { + + DnsResolveResult dnsResolveResult; + try { + // run dig command + dnsResolveResult = dig(metrics.getDns()); + } catch (IOException e) { + log.info(CommonUtil.getMessageFromThrowable(e)); + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg(e.getMessage()); + return; + } catch (Exception e) { + String errorMsg = CommonUtil.getMessageFromThrowable(e); + log.warn("[dns collect] error: {}", e.getMessage(), e); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + return; + } + + // build dns metrics data + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + if (StringUtils.equals(HEADER, metrics.getName())) { + // add header columns + Map headerInfo = dnsResolveResult.getHeaderInfo(); + metrics.getAliasFields().forEach(field -> valueRowBuilder.addColumns(headerInfo.getOrDefault(field, CommonConstants.NULL_VALUE))); + } else { + // add question/answer/authority/additional columns + List currentMetricsResolveResultList = dnsResolveResult.getList(metrics.getName()); + for (int index = 0; index < metrics.getAliasFields().size(); index++) { + valueRowBuilder.addColumns(index >= currentMetricsResolveResultList.size() + ? CommonConstants.NULL_VALUE + : currentMetricsResolveResultList.get(index)); + } + } + + builder.addValues(valueRowBuilder.build()); + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_DNS; + } + + private boolean checkDnsProtocolFailed(DnsProtocol dnsProtocol) { + return Objects.isNull(dnsProtocol) || dnsProtocol.isInvalid(); + } + + /** + * run dig command + */ + private DnsResolveResult dig(DnsProtocol dns) throws IOException { + StopWatch responseTimeStopWatch = new StopWatch("responseTime"); + responseTimeStopWatch.start(); + + Name name = Name.fromString(dns.getAddress(), Name.root); + Message query = Message.newQuery(Record.newRecord(name, Type.ANY, DClass.ANY)); + Resolver res = new SimpleResolver(dns.getDnsServerIP()); + res.setTimeout(Duration.of(Long.parseLong(dns.getTimeout()), ChronoUnit.MILLIS)); + res.setTCP(Boolean.parseBoolean(dns.getTcp())); + res.setPort(Integer.parseInt(dns.getPort())); + + Message response = res.send(query); + responseTimeStopWatch.stop(); + return resolve(response, responseTimeStopWatch.lastTaskInfo().getTimeMillis()); + } + + private DnsResolveResult resolve(Message message, Long responseTime) { + return DnsResolveResult.builder() + .headerInfo(getHeaderInfo(message, responseTime)) + .questionList(getSectionInfo(message, Section.QUESTION)) + .answerList(getSectionInfo(message, Section.ANSWER)) + .authorityList(getSectionInfo(message, Section.AUTHORITY)) + .additionalList(getSectionInfo(message, Section.ADDITIONAL)) + .build(); + } + + private Map getHeaderInfo(Message message, Long responseTime) { + Map resultMap = Maps.newHashMap(); + resultMap.put(RESPONSE_TIME, String.valueOf(responseTime)); + resultMap.put(OP_CODE, Opcode.string(message.getHeader().getOpcode())); + resultMap.put(STATUS, Rcode.string(message.getHeader().getRcode())); + resultMap.put(FLAGS, message.getHeader().printFlags()); + resultMap.put(QUESTION_ROW_COUNT, String.valueOf(message.getHeader().getCount(Section.QUESTION))); + resultMap.put(ANSWER_ROW_COUNT, String.valueOf(message.getHeader().getCount(Section.ANSWER))); + resultMap.put(AUTHORITY_ROW_COUNT, String.valueOf(message.getHeader().getCount(Section.AUTHORITY))); + resultMap.put(ADDITIONAL_ROW_COUNT, String.valueOf(message.getHeader().getCount(Section.ADDITIONAL))); + + return resultMap; + } + + private List getSectionInfo(Message message, int section) { + List currentSetList = message.getSectionRRsets(section); + + if (CollectionUtils.isEmpty(currentSetList)) { + return Lists.newArrayList(); + } + + List infoList = Lists.newArrayListWithCapacity(currentSetList.size()); + currentSetList.forEach(res -> infoList.add(res.toString())); + + return infoList; + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + private static class DnsResolveResult { + private Map headerInfo; + /** example: www.google.com. 140 IN A 192.133.77.133 **/ + private List questionList; + private List answerList; + private List authorityList; + private List additionalList; + + public List getList(String metricsName) { + return switch (metricsName) { + case QUESTION -> questionList; + case ANSWER -> answerList; + case AUTHORITY -> authorityList; + case ADDITIONAL -> additionalList; + default -> Collections.emptyList(); + }; + } + } +} diff --git a/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/ftp/FtpCollectImpl.java b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/ftp/FtpCollectImpl.java new file mode 100644 index 0000000..e287eea --- /dev/null +++ b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/ftp/FtpCollectImpl.java @@ -0,0 +1,153 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.ftp; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.net.ftp.FTPClient; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.FtpProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * ftp protocol collection implementation + */ +@Slf4j +public class FtpCollectImpl extends AbstractCollect { + + private static final String ANONYMOUS = "anonymous"; + private static final String PASSWORD = "password"; + + /** + * preCheck params + */ + public void preCheck(Metrics metrics) throws IllegalArgumentException{ + if (metrics == null || metrics.getFtp() == null) { + throw new IllegalArgumentException("Ftp collect must has ftp params."); + } + FtpProtocol ftpProtocol = metrics.getFtp(); + Assert.hasText(ftpProtocol.getHost(), "Ftp Protocol host is required."); + Assert.hasText(ftpProtocol.getPort(), "Ftp Protocol port is required."); + Assert.hasText(ftpProtocol.getDirection(), "Ftp Protocol direction is required."); + Assert.hasText(ftpProtocol.getTimeout(), "Ftp Protocol timeout is required."); + } + + + @Override + public void collect(CollectRep.MetricsData.Builder builder, long monitorId, String app, Metrics metrics) { + FTPClient ftpClient = new FTPClient(); + FtpProtocol ftpProtocol = metrics.getFtp(); + // Set timeout + ftpClient.setControlKeepAliveReplyTimeout(Integer.parseInt(ftpProtocol.getTimeout())); + + // Collect data to load in CollectRep.ValueRow.Builder's object + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + Map valueMap; + try { + valueMap = collectValue(ftpClient, ftpProtocol); + metrics.getAliasFields().forEach(it -> { + if (valueMap.containsKey(it)) { + String fieldValue = valueMap.get(it); + valueRowBuilder.addColumns(Objects.requireNonNullElse(fieldValue, CommonConstants.NULL_VALUE)); + } else { + valueRowBuilder.addColumns(CommonConstants.NULL_VALUE); + } + }); + } catch (Exception e) { + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg(e.getMessage()); + return; + } + builder.addValues(valueRowBuilder.build()); + } + + /** + * collect data: key-value + * Please modify this, if you want to add some metrics. + */ + private Map collectValue(FTPClient ftpClient, FtpProtocol ftpProtocol) { + boolean isActive; + String responseTime; + try { + long startTime = System.currentTimeMillis(); + connect(ftpClient, ftpProtocol); + login(ftpClient, ftpProtocol); + // In here, we can do some extended operation without changing the architecture + isActive = ftpClient.changeWorkingDirectory(ftpProtocol.getDirection()); + long endTime = System.currentTimeMillis(); + responseTime = String.valueOf(endTime - startTime); + ftpClient.disconnect(); + } catch (Exception e) { + log.info("[FTPClient] error: {}", CommonUtil.getMessageFromThrowable(e), e); + throw new IllegalArgumentException(e.getMessage()); + } + return new HashMap<>(8) { + { + put("isActive", Boolean.toString(isActive)); + put("responseTime", responseTime); + } + }; + } + + /** + * login + */ + private void login(FTPClient ftpClient, FtpProtocol ftpProtocol) { + try { + // username: not empty, password: not empty + if (StringUtils.hasText(ftpProtocol.getUsername()) && StringUtils.hasText(ftpProtocol.getPassword())) { + if (!ftpClient.login(ftpProtocol.getUsername(), ftpProtocol.getPassword())) { + throw new IllegalArgumentException("The username or password may be wrong."); + } + return; + } + // anonymous access + if (!ftpClient.login(ANONYMOUS, PASSWORD)) { + throw new IllegalArgumentException("The server may not allow anonymous access, we need to username and password."); + } + } catch (Exception e) { + log.info("[ftp login] error: {}", CommonUtil.getMessageFromThrowable(e), e); + throw new IllegalArgumentException(e.getMessage()); + } + } + + /** + * connect + */ + private void connect(FTPClient ftpClient, FtpProtocol ftpProtocol) { + try { + ftpClient.connect(ftpProtocol.getHost(), Integer.parseInt(ftpProtocol.getPort())); + } catch (Exception e) { + log.info("[ftp connection] error: {}", CommonUtil.getMessageFromThrowable(e), e); + throw new IllegalArgumentException("The host or port may be wrong."); + } + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_FTP; + } +} diff --git a/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/http/HttpCollectImpl.java b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/http/HttpCollectImpl.java new file mode 100644 index 0000000..2aa08bf --- /dev/null +++ b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/http/HttpCollectImpl.java @@ -0,0 +1,598 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.http; + +import static org.apache.hertzbeat.common.constants.SignConstants.RIGHT_DASH; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.ConnectException; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.net.ssl.SSLException; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.net.util.Base64; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.collect.common.http.CommonHttpClient; +import org.apache.hertzbeat.collector.collect.http.promethus.AbstractPrometheusParse; +import org.apache.hertzbeat.collector.collect.http.promethus.PrometheusParseCreater; +import org.apache.hertzbeat.collector.collect.http.promethus.exporter.ExporterParser; +import org.apache.hertzbeat.collector.collect.http.promethus.exporter.MetricFamily; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.collector.util.CollectUtil; +import org.apache.hertzbeat.collector.util.JsonPathParser; +import org.apache.hertzbeat.common.constants.CollectorConstants; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.HttpProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.apache.hertzbeat.common.util.IpDomainUtil; +import org.apache.http.HttpHeaders; +import org.apache.http.HttpHost; +import org.apache.http.HttpStatus; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.AuthCache; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.methods.RequestBuilder; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.auth.DigestScheme; +import org.apache.http.impl.client.BasicAuthCache; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.protocol.HttpContext; +import org.apache.http.util.EntityUtils; +import org.springframework.http.HttpMethod; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +/** + * http https collect + */ +@Slf4j +public class HttpCollectImpl extends AbstractCollect { + + public static final String OpenAIHost = "api.openai.com"; + public static final String OpenAIUsageAPI = "/dashboard/billing/usage"; + public static final String startDate = "start_date"; + public static final String endDate = "end_date"; + + private final Set defaultSuccessStatusCodes = Stream.of(HttpStatus.SC_OK, HttpStatus.SC_CREATED, + HttpStatus.SC_ACCEPTED, HttpStatus.SC_MULTIPLE_CHOICES, HttpStatus.SC_MOVED_PERMANENTLY, + HttpStatus.SC_MOVED_TEMPORARILY).collect(Collectors.toSet()); + + public HttpCollectImpl() { + } + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + if (metrics == null || metrics.getHttp() == null) { + throw new IllegalArgumentException("Http/Https collect must has http params"); + } + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, + long monitorId, String app, Metrics metrics) { + long startTime = System.currentTimeMillis(); + + HttpProtocol httpProtocol = metrics.getHttp(); + String url = httpProtocol.getUrl(); + if (!StringUtils.hasText(url) || !url.startsWith(RIGHT_DASH)) { + httpProtocol.setUrl(StringUtils.hasText(url) ? RIGHT_DASH + url.trim() : RIGHT_DASH); + } + if (CollectionUtils.isEmpty(httpProtocol.getSuccessCodes())) { + httpProtocol.setSuccessCodes(List.of("200")); + } + + HttpContext httpContext = createHttpContext(metrics.getHttp()); + HttpUriRequest request = createHttpRequest(metrics.getHttp()); + try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(request, httpContext)) { + int statusCode = response.getStatusLine().getStatusCode(); + boolean isSuccessInvoke = checkSuccessInvoke(metrics, statusCode); + log.debug("http response status: {}", statusCode); + if (!isSuccessInvoke) { + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("StatusCode " + statusCode); + return; + } + // todo This code converts an InputStream directly to a String. For large data in Prometheus exporters, + // this could create large objects, potentially impacting JVM memory space significantly. + // Option 1: Parse using InputStream, but this requires significant code changes; + // Option 2: Manually trigger garbage collection, similar to how it's done in Dubbo for large inputs. + String resp = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + if (StringUtils.hasText(resp)) { + log.info("http response entity is empty, status: {}.", statusCode); + } + Long responseTime = System.currentTimeMillis() - startTime; + String parseType = metrics.getHttp().getParseType(); + try { + switch (parseType) { + case DispatchConstants.PARSE_JSON_PATH -> + parseResponseByJsonPath(resp, metrics.getAliasFields(), metrics.getHttp(), builder, responseTime); + case DispatchConstants.PARSE_PROM_QL -> + parseResponseByPromQl(resp, metrics.getAliasFields(), metrics.getHttp(), builder); + case DispatchConstants.PARSE_PROMETHEUS -> + parseResponseByPrometheusExporter(resp, metrics.getAliasFields(), builder); + case DispatchConstants.PARSE_XML_PATH -> + parseResponseByXmlPath(resp, metrics.getAliasFields(), metrics.getHttp(), builder); + case DispatchConstants.PARSE_WEBSITE -> + parseResponseByWebsite(resp, metrics.getAliasFields(), metrics.getHttp(), builder, responseTime); + case DispatchConstants.PARSE_SITE_MAP -> + parseResponseBySiteMap(resp, metrics.getAliasFields(), builder); + default -> + parseResponseByDefault(resp, metrics.getAliasFields(), metrics.getHttp(), builder, responseTime); + } + } catch (Exception e) { + log.info("parse error: {}.", e.getMessage(), e); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("parse response data error:" + e.getMessage()); + } + } catch (ClientProtocolException e1) { + String errorMsg = CommonUtil.getMessageFromThrowable(e1); + log.error(errorMsg); + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg(errorMsg); + } catch (UnknownHostException e2) { + String errorMsg = CommonUtil.getMessageFromThrowable(e2); + log.info(errorMsg); + builder.setCode(CollectRep.Code.UN_REACHABLE); + builder.setMsg("unknown host:" + errorMsg); + } catch (InterruptedIOException | ConnectException | SSLException e3) { + String errorMsg = CommonUtil.getMessageFromThrowable(e3); + log.info(errorMsg); + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg(errorMsg); + } catch (IOException e4) { + String errorMsg = CommonUtil.getMessageFromThrowable(e4); + log.info(errorMsg); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } catch (Exception e) { + String errorMsg = CommonUtil.getMessageFromThrowable(e); + log.error(errorMsg, e); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } finally { + if (request != null) { + request.abort(); + } + } + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_HTTP; + } + + private void parseResponseByWebsite(String resp, List aliasFields, HttpProtocol http, + CollectRep.MetricsData.Builder builder, Long responseTime) { + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + int keywordNum = CollectUtil.countMatchKeyword(resp, http.getKeyword()); + for (String alias : aliasFields) { + if (CollectorConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumns(responseTime.toString()); + } else if (CollectorConstants.KEYWORD.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumns(Integer.toString(keywordNum)); + } else { + valueRowBuilder.addColumns(CommonConstants.NULL_VALUE); + } + } + builder.addValues(valueRowBuilder.build()); + } + + private void parseResponseBySiteMap(String resp, List aliasFields, + CollectRep.MetricsData.Builder builder) { + List siteUrls = new LinkedList<>(); + boolean isXmlFormat = true; + try { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + DocumentBuilder db = dbf.newDocumentBuilder(); + Document document = db.parse(new ByteArrayInputStream(resp.getBytes(StandardCharsets.UTF_8))); + NodeList urlList = document.getElementsByTagName("url"); + for (int i = 0; i < urlList.getLength(); i++) { + Node urlNode = urlList.item(i); + NodeList childNodes = urlNode.getChildNodes(); + for (int k = 0; k < childNodes.getLength(); k++) { + Node currentNode = childNodes.item(k); + // distinguish between text nodes and element nodes + if (currentNode.getNodeType() == Node.ELEMENT_NODE && "loc".equals(currentNode.getNodeName())) { + // retrieves the value of the loc node + siteUrls.add(currentNode.getFirstChild().getNodeValue()); + break; + } + } + } + } catch (Exception e) { + log.warn(e.getMessage()); + isXmlFormat = false; + } + // if XML parsing fails, parse in TXT format + if (!isXmlFormat) { + try { + String[] urls = resp.split("\n"); + // validate whether the given value is a URL + if (IpDomainUtil.isHasSchema(urls[0])) { + siteUrls.addAll(Arrays.asList(urls)); + } + } catch (Exception e) { + log.warn(e.getMessage(), e); + } + } + // start looping through each site URL to collect its HTTP status code, response time, and exception information + for (String siteUrl : siteUrls) { + String errorMsg = ""; + Integer statusCode = null; + long startTime = System.currentTimeMillis(); + try { + HttpGet httpGet = new HttpGet(siteUrl); + CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(httpGet); + statusCode = response.getStatusLine().getStatusCode(); + EntityUtils.consume(response.getEntity()); + } catch (ClientProtocolException e1) { + if (e1.getCause() != null) { + errorMsg = e1.getCause().getMessage(); + } else { + errorMsg = e1.getMessage(); + } + } catch (UnknownHostException e2) { + errorMsg = "unknown host"; + } catch (InterruptedIOException | ConnectException | SSLException e3) { + errorMsg = "connect error: " + e3.getMessage(); + } catch (IOException e4) { + errorMsg = "io error: " + e4.getMessage(); + } catch (Exception e) { + errorMsg = "error: " + e.getMessage(); + } + long responseTime = System.currentTimeMillis() - startTime; + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String alias : aliasFields) { + if (CollectorConstants.URL.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumns(siteUrl); + } else if (CollectorConstants.STATUS_CODE.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumns(statusCode == null + ? CommonConstants.NULL_VALUE : String.valueOf(statusCode)); + } else if (CollectorConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumns(String.valueOf(responseTime)); + } else if (CollectorConstants.ERROR_MSG.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumns(errorMsg); + } else { + valueRowBuilder.addColumns(CommonConstants.NULL_VALUE); + } + } + builder.addValues(valueRowBuilder.build()); + } + } + + private void parseResponseByXmlPath(String resp, List aliasFields, HttpProtocol http, + CollectRep.MetricsData.Builder builder) { + } + + private void parseResponseByJsonPath(String resp, List aliasFields, HttpProtocol http, + CollectRep.MetricsData.Builder builder, Long responseTime) { + List results = JsonPathParser.parseContentWithJsonPath(resp, http.getParseScript()); + int keywordNum = CollectUtil.countMatchKeyword(resp, http.getKeyword()); + for (int i = 0; i < results.size(); i++) { + Object objectValue = results.get(i); + // if a property is missing or empty due to target version issues, filter it. Refer to the app-elasticsearch.yml configuration under name: nodes + if (objectValue == null) { + continue; + } + if (objectValue instanceof Map) { + Map stringMap = (Map) objectValue; + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String alias : aliasFields) { + Object value = stringMap.get(alias); + if (value != null) { + valueRowBuilder.addColumns(String.valueOf(value)); + } else { + if (alias.startsWith("$.")) { + List subResults = JsonPathParser.parseContentWithJsonPath(resp, http.getParseScript() + alias.substring(1)); + if (subResults != null && subResults.size() > i) { + Object resultValue = subResults.get(i); + valueRowBuilder.addColumns(resultValue == null ? CommonConstants.NULL_VALUE : String.valueOf(resultValue)); + } else { + valueRowBuilder.addColumns(CommonConstants.NULL_VALUE); + } + } else if (CollectorConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumns(responseTime.toString()); + } else if (CollectorConstants.KEYWORD.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumns(Integer.toString(keywordNum)); + } else { + valueRowBuilder.addColumns(CommonConstants.NULL_VALUE); + } + } + } + builder.addValues(valueRowBuilder.build()); + } else if (objectValue instanceof String stringValue) { + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String alias : aliasFields) { + if (CollectorConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumns(responseTime.toString()); + } else if (CollectorConstants.KEYWORD.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumns(Integer.toString(keywordNum)); + } else { + valueRowBuilder.addColumns(stringValue); + } + } + builder.addValues(valueRowBuilder.build()); + } + } + } + + private void parseResponseByPromQl(String resp, List aliasFields, HttpProtocol http, + CollectRep.MetricsData.Builder builder) { + AbstractPrometheusParse prometheusParser = PrometheusParseCreater.getPrometheusParse(); + prometheusParser.handle(resp, aliasFields, http, builder); + } + + private static final Map EXPORTER_PARSER_TABLE = new ConcurrentHashMap<>(); + + private void parseResponseByPrometheusExporter(String resp, List aliasFields, + CollectRep.MetricsData.Builder builder) { + if (!EXPORTER_PARSER_TABLE.containsKey(builder.getId())) { + EXPORTER_PARSER_TABLE.put(builder.getId(), new ExporterParser()); + } + ExporterParser parser = EXPORTER_PARSER_TABLE.get(builder.getId()); + Map metricFamilyMap = parser.textToMetric(resp); + String metrics = builder.getMetrics(); + if (metricFamilyMap.containsKey(metrics)) { + MetricFamily metricFamily = metricFamilyMap.get(metrics); + for (MetricFamily.Metric metric : metricFamily.getMetricList()) { + Map labelMap = metric.getLabelPair() + .stream() + .collect(Collectors.toMap(MetricFamily.Label::getName, MetricFamily.Label::getValue)); + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String aliasField : aliasFields) { + if ("value".equals(aliasField)) { + if (metric.getCounter() != null) { + valueRowBuilder.addColumns(String.valueOf(metric.getCounter().getValue())); + } else if (metric.getGauge() != null) { + valueRowBuilder.addColumns(String.valueOf(metric.getGauge().getValue())); + } else if (metric.getUntyped() != null) { + valueRowBuilder.addColumns(String.valueOf(metric.getUntyped().getValue())); + } else if (metric.getInfo() != null) { + valueRowBuilder.addColumns(String.valueOf(metric.getInfo().getValue())); + } + } else { + valueRowBuilder.addColumns(labelMap.get(aliasField)); + } + } + builder.addValues(valueRowBuilder.build()); + } + } + } + + private void parseResponseByDefault(String resp, List aliasFields, HttpProtocol http, + CollectRep.MetricsData.Builder builder, Long responseTime) { + JsonElement element = JsonParser.parseString(resp); + int keywordNum = CollectUtil.countMatchKeyword(resp, http.getKeyword()); + if (element.isJsonArray()) { + JsonArray array = element.getAsJsonArray(); + for (JsonElement jsonElement : array) { + getValueFromJson(aliasFields, builder, responseTime, jsonElement, keywordNum); + } + } else { + getValueFromJson(aliasFields, builder, responseTime, element, keywordNum); + } + } + + private void getValueFromJson(List aliasFields, CollectRep.MetricsData.Builder builder, Long responseTime, JsonElement element, int keywordNum) { + if (element.isJsonObject()) { + JsonObject object = element.getAsJsonObject(); + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String alias : aliasFields) { + JsonElement valueElement = object.get(alias); + if (valueElement != null) { + String value = valueElement.getAsString(); + valueRowBuilder.addColumns(value); + } else { + if (CollectorConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumns(responseTime.toString()); + } else if (CollectorConstants.KEYWORD.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumns(Integer.toString(keywordNum)); + } else { + valueRowBuilder.addColumns(CommonConstants.NULL_VALUE); + } + } + } + builder.addValues(valueRowBuilder.build()); + } + } + + /** + * create httpContext + * @param httpProtocol http protocol + * @return context + */ + public HttpContext createHttpContext(HttpProtocol httpProtocol) { + HttpProtocol.Authorization auth = httpProtocol.getAuthorization(); + if (auth != null && DispatchConstants.DIGEST_AUTH.equals(auth.getType())) { + HttpClientContext clientContext = new HttpClientContext(); + if (StringUtils.hasText(auth.getDigestAuthUsername()) + && StringUtils.hasText(auth.getDigestAuthPassword())) { + CredentialsProvider provider = new BasicCredentialsProvider(); + UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(auth.getDigestAuthUsername(), + auth.getDigestAuthPassword()); + provider.setCredentials(AuthScope.ANY, credentials); + AuthCache authCache = new BasicAuthCache(); + authCache.put(new HttpHost(httpProtocol.getHost(), Integer.parseInt(httpProtocol.getPort())), new DigestScheme()); + clientContext.setCredentialsProvider(provider); + clientContext.setAuthCache(authCache); + return clientContext; + } + } + return null; + } + + /** + * create http request + * @param httpProtocol http params + * @return http uri request + */ + public HttpUriRequest createHttpRequest(HttpProtocol httpProtocol) { + RequestBuilder requestBuilder; + String httpMethod = httpProtocol.getMethod().toUpperCase(); + if (HttpMethod.GET.matches(httpMethod)) { + requestBuilder = RequestBuilder.get(); + } else if (HttpMethod.POST.matches(httpMethod)) { + requestBuilder = RequestBuilder.post(); + } else if (HttpMethod.PUT.matches(httpMethod)) { + requestBuilder = RequestBuilder.put(); + } else if (HttpMethod.DELETE.matches(httpMethod)) { + requestBuilder = RequestBuilder.delete(); + } else if (HttpMethod.PATCH.matches(httpMethod)) { + requestBuilder = RequestBuilder.patch(); + } else { + // not support the method + log.error("not support the http method: {}.", httpProtocol.getMethod()); + return null; + } + // params + Map params = httpProtocol.getParams(); + if (params != null && !params.isEmpty()) { + for (Map.Entry param : params.entrySet()) { + if (StringUtils.hasText(param.getValue())) { + requestBuilder.addParameter(param.getKey(), param.getValue()); + } + } + } + // OpenAI /dashboard/billing/usage + if (OpenAIHost.equalsIgnoreCase(httpProtocol.getHost()) && OpenAIUsageAPI.equalsIgnoreCase(httpProtocol.getUrl())) { + LocalDate today = LocalDate.now(); + LocalDate tomorrow = LocalDate.now().plusDays(1); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + requestBuilder.addParameter(startDate, today.format(formatter)); + requestBuilder.addParameter(endDate, tomorrow.format(formatter)); + } + // The default request header can be overridden if customized + // keep-alive + requestBuilder.addHeader(HttpHeaders.CONNECTION, "keep-alive"); + requestBuilder.addHeader(HttpHeaders.USER_AGENT, "Mozilla/5.0 (Windows NT 6.1; WOW64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.76 Safari/537.36"); + // headers The custom request header is overwritten here + Map headers = httpProtocol.getHeaders(); + if (headers != null && !headers.isEmpty()) { + for (Map.Entry header : headers.entrySet()) { + if (StringUtils.hasText(header.getValue())) { + requestBuilder.addHeader(CollectUtil.replaceUriSpecialChar(header.getKey()), + CollectUtil.replaceUriSpecialChar(header.getValue())); + } + } + } + // add accept + if (DispatchConstants.PARSE_DEFAULT.equals(httpProtocol.getParseType()) + || DispatchConstants.PARSE_JSON_PATH.equals(httpProtocol.getParseType())) { + requestBuilder.addHeader(HttpHeaders.ACCEPT, "application/json"); + } else if (DispatchConstants.PARSE_XML_PATH.equals(httpProtocol.getParseType())) { + requestBuilder.addHeader(HttpHeaders.ACCEPT, "text/xml,application/xml"); + } else { + requestBuilder.addHeader(HttpHeaders.ACCEPT, "*/*"); + } + + if (httpProtocol.getAuthorization() != null) { + HttpProtocol.Authorization authorization = httpProtocol.getAuthorization(); + if (DispatchConstants.BEARER_TOKEN.equalsIgnoreCase(authorization.getType())) { + String value = DispatchConstants.BEARER + " " + authorization.getBearerTokenToken(); + requestBuilder.addHeader(HttpHeaders.AUTHORIZATION, value); + } else if (DispatchConstants.BASIC_AUTH.equals(authorization.getType())) { + if (StringUtils.hasText(authorization.getBasicAuthUsername()) + && StringUtils.hasText(authorization.getBasicAuthPassword())) { + String authStr = authorization.getBasicAuthUsername() + ":" + authorization.getBasicAuthPassword(); + String encodedAuth = new String(Base64.encodeBase64(authStr.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8); + requestBuilder.addHeader(HttpHeaders.AUTHORIZATION, DispatchConstants.BASIC + " " + encodedAuth); + } + } + } + + // if it has payload, would override post params + if (StringUtils.hasLength(httpProtocol.getPayload())) { + requestBuilder.setEntity(new StringEntity(httpProtocol.getPayload(), StandardCharsets.UTF_8)); + } + + // uri + String uri = CollectUtil.replaceUriSpecialChar(httpProtocol.getUrl()); + if (IpDomainUtil.isHasSchema(httpProtocol.getHost())) { + + requestBuilder.setUri(httpProtocol.getHost() + ":" + httpProtocol.getPort() + uri); + } else { + String ipAddressType = IpDomainUtil.checkIpAddressType(httpProtocol.getHost()); + String baseUri = CollectorConstants.IPV6.equals(ipAddressType) + ? String.format("[%s]:%s%s", httpProtocol.getHost(), httpProtocol.getPort(), uri) + : String.format("%s:%s%s", httpProtocol.getHost(), httpProtocol.getPort(), uri); + boolean ssl = Boolean.parseBoolean(httpProtocol.getSsl()); + if (ssl) { + requestBuilder.setUri(CollectorConstants.HTTPS_HEADER + baseUri); + } else { + requestBuilder.setUri(CollectorConstants.HTTP_HEADER + baseUri); + } + } + + // custom timeout + int timeout = CollectUtil.getTimeout(httpProtocol.getTimeout(), 0); + if (timeout > 0) { + RequestConfig requestConfig = RequestConfig.custom() + .setConnectTimeout(timeout) + .setSocketTimeout(timeout) + .setRedirectsEnabled(true) + .build(); + requestBuilder.setConfig(requestConfig); + } + return requestBuilder.build(); + } + + private boolean checkSuccessInvoke(Metrics metrics, int statusCode) { + List successCodes = metrics.getHttp().getSuccessCodes(); + Set successCodeSet = successCodes != null ? successCodes.stream().map(code -> { + try { + return Integer.valueOf(code); + } catch (Exception ignored) { + return null; + } + }).filter(Objects::nonNull).collect(Collectors.toSet()) : defaultSuccessStatusCodes; + if (successCodeSet.isEmpty()) { + successCodeSet = defaultSuccessStatusCodes; + } + return successCodeSet.contains(statusCode); + } +} diff --git a/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/http/SslCertificateCollectImpl.java b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/http/SslCertificateCollectImpl.java new file mode 100644 index 0000000..d214f10 --- /dev/null +++ b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/http/SslCertificateCollectImpl.java @@ -0,0 +1,165 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.http; + +import static org.apache.hertzbeat.common.constants.SignConstants.RIGHT_DASH; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.ConnectException; +import java.net.URL; +import java.net.UnknownHostException; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Date; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLPeerUnverifiedException; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.AbstractCollect; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.common.constants.CollectorConstants; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.Metrics; +import org.apache.hertzbeat.common.entity.job.protocol.HttpProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.CommonUtil; +import org.apache.hertzbeat.common.util.IpDomainUtil; +import org.springframework.util.StringUtils; + +/** + * ssl Certificate + */ +@Slf4j +public class SslCertificateCollectImpl extends AbstractCollect { + + private static final String NAME_SUBJECT = "subject"; + private static final String NAME_EXPIRED = "expired"; + private static final String NAME_START_TIME = "start_time"; + private static final String NAME_START_TIMESTAMP = "start_timestamp"; + private static final String NAME_END_TIME = "end_time"; + private static final String NAME_END_TIMESTAMP = "end_timestamp"; + + public SslCertificateCollectImpl() {} + + @Override + public void preCheck(Metrics metrics) throws IllegalArgumentException { + if (metrics == null || metrics.getHttp() == null) { + throw new IllegalArgumentException("Http/Https collect must has http params"); + } + } + + @Override + public void collect(CollectRep.MetricsData.Builder builder, + long monitorId, String app, Metrics metrics) { + long startTime = System.currentTimeMillis(); + + HttpProtocol httpProtocol = metrics.getHttp(); + String url = httpProtocol.getUrl(); + if (!StringUtils.hasText(url) || !url.startsWith(RIGHT_DASH)) { + httpProtocol.setUrl(StringUtils.hasText(url) ? RIGHT_DASH + url.trim() : RIGHT_DASH); + } + + HttpsURLConnection urlConnection = null; + try { + String uri = ""; + if (IpDomainUtil.isHasSchema(httpProtocol.getHost())) { + uri = httpProtocol.getHost() + ":" + httpProtocol.getPort(); + } else { + uri = "https://" + httpProtocol.getHost() + ":" + httpProtocol.getPort(); + } + urlConnection = (HttpsURLConnection) new URL(uri).openConnection(); + urlConnection.connect(); + Certificate[] certificates = urlConnection.getServerCertificates(); + if (certificates == null || certificates.length == 0) { + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg("Ssl certificate does not exist."); + return; + } + + long responseTime = System.currentTimeMillis() - startTime; + for (Certificate certificate : urlConnection.getServerCertificates()) { + X509Certificate x509Certificate = (X509Certificate) certificate; + Date now = new Date(); + Date deadline = x509Certificate.getNotAfter(); + boolean expired = deadline != null && now.after(deadline); + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String alias : metrics.getAliasFields()) { + if (CollectorConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumns(Long.toString(responseTime)); + } else if (NAME_SUBJECT.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumns(x509Certificate.getSubjectX500Principal().getName()); + } else if (NAME_EXPIRED.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumns(Boolean.toString(expired)); + } else if (NAME_START_TIME.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumns(x509Certificate.getNotBefore().toLocaleString()); + } else if (NAME_START_TIMESTAMP.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumns(String.valueOf(x509Certificate.getNotBefore().getTime())); + } else if (NAME_END_TIME.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumns(x509Certificate.getNotAfter().toLocaleString()); + } else if (NAME_END_TIMESTAMP.equalsIgnoreCase(alias)) { + valueRowBuilder.addColumns(String.valueOf(x509Certificate.getNotAfter().getTime())); + } else { + valueRowBuilder.addColumns(CommonConstants.NULL_VALUE); + } + } + builder.addValues(valueRowBuilder.build()); + } + } catch (SSLPeerUnverifiedException e1) { + String errorMsg = "Ssl certificate does not exist."; + if (e1.getMessage() != null) { + errorMsg = e1.getMessage(); + log.error(errorMsg); + } + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } catch (UnknownHostException e2) { + String errorMsg = CommonUtil.getMessageFromThrowable(e2); + log.info(errorMsg); + builder.setCode(CollectRep.Code.UN_REACHABLE); + builder.setMsg("unknown host:" + errorMsg); + } catch (InterruptedIOException | ConnectException | SSLException e3) { + String errorMsg = CommonUtil.getMessageFromThrowable(e3); + log.info(errorMsg); + builder.setCode(CollectRep.Code.UN_CONNECTABLE); + builder.setMsg(errorMsg); + } catch (IOException e4) { + String errorMsg = CommonUtil.getMessageFromThrowable(e4); + log.info(errorMsg); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } catch (Exception e) { + String errorMsg = CommonUtil.getMessageFromThrowable(e); + log.error(errorMsg, e); + builder.setCode(CollectRep.Code.FAIL); + builder.setMsg(errorMsg); + } finally { + if (urlConnection != null) { + urlConnection.disconnect(); + } + } + } + + @Override + public String supportProtocol() { + return DispatchConstants.PROTOCOL_SSL_CERT; + } + + private void validateParams(Metrics metrics) { + + } +} diff --git a/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/AbstractPrometheusParse.java b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/AbstractPrometheusParse.java new file mode 100644 index 0000000..2195ac2 --- /dev/null +++ b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/AbstractPrometheusParse.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.http.promethus; + +import java.util.List; +import org.apache.hertzbeat.common.entity.job.protocol.HttpProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; + +/** + * prometheus parse abstract class + * todo: parse response formats for string and scalar types + */ +public abstract class AbstractPrometheusParse { + + /** + * Downstream node + */ + private AbstractPrometheusParse prometheusParse; + + AbstractPrometheusParse() { + } + + public AbstractPrometheusParse setInstance(AbstractPrometheusParse prometheusParse) { + this.prometheusParse = prometheusParse; + return this; + } + + /** + * checks the Prometheus response type: string, matrix, vector, scalar + * todo:implementation for string and scalar types is missing + * @param responseStr The returned string + * @return boolean indicating the result + */ + abstract Boolean checkType(String responseStr); + + /** + * Parse the prom interface response data + * @param resp The returned data + * @param aliasFields alias fields + * @param http httpProtocol + * @param builder builder + */ + abstract void parse(String resp, List aliasFields, HttpProtocol http, + CollectRep.MetricsData.Builder builder); + + /** + * Processing prom interface response data + * @param resp resp + * @param aliasFields alias fields + * @param http http + * @param builder builder + */ + public void handle(String resp, List aliasFields, HttpProtocol http, + CollectRep.MetricsData.Builder builder) { + if (checkType(resp)) { + parse(resp, aliasFields, http, + builder); + } else { + prometheusParse.handle(resp, aliasFields, http, + builder); + } + } + + +} diff --git a/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/ParseException.java b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/ParseException.java new file mode 100644 index 0000000..a571e1c --- /dev/null +++ b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/ParseException.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.http.promethus; + +/** + * Parse Exception + */ +public class ParseException extends RuntimeException { + + public ParseException(String msg) { + super(msg); + } +} diff --git a/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/PrometheusLastParser.java b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/PrometheusLastParser.java new file mode 100644 index 0000000..fa60b33 --- /dev/null +++ b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/PrometheusLastParser.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.http.promethus; + +import java.util.List; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.job.protocol.HttpProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; + +/** + * Prometheus Last Parser + */ +@Slf4j +@NoArgsConstructor +public class PrometheusLastParser extends AbstractPrometheusParse { + @Override + public Boolean checkType(String responseStr) { + log.error("prometheus response data:{} ,no adaptive parser", responseStr); + return true; + } + + @Override + public void parse(String resp, List aliasFields, HttpProtocol http, CollectRep.MetricsData.Builder builder) { + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + aliasFields.forEach(aliasField -> valueRowBuilder.addColumns(CommonConstants.NULL_VALUE)); + } +} diff --git a/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/PrometheusMatrixParser.java b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/PrometheusMatrixParser.java new file mode 100644 index 0000000..e6c7e5c --- /dev/null +++ b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/PrometheusMatrixParser.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.http.promethus; + +import com.google.gson.JsonElement; +import java.math.BigDecimal; +import java.util.List; +import lombok.NoArgsConstructor; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.collector.util.CollectUtil; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.dto.PromVectorOrMatrix; +import org.apache.hertzbeat.common.entity.job.protocol.HttpProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.JsonUtil; + +/** + * Processing prometheus returns a response format of type "matrix" + */ +@NoArgsConstructor +public class PrometheusMatrixParser extends AbstractPrometheusParse { + @Override + public Boolean checkType(String responseStr) { + try { + PromVectorOrMatrix promVectorOrMatrix = JsonUtil.fromJson(responseStr, PromVectorOrMatrix.class); + if (promVectorOrMatrix != null && promVectorOrMatrix.getData() != null) { + return DispatchConstants.PARSE_PROM_QL_MATRIX.equals(promVectorOrMatrix.getData().getResultType()); + } + return false; + } catch (Exception e) { + return false; + } + } + + @Override + public void parse(String resp, List aliasFields, HttpProtocol http, CollectRep.MetricsData.Builder builder) { + PromVectorOrMatrix promVectorOrMatrix = JsonUtil.fromJson(resp, PromVectorOrMatrix.class); + if (promVectorOrMatrix == null){ + return; + } + List result = promVectorOrMatrix.getData().getResult(); + for (PromVectorOrMatrix.Result r : result) { + for (List value : r.getValues()) { + boolean setTimeFlag = false; + boolean setValueFlag = false; + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String aliasField : aliasFields) { + if (!CollectUtil.assertPromRequireField(aliasField)) { + JsonElement jsonElement = r.getMetric().get(aliasField); + if (jsonElement != null) { + valueRowBuilder.addColumns(jsonElement.getAsString()); + } else { + valueRowBuilder.addColumns(CommonConstants.NULL_VALUE); + } + } else { + if (CommonConstants.PROM_TIME.equals(aliasField)) { + for (Object o : value) { + if (o instanceof Double time) { + valueRowBuilder.addColumns(String.valueOf(BigDecimal.valueOf(time * 1000))); + setTimeFlag = true; + } + } + if (!setTimeFlag) { + valueRowBuilder.addColumns(CommonConstants.NULL_VALUE); + } + } else { + for (Object o : value) { + if (o instanceof String str) { + valueRowBuilder.addColumns(str); + setValueFlag = true; + } + } + if (!setValueFlag) { + valueRowBuilder.addColumns(CommonConstants.NULL_VALUE); + } + } + } + } + builder.addValues(valueRowBuilder); + } + + } + } +} diff --git a/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/PrometheusParseCreater.java b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/PrometheusParseCreater.java new file mode 100644 index 0000000..8078ec0 --- /dev/null +++ b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/PrometheusParseCreater.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.http.promethus; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.stereotype.Component; + +/** + * prometheus parse creater + */ +@Slf4j +@Component +public class PrometheusParseCreater implements InitializingBean { + private static AbstractPrometheusParse PROMETHEUSPARSE = new PrometheusVectorParser(); + + private static void create() { + PROMETHEUSPARSE.setInstance(new PrometheusMatrixParser().setInstance(new PrometheusLastParser())); + } + + public static AbstractPrometheusParse getPrometheusParse(){ + return PROMETHEUSPARSE; + } + + @Override + public void afterPropertiesSet() throws Exception { + create(); + } +} diff --git a/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/PrometheusVectorParser.java b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/PrometheusVectorParser.java new file mode 100644 index 0000000..d5d2873 --- /dev/null +++ b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/PrometheusVectorParser.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.http.promethus; + +import com.google.gson.JsonElement; +import java.math.BigDecimal; +import java.util.List; +import lombok.NoArgsConstructor; +import org.apache.hertzbeat.collector.dispatch.DispatchConstants; +import org.apache.hertzbeat.collector.util.CollectUtil; +import org.apache.hertzbeat.common.constants.CommonConstants; +import org.apache.hertzbeat.common.entity.dto.PromVectorOrMatrix; +import org.apache.hertzbeat.common.entity.job.protocol.HttpProtocol; +import org.apache.hertzbeat.common.entity.message.CollectRep; +import org.apache.hertzbeat.common.util.JsonUtil; + +/** + * Processing prometheus returns a response format of type "vector" + */ +@NoArgsConstructor +public class PrometheusVectorParser extends AbstractPrometheusParse { + @Override + public Boolean checkType(String responseStr) { + try { + PromVectorOrMatrix promVectorOrMatrix = JsonUtil.fromJson(responseStr, PromVectorOrMatrix.class); + if (promVectorOrMatrix != null && promVectorOrMatrix.getData() != null) { + return DispatchConstants.PARSE_PROM_QL_VECTOR.equals(promVectorOrMatrix.getData().getResultType()); + } + return false; + } catch (Exception e) { + return false; + } + } + + @Override + public void parse(String resp, List aliasFields, HttpProtocol http, CollectRep.MetricsData.Builder builder) { + boolean setTimeFlag = false; + boolean setValueFlag = false; + PromVectorOrMatrix promVectorOrMatrix = JsonUtil.fromJson(resp, PromVectorOrMatrix.class); + if (promVectorOrMatrix == null){ + return; + } + List result = promVectorOrMatrix.getData().getResult(); + for (PromVectorOrMatrix.Result r : result) { + CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder(); + for (String aliasField : aliasFields) { + if (!CollectUtil.assertPromRequireField(aliasField)) { + JsonElement jsonElement = r.getMetric().get(aliasField); + if (jsonElement != null) { + valueRowBuilder.addColumns(jsonElement.getAsString()); + } else { + valueRowBuilder.addColumns(CommonConstants.NULL_VALUE); + } + } else { + if (CommonConstants.PROM_TIME.equals(aliasField)) { + for (Object o : r.getValue()) { + if (o instanceof Double time) { + valueRowBuilder.addColumns(String.valueOf(BigDecimal.valueOf(time * 1000))); + setTimeFlag = true; + } + } + if (!setTimeFlag) { + valueRowBuilder.addColumns(CommonConstants.NULL_VALUE); + } + } else { + for (Object o : r.getValue()) { + if (o instanceof String str) { + valueRowBuilder.addColumns(str); + setValueFlag = true; + } + } + if (!setValueFlag) { + valueRowBuilder.addColumns(CommonConstants.NULL_VALUE); + } + } + } + } + builder.addValues(valueRowBuilder); + } + } +} diff --git a/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/exporter/ExporterParser.java b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/exporter/ExporterParser.java new file mode 100644 index 0000000..9f15c2f --- /dev/null +++ b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/exporter/ExporterParser.java @@ -0,0 +1,461 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.http.promethus.exporter; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import lombok.extern.slf4j.Slf4j; +import org.apache.hertzbeat.collector.collect.http.promethus.ParseException; +import org.apache.hertzbeat.common.util.StrBuffer; +import org.springframework.util.StringUtils; + +/** + * Resolves the data passed by prometheus's exporter interface http:xxx/metrics + * Reference: prometheus text_parse.go code, entry: TextToMetricFamilies + */ +@Slf4j +public class ExporterParser { + private static final String HELP = "HELP"; + private static final String TYPE = "TYPE"; + private static final String EOF = "EOF"; + + private static final String QUANTILE_LABEL = "quantile"; + private static final String BUCKET_LABEL = "le"; + private static final String NAME_LABEL = "__name__"; + private static final String SUM_SUFFIX = "_sum"; + private static final String COUNT_SUFFIX = "_count"; + + private static final char LEFT_CURLY_BRACKET = '{'; + private static final char RIGHT_CURLY_BRACKET = '}'; + private static final char EQUALS = '='; + private static final char QUOTES = '"'; + private static final char ENTER = '\n'; + private static final char SPACE = ' '; + private static final char COMMA = ','; + + private MetricFamily currentMetricFamily; + private String currentQuantile; + private String currentBucket; + + private final Lock lock = new ReentrantLock(); + + public Map textToMetric(String resp) { + // key: metric name, value: metric family + Map metricMap = new ConcurrentHashMap<>(10); + lock.lock(); + try { + String[] lines = resp.split("\n"); + for (String line : lines) { + this.parseLine(metricMap, new StrBuffer(line)); + } + return metricMap; + } catch (Exception e) { + log.error("parse prometheus exporter data error, msg: {}", e.getMessage()); + } finally { + lock.unlock(); + } + return metricMap; + } + + private void parseLine(Map metricMap, StrBuffer buffer) { + buffer.skipBlankTabs(); + if (buffer.isEmpty()) return; + switch (buffer.charAt(0)) { + case '#' -> { + buffer.read(); + this.currentMetricFamily = null; + this.parseComment(metricMap, buffer); + } + case ENTER -> {} + default -> { + this.currentBucket = null; + this.currentQuantile = null; + this.parseMetric(buffer); + } + } + } + + private void parseComment(Map metricMap, StrBuffer buffer) { + buffer.skipBlankTabs(); + if (buffer.isEmpty()) { + return; + } + String token = this.readTokenUnitWhitespace(buffer); + if (EOF.equals(token)) { + return; + } + if (!HELP.equals(token) && !TYPE.equals(token)) { + log.error("parse comment error {}, start without {} or {}", buffer.toStr(), HELP, TYPE); + return; + } + String metricName = this.readTokenAsMetricName(buffer); + this.currentMetricFamily = metricMap.computeIfAbsent(metricName, key -> new MetricFamily()); + this.currentMetricFamily.setName(metricName); + switch (token) { + case HELP -> this.parseHelp(buffer); + case TYPE -> this.parseType(buffer); + default -> {} + } + } + + private void parseHelp(StrBuffer line) { + line.skipBlankTabs(); + this.currentMetricFamily.setHelp(line.toStr()); + } + + private void parseType(StrBuffer line) { + line.skipBlankTabs(); + String type = line.toStr().toLowerCase(); + MetricType metricType = MetricType.getType(type); + if (metricType == null) { + throw new ParseException("pare type error"); + } + this.currentMetricFamily.setMetricType(metricType); + } + + private void parseMetric(StrBuffer buffer) { + String metricName = this.readTokenAsMetricName(buffer); + if (metricName.isEmpty()) { + log.error("error parse metric, metric name is null, line: {}", buffer.toStr()); + return; + } + List metricList = this.currentMetricFamily.getMetricList(); + if (metricList == null) { + metricList = new ArrayList<>(); + this.currentMetricFamily.setMetricList(metricList); + } + // TODO: This part may have issues. The current logic creates only one metric for both HISTOGRAM and SUMMARY + // compared to the source code, there is a slight modification: the source code stores parsing results in a property + // here, the results are passed through parameters. + MetricFamily.Metric metric; + if (!metricList.isEmpty() + && (this.currentMetricFamily.getMetricType().equals(MetricType.HISTOGRAM) + || this.currentMetricFamily.getMetricType().equals(MetricType.SUMMARY))) { + metric = metricList.get(0); + } else { + metric = new MetricFamily.Metric(); + metricList.add(metric); + } + + this.readLabels(metric, buffer); + } + + private void readLabels(MetricFamily.Metric metric, StrBuffer buffer) { + buffer.skipBlankTabs(); + if (buffer.isEmpty()) { + return; + } + metric.setLabelPair(new ArrayList<>()); + if (buffer.charAt(0) == LEFT_CURLY_BRACKET) { + buffer.read(); + this.startReadLabelName(metric, buffer); + } else { + this.readLabelValue(metric, null, buffer); + } + } + + private void startReadLabelName(MetricFamily.Metric metric, StrBuffer buffer) { + buffer.skipBlankTabs(); + if (buffer.isEmpty()) { + return; + } + if (buffer.charAt(0) == RIGHT_CURLY_BRACKET) { + buffer.read(); + buffer.skipBlankTabs(); + if (buffer.isEmpty()) { + return; + } + this.readLabelValue(metric, new MetricFamily.Label(), buffer); + return; + } + String labelName = this.readTokenAsLabelName(buffer); + if (labelName.isEmpty() || labelName.equals(NAME_LABEL)) { + throw new ParseException("invalid label name" + labelName + ", label name size = 0 or label name equals " + NAME_LABEL); + } + MetricFamily.Label label = new MetricFamily.Label(); + label.setName(labelName); + if (buffer.read() != EQUALS) { + throw new ParseException("parse error, not match the format of labelName=labelValue"); + } + this.startReadLabelValue(metric, label, buffer); + } + + private void startReadLabelValue(MetricFamily.Metric metric, MetricFamily.Label label, StrBuffer buffer) { + buffer.skipBlankTabs(); + if (buffer.isEmpty()) { + return; + } + char c = buffer.read(); + if (c != QUOTES) { + throw new ParseException("expected '\"' at start of label value, line: " + buffer.toStr()); + } + String labelValue = this.readTokenAsLabelValue(buffer); + label.setValue(labelValue); + if (!this.isValidLabelValue(labelValue)) { + throw new ParseException("no valid label value: " + labelValue); + } + if (this.currentMetricFamily.getMetricType().equals(MetricType.SUMMARY) && label.getName().equals(QUANTILE_LABEL)) { + this.currentQuantile = labelValue; + } else if (this.currentMetricFamily.getMetricType().equals(MetricType.HISTOGRAM) && label.getName().equals(BUCKET_LABEL)) { + this.currentBucket = labelValue; + } else { + metric.getLabelPair().add(label); + } + if (buffer.isEmpty()) { + return; + } + c = buffer.read(); + switch (c) { + case COMMA -> this.startReadLabelName(metric, buffer); + case RIGHT_CURLY_BRACKET -> this.readLabelValue(metric, label, buffer); + default -> throw new ParseException("expected '}' or ',' at end of label value, line: " + buffer.toStr()); + } + } + + private void readLabelValue(MetricFamily.Metric metric, MetricFamily.Label label, StrBuffer buffer) { + buffer.skipBlankTabs(); + if (buffer.isEmpty()) return; + switch (this.currentMetricFamily.getMetricType()) { + case INFO -> { + MetricFamily.Info info = new MetricFamily.Info(); + info.setValue(buffer.toDouble()); + metric.setInfo(info); + } + case COUNTER -> { + MetricFamily.Counter counter = new MetricFamily.Counter(); + counter.setValue(buffer.toDouble()); + metric.setCounter(counter); + } + case GAUGE -> { + MetricFamily.Gauge gauge = new MetricFamily.Gauge(); + gauge.setValue(buffer.toDouble()); + metric.setGauge(gauge); + } + case UNTYPED -> { + MetricFamily.Untyped untyped = new MetricFamily.Untyped(); + untyped.setValue(buffer.toDouble()); + metric.setUntyped(untyped); + } + case SUMMARY -> { + MetricFamily.Summary summary = metric.getSummary(); + if (summary == null) { + summary = new MetricFamily.Summary(); + metric.setSummary(summary); + } + // Process data for xxx_sum + if (label != null && this.isSum(label.getName())) { + summary.setSum(buffer.toDouble()); + } + // Process data for xxx_count + else if (label != null && this.isCount(label.getName())) { + summary.setCount(buffer.toLong()); + } + // Handle format for "xxx{quantile=\"0\"} 0" + else if (StringUtils.hasText(this.currentQuantile)) { + List quantileList = summary.getQuantileList(); + MetricFamily.Quantile quantile = new MetricFamily.Quantile(); + quantile.setXLabel(StrBuffer.parseDouble(this.currentQuantile)); + quantile.setValue(buffer.toDouble()); + quantileList.add(quantile); + } + } + case HISTOGRAM -> { + MetricFamily.Histogram histogram = metric.getHistogram(); + if (histogram == null) { + histogram = new MetricFamily.Histogram(); + metric.setHistogram(histogram); + } + if (label != null && this.isSum(label.getName())) { + histogram.setSum(buffer.toDouble()); + } else if (label != null && this.isCount(label.getName())) { + histogram.setCount(buffer.toLong()); + } + // Process the format "xxx{quantile=\"0\"} 0" + else if (StringUtils.hasText(this.currentBucket)) { + List bucketList = histogram.getBucketList(); + MetricFamily.Bucket bucket = new MetricFamily.Bucket(); + bucket.setUpperBound(StrBuffer.parseDouble(this.currentBucket)); + bucket.setCumulativeCount(buffer.toLong()); + bucketList.add(bucket); + } + } + default -> throw new ParseException("no such type in metricFamily"); + } + } + + /** + * Reads the token before the first whitespace + * + * @param buffer A line data object + * @return token unit + */ + private String readTokenUnitWhitespace(StrBuffer buffer) { + StringBuilder builder = new StringBuilder(); + while (!buffer.isEmpty()) { + char c = buffer.read(); + if (c == SPACE) { + break; + } + builder.append(c); + } + return builder.toString(); + } + + /** + * Gets the name of the metric + * + * @param buffer A line data object + * @return token name + */ + private String readTokenAsMetricName(StrBuffer buffer) { + buffer.skipBlankTabs(); + StringBuilder builder = new StringBuilder(); + if (this.isValidMetricNameStart(buffer.charAt(0))) { + while (!buffer.isEmpty()) { + char c = buffer.read(); + if (!this.isValidMetricNameContinuation(c)) { + buffer.rollback(); + break; + } + builder.append(c); + } + return builder.toString(); + } + throw new ParseException("parse metric name error"); + } + + /** + * Gets the name of the label + * + * @param buffer A line data object + * @return label name + */ + private String readTokenAsLabelName(StrBuffer buffer) { + buffer.skipBlankTabs(); + StringBuilder builder = new StringBuilder(); + char c = buffer.read(); + if (this.isValidLabelNameStart(c)) { + builder.append(c); + while (!buffer.isEmpty()) { + c = buffer.read(); + if (!this.isValidLabelNameContinuation(c)) { + buffer.rollback(); + break; + } + builder.append(c); + } + return builder.toString(); + } + throw new ParseException("parse label name error"); + } + + /** + * Gets the value of the label + * + * @param buffer A line data object + * @return label value + */ + private String readTokenAsLabelValue(StrBuffer buffer) { + StringBuilder builder = new StringBuilder(); + boolean escaped = false; + while (!buffer.isEmpty()) { + char c = buffer.read(); + // Handle '\\' escape sequences + if (escaped) { + switch (c) { + case QUOTES, '\\' -> builder.append(c); + case 'n' -> builder.append('\n'); + default -> throw new ParseException("parse label value error"); + } + escaped = false; + } else { + switch (c) { + case QUOTES -> { return builder.toString(); } + case ENTER -> throw new ParseException("parse label value error, next line"); + case '\\' -> escaped = true; + default -> builder.append(c); + } + } + } + return builder.toString(); + } + + /** + * Checks whether a character conforms to the first character rule for metric names + * + * @param c metric character + * @return true/false + */ + private boolean isValidMetricNameStart(char c) { + return isValidLabelNameStart(c) || c == ':'; + } + + /** + * Checks whether a character conforms to rules for metric name characters other than the first + * + * @param c metric character + * @return true/false + */ + private boolean isValidMetricNameContinuation(char c) { + return isValidLabelNameContinuation(c) || c == ':'; + } + + /** + * Checks whether a character conforms to the first character rule for label names + * + * @param c metric character + * @return true/false + */ + private boolean isValidLabelNameStart(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; + } + + /** + * Checks whether a character conforms to rules for label name characters other than the first + * + * @param c metric character + * @return true/false + */ + private boolean isValidLabelNameContinuation(char c) { + return isValidLabelNameStart(c) || (c >= '0' && c <= '9'); + } + + /** + * Checks if a string is a valid UTF-8 encoded string + * + * @param s label value + * @return true/false + */ + private boolean isValidLabelValue(String s) { + return s != null && s.equals(new String(s.getBytes(StandardCharsets.UTF_8))); + } + + private boolean isSum(String s) { + return s != null && s.endsWith(SUM_SUFFIX); + } + + private boolean isCount(String s) { + return s != null && s.endsWith(COUNT_SUFFIX); + } + +} diff --git a/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/exporter/MetricFamily.java b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/exporter/MetricFamily.java new file mode 100644 index 0000000..1b3b427 --- /dev/null +++ b/applications/hertzbeat/collector/src/main/java/org/apache/hertzbeat/collector/collect/http/promethus/exporter/MetricFamily.java @@ -0,0 +1,242 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hertzbeat.collector.collect.http.promethus.exporter; + +import java.util.ArrayList; +import java.util.List; +import lombok.Data; +import lombok.ToString; + +/** + * MetricFamily. + */ +@Data +@ToString +public class MetricFamily { + /** + * metric name + */ + private String name; + + /** + * metric help + */ + private String help; + + /** + * metric type + */ + private MetricType metricType; + + /** + * Specific metric + */ + private List metricList; + + /** + * Metric + */ + @Data + public static class Metric { + + /** + * Label data, mainly corresponding to the content within {} + */ + private List