Compare commits

...
Author SHA1 Message Date
tomsun28 793dbaf1ab [doc] update doc 2025-05-30 21:05:02 +08:00
Jast 518eebb6b1 [Doc] Update 1.7.1 (#3411) 2025-05-29 16:12:24 +08:00
Calvin 210454f969 [doc] japanese doris_be (#3409) 2025-05-29 08:03:06 +08:00
Calvin a8db5e31a0 [doc] japanese docker (#3408) 2025-05-28 07:16:33 +08:00
Calvin c01653aff3 [doc] japanese dns sd (#3405) 2025-05-27 21:27:58 +08:00
Calvin 480bd0e449 [doc] japanese dns (#3404) 2025-05-27 08:47:33 +08:00
97faca6753 [docs] update alert threshold doc (#3399)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: liutianyou <tianyou.liu@qq.com>
2025-05-26 16:12:50 +08:00
Calvinandtomsun28 e271bd7417 [doc] japanese deepseek & dm (#3400)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-26 08:53:40 +08:00
7de4fd1cbf [improve] http sd collector adds authentication (#3388)
Co-authored-by: Calvin <naruse_shinji@163.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-05-26 08:42:21 +08:00
Calvin c2a7d0db35 [doc] japanese Debian (#3398) 2025-05-25 15:42:54 +08:00
Calvin 93b2630bf3 [doc] japanese dahua (#3396) 2025-05-25 13:09:06 +08:00
Calvinandliutianyou 23749e7fac [doc] japanese coreos (#3393)
Co-authored-by: liutianyou <tianyou.liu@qq.com>
2025-05-24 09:35:46 +08:00
kangli d5eae40754 [doc]add new committer blog (#3395) 2025-05-24 08:23:17 +08:00
Calvin 7ef79de1de [doc] japanese consul sd (#3392) 2025-05-23 13:07:11 +08:00
tomsun28 8496771c8b [doc] new contributor and committer, update doc (#3391)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-05-23 12:15:09 +08:00
Calvin f31c835181 [doc] japanese clickhouse (#3390) 2025-05-23 08:42:33 +08:00
Logicandtomsun28 552554b96d [fix](warehouse): improve GreptimeDB data storage and querying (#3387)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-22 23:31:06 +08:00
zhangyaxiandtomsun28 4adcfd2a4e [test] Add unit tests for HttpSdCollectImpl (#3386)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-22 23:29:23 +08:00
Calvin e630beecb4 [doc] japanese cisco switch (#3389) 2025-05-22 22:46:53 +08:00
Duansgandtomsun28 6c2b38e180 [doc] modify supplement related documentation (#3383)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-21 23:17:50 +08:00
84986dd1bf [feature] support auto nacos service discovery (#3324)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Signed-off-by: zhangyaxi <yaxi.zhang950408@outlook.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Calvin <naruse_shinji@163.com>
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-05-21 22:01:09 +08:00
af868e083a [Task] Batch import metrics data in victoria-metrics (#3337)
Co-authored-by: yinyijun <yinyijun6@mgtv.com>
Co-authored-by: Calvin <naruse_shinji@163.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-05-20 23:56:56 +08:00
be3e2f3558 [doc] modify chinese words (#3380)
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-20 21:42:37 +08:00
d32818e8ef [feat] Support Zookeeper Service Discovery (#3377)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Calvin <naruse_shinji@163.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-20 21:30:48 +08:00
10b283522c [improve] add path validation for pluginservice (#3375)
Signed-off-by: aias00 <liuhongyu@apache.org>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: Calvin <naruse_shinji@163.com>
Co-authored-by: liutianyou <tianyou.liu@qq.com>
2025-05-20 12:32:24 +08:00
31ab2da41e [doc] Add blog (#3379)
Co-authored-by: Calvin <naruse_shinji@163.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-20 00:16:06 +08:00
LL-LINandtomsun28 a9436049e3 [doc] fix incorrect time template syntax usercase in doc (#3378)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-20 00:12:20 +08:00
Calvinandtomsun28 41c010511c [doc] japanese centos (#3376)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-20 00:08:39 +08:00
aias00andCalvin b5281c1274 update maven dep (#3359)
Co-authored-by: Calvin <naruse_shinji@163.com>
2025-05-20 00:06:59 +08:00
62cf3d1b2b [improve] improve jndi validation (#3358)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Calvin <naruse_shinji@163.com>
2025-05-18 17:45:09 +08:00
dc8ae844c1 [improve] improve url validation for serverChan (#3364)
Signed-off-by: aias00 <liuhongyu@apache.org>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: Calvin <naruse_shinji@163.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-18 16:48:31 +08:00
91a7593b87 [improve] improve url validation for SlackAlertNotifyHandlerImpl (#3363)
Signed-off-by: aias00 <liuhongyu@apache.org>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: Calvin <naruse_shinji@163.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-18 16:17:31 +08:00
4442a52adf [improve] improve url validation for TelegramBotAlertNotifyHandlerImpl (#3362)
Signed-off-by: aias00 <liuhongyu@apache.org>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-18 14:09:09 +08:00
60e3437e82 [improve] improve url validation for WeComRobotAlertNotifyHandlerImpl (#3361)
Signed-off-by: aias00 <liuhongyu@apache.org>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-18 13:31:39 +08:00
21d6ec2f1b [bugfix] Incorrect SD sub-monitor status (#3340)
Signed-off-by: Sherlock Yin <sherlock.yin1994@gmail.com>
Co-authored-by: yinyijun <yinyijun6@mgtv.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Calvin <naruse_shinji@163.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-05-18 01:00:37 +08:00
tomsun28 3ed50f39cb [doc] new ppmc liutianyou and update qq num (#3357)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-05-17 11:08:39 +08:00
Logic dfba21f224 [doc](start): update greptime-init documentation (#3355) 2025-05-16 20:41:08 +08:00
Sherlock Yinandtomsun28 9fb3f39eb1 [doc] update OS monitor (#3353)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-16 07:46:41 +08:00
Calvin 2430448089 [doc] japanese api (#3352) 2025-05-16 00:41:36 +08:00
Yang Chen 993de3d504 [bugfix] Fix incorrect webhook URLs for Uptime Kuma, Zabbix, and Tencent Cloud (#3351) 2025-05-15 20:03:19 +08:00
Jast 955ecd2128 [release] update release version 1.7.1 (#3347) 2025-05-15 15:20:52 +08:00
LL-LIN 96e5402c09 [bugfix] Fix frontend error when all monitor metrics are selected in new bulletin form (#3345) 2025-05-15 14:07:34 +08:00
Jast 0a6e8147e8 [build] Update .gitignore and add Maven wrapper properties (#3346) 2025-05-15 11:31:47 +08:00
7779056983 [feature] Support DNS Service Discovery (#3328)
Signed-off-by: Sherlock Yin <sherlock.yin1994@gmail.com>
Co-authored-by: yinyijun <yinyijun6@mgtv.com>
Co-authored-by: yinyijun <yingey2011>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-15 08:25:50 +08:00
Calvinandtomsun28 ca3348adf2 [doc] japanese almalinux (#3343)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-15 00:07:06 +08:00
dc822f50e8 [feat] Enhance Label Management for Monitors (#3327)
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: Calvin <naruse_shinji@163.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-14 23:31:15 +08:00
896adb76a2 [feature] Support Consul Service Discovery (#3326)
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: Calvin <naruse_shinji@163.com>
2025-05-14 23:21:00 +08:00
Calvin 2a49fa5774 [doc] japanese airflow (#3339) 2025-05-14 21:54:54 +08:00
aias00 2259d51cae [feat] add deep wiki badge (#3334) 2025-05-13 14:38:31 +08:00
Calvin e9e49cfbc5 [doc] japanese activemq (#3333) 2025-05-13 08:13:14 +08:00
kangliandtomsun28 3eba906491 [feature] Support Eureka Service Discovery (#3323)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-12 20:35:13 +08:00
6645c5d4b2 [Improve] Add i18 for ai bot (#3330)
Co-authored-by: yinyijun <yinyijun6@mgtv.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-12 20:06:16 +08:00
Calvin 88fb007ef0 [doc] japanese readme (#3329) 2025-05-12 16:03:10 +08:00
LogicandJast 4892ef101f [feature]: integrate OpenTelemetry for GreptimeDB logs and traces- Rename hertzbeat-log to hertzbeat-otel (#3319)
Co-authored-by: Jast <shenghang@apache.org>
2025-05-09 15:03:49 +08:00
tomsun28andshown d0b7e38537 [bugfix] fix the calculator expr exist not work (#3314)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: shown <yuluo08290126@gmail.com>
2025-05-09 13:15:46 +08:00
096153b3a4 [license] update the license file, to add the plc4j-driver-s7 license (#3318)
Co-authored-by: cuijianzhong <shangyuancjz@126.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: aias00 <rokkki@163.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-05-07 22:00:05 +08:00
zhangyaxi b313db36c6 [feature] Support Zabbix alert source (#3317)
Signed-off-by: zhangyaxi <yaxi.zhang950408@outlook.com>
2025-05-07 08:54:23 +08:00
tomsun28andCopilot 47298033dc [refactor] set default encoding charset utf8 (#3315)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-05 23:30:30 +08:00
tomsun28 f05e91247a [refactor] refactor auto discovery http sd (#3300)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-05-04 00:07:06 +08:00
zhangyaxiandtomsun28 1592c93d25 [feature] Support Uptime Kuma alert source (#3312)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-01 14:31:52 +08:00
Yang Chenandtomsun28 46d609a95e [docs] add help documentation for alert integration (#3308)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-04-30 16:05:59 +08:00
Yang Chen f17fb02441 [docs] Correct Markdown formatting and update lint config to enable local checking (#3310) 2025-04-30 11:08:38 +08:00
e6cc71ed56 [Improve] Add unit test for PeriodicAlertCalculator (#3304)
Co-authored-by: yinyijun <yinyijun6@mgtv.com>
Co-authored-by: yinyijun <yingey2011>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-04-28 17:46:14 +08:00
Cyanty e2a9160e59 [refactor] Remove the possible security in action and ignore the 500 code for link check (#3303) 2025-04-28 11:30:14 +08:00
7ebeee2c1f [docs] Add Chinese and English versions of the Hertzbeat upgrade guide (#3294)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-04-27 00:26:40 +08:00
Cyantyandtomsun28 ccfc8dc8ce [improve] Optimize the way of Dead Link Check (#3302)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-04-26 23:04:49 +08:00
Yang Chenandaias00 ca9db17241 [bugfix] enables support for VictoriaMetrics in cluster mode within HertzBeat (#3297)
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-04-26 22:29:47 +08:00
Cyanty 7a40bdee12 [Doc] Add English doc for springboot auto practice usecase (#3298) 2025-04-26 20:40:16 +08:00
tomsun28 5db20d33f4 [bugfix] fix collector direct oom (#3295) 2025-04-24 20:22:50 +08:00
Cyanty 896869c611 [Doc] Add springboot auto practice usecase (#3293)
Signed-off-by: Cyanty <153884653+Cyanty@users.noreply.github.com>
2025-04-24 12:51:04 +08:00
tomsun28 32ce88b2c0 [webapp] try reduce memory growth and fix crash (#3292)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-04-23 19:47:24 +08:00
2288d8e900 [e2e] add e2e test for k8s monitor (#3280)
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Jast <shenghang@apache.org>
2025-04-22 23:57:01 +08:00
tomsun28andJast dbb81d44ff [doc] update doc, usecase blog and help doc (#3286)
Co-authored-by: Jast <shenghang@apache.org>
2025-04-22 22:33:47 +08:00
411 changed files with 11335 additions and 3334 deletions
+19
View File
@@ -2485,6 +2485,25 @@
"contributions": [
"code"
]
},
{
"login": "Duansg",
"name": "Duansg",
"avatar_url": "https://avatars.githubusercontent.com/u/112607719?v=4",
"profile": "https://github.com/Duansg",
"contributions": [
"doc"
]
},
{
"login": "xiaomizhou2",
"name": "zhangyaxi",
"avatar_url": "https://avatars.githubusercontent.com/u/47807926?v=4",
"profile": "https://github.com/xiaomizhou2",
"contributions": [
"code",
"test"
]
}
],
"contributorsPerLine": 7,
+53
View File
@@ -0,0 +1,53 @@
# 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.
# This workflow will build a Java project with Maven
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
name: Monitor E2E Test
on:
schedule:
# trigger at 01:00 everyday
- cron: '0 1 * * *'
push:
branches: [ action* ]
jobs:
check-license-header:
name: Monitor E2E Test
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: ./script/ci/github-actions/setup-deps
- name: Build with Maven
run: mvn clean -B package -Prelease -Dmaven.test.skip=true --file pom.xml
- name: Build Image
uses: docker/build-push-action@v3
with:
context: ./dist
file: ./script/docker/server/Dockerfile
push: false
tags: apache/hertzbeat:test
- name: Run K8s Monitor E2E Test
run: |
sudo curl -L https://github.com/docker/compose/releases/download/v2.23.0/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose
sudo chmod u+x /usr/local/bin/docker-compose
cd e2e/k8s
sudo docker-compose version
sudo docker-compose up --exit-code-from testing --remove-orphans
+2 -1
View File
@@ -6,13 +6,14 @@ Thumbs.db
build/
out/
micronaut-cli.yml
.mvn/
*.log
package-lock.json
*.zip
jdk/
data/
!/e2e/**/data/
### STS ###
.apt_generated
.classpath
+5 -4
View File
@@ -15,6 +15,7 @@
// limitations under the License.
//
{
"globs": ["home/**/*.md"],
"config": {
"default": true,
"MD001": true,
@@ -39,9 +40,9 @@
"MD047": true,
"MD052": true
},
"ignore": [
"node_modules/",
"target/",
"dist/"
"ignores": [
"**/node_modules/**",
"**/target/**",
"**/dist/**"
]
}
+17
View File
@@ -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.
wrapperVersion=3.3.2
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
+10 -4
View File
@@ -4,9 +4,10 @@
</a>
</p>
<h4 align="center">
<a href="README.md">English Document</a> | <a href="README_CN.md">中文文档</a>
</h4>
<p align="center">
<b>Readme</b>:
<b><a href="README.md">English</a></b> | <a href="README_CN.md">中文</a> | <a href="README_JP.md">日本語</a>
</p>
> A real-time monitoring system with agentless, performance cluster, prometheus-compatible, custom monitoring and status page building capabilities.
@@ -19,6 +20,7 @@
[![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/hertzbeat)](https://artifacthub.io/packages/search?repo=hertzbeat)
[![YouTube Channel Subscribers](https://img.shields.io/youtube/channel/subscribers/UCri75zfWX0GHqJFPENEbLow?logo=youtube&label=YouTube%20Channel)](https://www.youtube.com/channel/UCri75zfWX0GHqJFPENEbLow)
[![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod&color=green)](https://gitpod.io/#https://github.com/apache/hertzbeat)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/apache/hertzbeat)
**Home: [hertzbeat.apache.org](https://hertzbeat.apache.org)**
**Email: <a href="mailto:dev-subscribe@hertzbeat.apache.org">Mail to `dev-subscribe@hertzbeat.apache.org`</a> to subscribe mailing lists**
@@ -521,6 +523,10 @@ Thanks to these wonderful people, welcome to join us:
<td align="center" valign="top" width="14.28%"><a href="https://github.com/boyucjz"><img src="https://avatars.githubusercontent.com/u/18730041?v=4?s=100" width="100px;" alt="boyucjz"/><br /><sub><b>boyucjz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=boyucjz" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Cyanty"><img src="https://avatars.githubusercontent.com/u/153884653?v=4?s=100" width="100px;" alt="Cyanty"/><br /><sub><b>Cyanty</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KevinLLF"><img src="https://avatars.githubusercontent.com/u/85452733?v=4?s=100" width="100px;" alt="Jay丿167"/><br /><sub><b>Jay丿167</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=KevinLLF" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Duansg"><img src="https://avatars.githubusercontent.com/u/112607719?v=4?s=100" width="100px;" alt="Duansg"/><br /><sub><b>Duansg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Duansg" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaomizhou2"><img src="https://avatars.githubusercontent.com/u/47807926?v=4?s=100" width="100px;" alt="zhangyaxi"/><br /><sub><b>zhangyaxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Tests">⚠️</a></td>
</tr>
</tbody>
</table>
@@ -542,7 +548,7 @@ WeChat Group : Add friend `ahertzbeat` and invite to the group.
WeChat Public : Search ID `usthecom`.
[QQ Group](https://jq.qq.com/?_wv=1027&k=Bud9OzdI) : Group num `630061200`
[QQ Group](https://qm.qq.com/q/xxqecSC2cw) : Group num `1035688434`
[Github Discussion](https://github.com/apache/hertzbeat/discussions)
+13 -8
View File
@@ -4,9 +4,10 @@
</a>
</p>
<h4 align="center">
<a href="README_CN.md">中文文档</a> | <a href="README.md">English Document</a>
</h4>
<p align="center">
<b>Readme</b>:
<a href="README.md">English</a> | <b><a href="README_CN.md">中文</a></b> | <a href="README_JP.md">日本語</a>
</p>
> 实时监控系统,无需 Agent,性能集群,兼容 Prometheus,自定义监控和状态页构建能力。
@@ -33,7 +34,7 @@
-**监控+告警+通知** 为一体,支持对应用服务,应用程序,数据库,缓存,操作系统,大数据,中间件,Web 服务器,云原生,网络,自定义等监控阈值告警通知一步到位。
- 易用友好,无需 `Agent`,全 `WEB` 页面操作,鼠标点一点就能监控告警,零上手学习成本。
-`Http, Jmx, Ssh, Snmp, Jdbc, Prometheus` 等协议规范可配置化,只需在浏览器配置监控模 `YML` 就能使用这些协议去自定义采集想要的指标。您相信只需配置下就能立刻适配一款 `K8s``Docker` 等新的监控类型吗?
-`Http, Jmx, Ssh, Snmp, Jdbc, Prometheus` 等协议规范可配置化,只需在浏览器配置监控模 `YML` 就能使用这些协议去自定义采集想要的指标。您相信只需配置下就能立刻适配一款 `K8s``Docker` 等新的监控类型吗?
- 兼容 `Prometheus` 的系统生态并且更多,只需页面操作就可以监控 `Prometheus` 所能监控的。
- 高性能,支持多采集器集群横向扩展,支持多隔离网络监控,云边协同。
- 自由的告警阈值规则,`邮件` `Discord` `Slack` `Telegram` `钉钉` `微信` `飞书` `短信` `Webhook` `Server酱` 等方式消息及时送达。
@@ -58,8 +59,8 @@
## ⛄ 已支持
> 我们将监控采集类型(mysql,jvm,k8s)都定义为 `yml` 监控模,用户可以导入这些模来支持对应类型的监控!
> 欢迎大家一起贡献你使用过程中自定义的通用监控类型监控模
> 我们将监控采集类型(mysql,jvm,k8s)都定义为 `yml` 监控模,用户可以导入这些模来支持对应类型的监控!
> 欢迎大家一起贡献你使用过程中自定义的通用监控类型监控模
- [Website](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-website.yml), [Port Telnet](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-port.yml),
[Http Api](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-api.yml), [Ping Connect](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ping.yml),
@@ -97,7 +98,7 @@
- [CiscoSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-cisco_switch.yml), [HpeSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-hpe_switch.yml),
[HuaweiSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-huawei_switch.yml), [TpLinkSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-tplink_switch.yml),
[H3cSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-h3c_switch.yml)
- 和更多自定义监控模
- 和更多自定义监控模
- 通知支持 `Discord` `Slack` `Telegram` `邮件` `钉钉` `微信` `飞书` `短信` `Webhook` `Server酱`
## 🐕 快速开始
@@ -520,6 +521,10 @@ Thanks these wonderful people, welcome to join us:
<td align="center" valign="top" width="14.28%"><a href="https://github.com/boyucjz"><img src="https://avatars.githubusercontent.com/u/18730041?v=4?s=100" width="100px;" alt="boyucjz"/><br /><sub><b>boyucjz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=boyucjz" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Cyanty"><img src="https://avatars.githubusercontent.com/u/153884653?v=4?s=100" width="100px;" alt="Cyanty"/><br /><sub><b>Cyanty</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KevinLLF"><img src="https://avatars.githubusercontent.com/u/85452733?v=4?s=100" width="100px;" alt="Jay丿167"/><br /><sub><b>Jay丿167</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=KevinLLF" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Duansg"><img src="https://avatars.githubusercontent.com/u/112607719?v=4?s=100" width="100px;" alt="Duansg"/><br /><sub><b>Duansg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Duansg" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaomizhou2"><img src="https://avatars.githubusercontent.com/u/47807926?v=4?s=100" width="100px;" alt="zhangyaxi"/><br /><sub><b>zhangyaxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Tests">⚠️</a></td>
</tr>
</tbody>
</table>
@@ -541,7 +546,7 @@ Thanks these wonderful people, welcome to join us:
微信公众号 : 搜索 ID `usthecom`.
[QQ交流群](https://jq.qq.com/?_wv=1027&k=Bud9OzdI) : 群号 `630061200`
[QQ交流群](https://qm.qq.com/q/xxqecSC2cw) : 群号 `1035688434`
[Github Discussion](https://github.com/apache/hertzbeat/discussions)
+578
View File
@@ -0,0 +1,578 @@
<p align="center">
<a href="https://hertzbeat.apache.org">
<img alt="hertzbeat" src="/home/static/img/hertzbeat-brand.svg" width="260">
</a>
</p>
<p align="center">
<b>Readme</b>:
<a href="README.md">English</a> | <a href="README_CN.md">中文</a> | <b><a href="README_JP.md">日本語</a></b>
</p>
> オープンソースのリアルタイム監視システム、エージェントレス、クラスタ対応、Prometheus互換、カスタムおよびステータスページ付き。
[![Discord](https://img.shields.io/badge/Chat-Discord-7289DA?logo=discord)](https://discord.gg/Fb6M73htGr)
[![Reddit](https://img.shields.io/badge/Reddit-Community-7289DA?logo=reddit)](https://www.reddit.com/r/hertzbeat/)
[![Twitter](https://img.shields.io/twitter/follow/hertzbeat1024?logo=twitter)](https://x.com/hertzbeat1024)
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/8139/badge)](https://www.bestpractices.dev/projects/8139)
[![codecov](https://codecov.io/gh/apache/HertzBeat/branch/master/graph/badge.svg)](https://app.codecov.io/gh/apache/hertzbeat)
[![Docker Pulls](https://img.shields.io/docker/pulls/apache/hertzbeat?style=%20for-the-badge&logo=docker&label=DockerHub%20Download)](https://hub.docker.com/r/apache/hertzbeat)
[![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/hertzbeat)](https://artifacthub.io/packages/search?repo=hertzbeat)
[![YouTube Channel Subscribers](https://img.shields.io/youtube/channel/subscribers/UCri75zfWX0GHqJFPENEbLow?logo=youtube&label=YouTube%20Channel)](https://www.youtube.com/channel/UCri75zfWX0GHqJFPENEbLow)
[![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod&color=green)](https://gitpod.io/#https://github.com/apache/hertzbeat)
**公式サイト: [hertzbeat.apache.org](https://hertzbeat.apache.org)**
**メール:** メーリングリストに登録するために、<a href="mailto:dev-subscribe@hertzbeat.apache.org">dev-subscribe@hertzbeat.apache.org</a>にメールを送ってください。
## 🎡 <font color="green">紹介</font>
[Apache HertzBeat](https://github.com/apache/hertzbeat) incubating)はオープンソースのリアルタイム監視システム、エージェントレス、クラスタ対応、Prometheus互換、カスタムおよびステータスページ付き。
### 特性
- **モニター・アラーム・通知** を一体化し、Web、データベース、キャッシュ、OS、ミドルウェア、クラウドネイティブ、ネットワークなどをサポート。
- 使いやすさを重視し、マウスのクリックだけで完全なWebベースの操作が可能。
- `Http、Jmx、Ssh、Snmp、jdbc、Prometheus`などのプロトコルを設定可能にし、テンプレート`YML`ファイルをオンラインで設定するだけで、あらゆるメトリクスを収集できるようにします。HertzBeatでオンライン設定するだけで、`K8S``Docker`のような新しい監視タイプに素早く対応できることを想像してみてください。
- `Prometheus`のエコシステム互換、Prometheusが監視できるものを、全てオンライン設定によって監視することができます。
- 高性能で、コレクタークラスタ、マルチアイソレートネットワーク、クラウドエッジをサポート。
- 柔軟なアラーム閾値ルールで、メール、discord、slack、telegramなどを通じてタイムリーに通知。
- 強力なステータスページを簡単に構築し、リアルタイムのステータスをユーザーと共有。
> `HertzBeat`の強力なカスタマイズ、多種類のサポート、高性能および容易な拡張性は、ユーザーが迅速に独自の監視システムを構築するのを支援することを目的としています。
----
![hertzbeat](home/static/img/home/1.png)
![hertzbeat](home/static/img/home/status.png)
![hertzbeat](home/static/img/home/9.png)
----
## 🥐 モジュール
![hertzBeat](home/static/img/docs/hertzbeat-arch.png)
## ⛄ 利用可能
> `mysql`、`Java仮想マシン`、`k8s` などのすべての監視コレクション タイプを `YML` 監視テンプレートとして定義し、ユーザーがそれらをインポートして対応するタイプの監視をサポートできるようにしています。
> カスタマイズされた一般的なモニターのテンプレートを提供することを歓迎します。
- [Website](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-website.yml), [Port Telnet](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-port.yml),
[Http Api](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-api.yml), [Ping Connect](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ping.yml),
[Jvm](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-jvm.yml), [SiteMap](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-fullsite.yml),
[Ssl Certificate](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ssl_cert.yml), [SpringBoot2](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-springboot2.yml),
[FTP Server](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ftp.yml), [SpringBoot3](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-springboot3.yml),
[Udp Port](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-udp_port.yml), [Dns](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-dns.yml),
[Pop3](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-pop3.yml), [Ntp](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ntp.yml),
[Api Code](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-api_code.yml), [Smtp](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-smtp.yml),
[Nginx](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-nginx.yml)
- [Mysql](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-mysql.yml), [PostgreSQL](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-postgresql.yml),
[MariaDB](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-mariadb.yml), [Redis](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-redis.yml),
[ElasticSearch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-elasticsearch.yml), [SqlServer](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-sqlserver.yml),
[Oracle](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-oracle.yml), [MongoDB](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-mongodb.yml),
[DM](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-dm.yml), [OpenGauss](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-opengauss.yml),
[ClickHouse](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-clickhouse.yml), [IoTDB](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-iotdb.yml),
[Redis Cluster](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-redis_cluster.yml), [Redis Sentinel](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-redis_sentinel.yml)
[Doris BE](https://github.com/apache/hertzbeat/blob/master/hertzbeat-manager/src/main/resources/define/app-doris_be.yml), [Doris FE](https://github.com/apache/hertzbeat/blob/master/hertzbeat-manager/src/main/resources/define/app-doris_fe.yml),
[Memcached](https://github.com/apache/hertzbeat/blob/master/hertzbeat-manager/src/main/resources/define/app-memcached.yml), [NebulaGraph](https://github.com/apache/hertzbeat/blob/master/hertzbeat-manager/src/main/resources/define/app-nebula_graph.yml)
- [Linux](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-linux.yml), [Ubuntu](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ubuntu.yml),
[CentOS](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-centos.yml), [Windows](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-windows.yml),
[EulerOS](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-euleros.yml), [Fedora CoreOS](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-coreos.yml),
[OpenSUSE](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-opensuse.yml), [Rocky Linux](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-rockylinux.yml),
[Red Hat](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-redhat.yml), [FreeBSD](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-freebsd.yml),
[AlmaLinux](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-almalinux.yml), [Debian Linux](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-debian.yml)
- [Tomcat](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-tomcat.yml), [Nacos](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-nacos.yml),
[Zookeeper](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-zookeeper.yml), [RabbitMQ](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-rabbitmq.yml),
[Flink](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-flink.yml), [Kafka](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-kafka.yml),
[ShenYu](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-shenyu.yml), [DynamicTp](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-dynamic_tp.yml),
[Jetty](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-jetty.yml), [ActiveMQ](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-activemq.yml),
[Spring Gateway](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-spring_gateway.yml), [EMQX MQTT](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-emqx.yml),
[AirFlow](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-airflow.yml), [Hive](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-hive.yml),
[Spark](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-spark.yml), [Hadoop](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-hadoop.yml)
- [Kubernetes](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-kubernetes.yml), [Docker](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-docker.yml)
- [CiscoSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-cisco_switch.yml), [HpeSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-hpe_switch.yml),
[HuaweiSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-huawei_switch.yml), [TpLinkSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-tplink_switch.yml),
[H3cSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-h3c_switch.yml)
- その他、カスタマイズされたモニターテンプレート。
- 通知利用可能 `Discord` `Slack` `Telegram` `Email` `Dingtalk` `WeChat` `FeiShu` `Webhook` `SMS` `ServerChan`
## 🐕 クイックスタート
- HertzBeat をローカルに展開する場合は、以下のデプロイメントドキュメントを参照してください。
### 🍞 HertzBeatのインストール
> HertzBeatは、ソースコードのインストールとブート、Dockerコンテナの実行とインストールパッケージによるインストールとデプロイをサポートし、CPUアーキテクチャはx86/arm64をサポートします。
##### 方式1:Docker
1. `docker` で以下の指令を実行します:
```shell
docker run -d -p 1157:1157 -p 1158:1158 --name hertzbeat apache/hertzbeat
```
2. スタート:`http://localhost:4200`にアクセスします。デフォルトのアカウントとパスワード:`admin/hertzbeat`。
3. コレクタークラスタのデプロイメント(オプション)
```shell
docker run -d -e IDENTITY=custom-collector-name -e MANAGER_HOST=127.0.0.1 -e MANAGER_PORT=1158 --name hertzbeat-collector apache/hertzbeat-collector
```
- `-e IDENTITY=custom-collector-name` :コレクターのユニーク ID。
- `-e MODE=public` :実行モード(パブリッククラスタまたはプライベートクラウドエッジ)。
- `-e MANAGER_HOST=127.0.0.1` :メインhertzbeatサーバーのIP。
- `-e MANAGER_PORT=1158` :メインhertzbeatサーバポート。
詳細ステップ [通过 Docker 方式安装 HertzBeat](https://hertzbeat.apache.org/docs/start/docker-deploy)
##### 方式2:インストールパッケージ
1. リリースパッケージをダウンロード `hertzbeat-xx.tar.gz` [Download](https://hertzbeat.apache.org/docs/download)
2. HertzBeatのymlファイルを設定 `hertzbeat/config/application.yml` (オプション)
3. コマンド`$ ./bin/startup.sh`または`bin/startup.bat`を実行
4. スタート:`http://localhost:4200`にアクセスします。デフォルトのアカウントとパスワード:`admin/hertzbeat`
5. コレクタークラスタのデプロイメント(オプション)
- コレクターパッケージを別のホストにダウンロード `hertzbeat-collector-xx.tar.gz` [Download](https://hertzbeat.apache.org/docs/download)
- コレクターのymlファイルを設定 `hertzbeat-collector/config/application.yml`
```yaml
collector:
dispatch:
entrance:
netty:
enabled: true
identity: ${IDENTITY:}
mode: ${MODE:public}
manager-host: ${MANAGER_HOST:127.0.0.1}
manager-port: ${MANAGER_PORT:1158}
```
- `identity: ${IDENTITY:}`:コレクターのユニークID。
- `mode: ${MODE:public}`:実行モード(パブリッククラスタまたはプライベートクラウドエッジ)。
- `manager-host: ${MANAGER_HOST:127.0.0.1}`:メインhertzbeatサーバーのIP。
- `manager-port: ${MANAGER_PORT:1158}`:メインhertzbeatサーバポート。
- コマンド`$ ./bin/startup.sh`または`bin/startup.bat`を実行。
- `http://localhost:1157`にアクセスし、登録された新しいコレクターを見ることがでます。
詳細ステップ [通过安装包安装HertzBeat](https://hertzbeat.apache.org/docs/start/package-deploy)
##### 方式3:ローカルの実行
1. ローカルの実行には、バックエンドのプロジェクト`manager`とフロントエンドのプロジェクト`web-app`を起動する必要があります。
2. バックエンド:`maven3+`、`java17`と`lombok`の環境は必要です。`YML` 設定を修正し、Java仮想マシンパラメータに`--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED` を追加し、`manager` を起動します。
3. フロントエンド:`nodejs npm angular-cli`の環境は必要です。ローカルのバックエンドが立ち上がったら、`web-app` ディレクトリで `ng serve --open` というコマンドを実行します。
4. スタート:`http://localhost:4200`にアクセスします。デフォルトのアカウントとパスワード:`admin/hertzbeat`。
詳細ステップ [貢献ガイド](CONTRIBUTING.md)
##### 方式4:Docker-Compose
[Docker-Compose 部署脚本](script/docker-compose)でpostgresql/mysqlデータベース、victoria-metrics、iotdb、またはtdengine時系列データベースとHertzbeat一括デプロイ。
詳細ステップ [通过 Docker-Compose 安装 HertzBeat](script/docker-compose/README.md)
##### 方式5:Kubernetes Helm Charts
Helm ChartでHertzBeatクラスタコンポーネントをKubernetesクラスタに一括デプロイ。
詳細ステップ [Artifact Hub](https://artifacthub.io/packages/helm/hertzbeat/hertzbeat)
**HAVE FUN**
## ✨ Contributors
Thanks these wonderful people, welcome to join us:
[貢献ガイド](CONTRIBUTING.md)
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tomsun28"><img src="https://avatars.githubusercontent.com/u/24788200?v=4?s=100" width="100px;" alt="tomsun28"/><br /><sub><b>tomsun28</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tomsun28" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=tomsun28" title="Documentation">📖</a> <a href="#design-tomsun28" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wang1027-wqh"><img src="https://avatars.githubusercontent.com/u/71161318?v=4?s=100" width="100px;" alt="会编程的王学长"/><br /><sub><b>会编程的王学长</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wang1027-wqh" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=wang1027-wqh" title="Documentation">📖</a> <a href="#design-wang1027-wqh" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.maxkey.top/"><img src="https://avatars.githubusercontent.com/u/1563377?v=4?s=100" width="100px;" alt="MaxKey"/><br /><sub><b>MaxKey</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=shimingxy" title="Code">💻</a> <a href="#design-shimingxy" title="Design">🎨</a> <a href="#ideas-shimingxy" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://blog.gcdd.top/"><img src="https://avatars.githubusercontent.com/u/26523525?v=4?s=100" width="100px;" alt="观沧海"/><br /><sub><b>观沧海</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gcdd1993" title="Code">💻</a> <a href="#design-gcdd1993" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Agcdd1993" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/a25017012"><img src="https://avatars.githubusercontent.com/u/32265356?v=4?s=100" width="100px;" alt="yuye"/><br /><sub><b>yuye</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=a25017012" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=a25017012" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jx10086"><img src="https://avatars.githubusercontent.com/u/5323228?v=4?s=100" width="100px;" alt="jx10086"/><br /><sub><b>jx10086</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jx10086" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ajx10086" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/winnerTimer"><img src="https://avatars.githubusercontent.com/u/76024658?v=4?s=100" width="100px;" alt="winnerTimer"/><br /><sub><b>winnerTimer</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=winnerTimer" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AwinnerTimer" title="Bug reports">🐛</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/goo-kits"><img src="https://avatars.githubusercontent.com/u/13163673?v=4?s=100" width="100px;" alt="goo-kits"/><br /><sub><b>goo-kits</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=goo-kits" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Agoo-kits" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/brave4Time"><img src="https://avatars.githubusercontent.com/u/105094014?v=4?s=100" width="100px;" alt="brave4Time"/><br /><sub><b>brave4Time</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=brave4Time" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Abrave4Time" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/walkerlee-lab"><img src="https://avatars.githubusercontent.com/u/8426753?v=4?s=100" width="100px;" alt="WalkerLee"/><br /><sub><b>WalkerLee</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=walkerlee-lab" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Awalkerlee-lab" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fullofjoy"><img src="https://avatars.githubusercontent.com/u/30247571?v=4?s=100" width="100px;" alt="jianghang"/><br /><sub><b>jianghang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=fullofjoy" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Afullofjoy" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ChineseTony"><img src="https://avatars.githubusercontent.com/u/24618786?v=4?s=100" width="100px;" alt="ChineseTony"/><br /><sub><b>ChineseTony</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ChineseTony" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AChineseTony" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wyt199905"><img src="https://avatars.githubusercontent.com/u/85098809?v=4?s=100" width="100px;" alt="wyt199905"/><br /><sub><b>wyt199905</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wyt199905" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/weifuqing"><img src="https://avatars.githubusercontent.com/u/13931013?v=4?s=100" width="100px;" alt="卫傅庆"/><br /><sub><b>卫傅庆</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=weifuqing" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aweifuqing" title="Bug reports">🐛</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zklmcookle"><img src="https://avatars.githubusercontent.com/u/107192352?v=4?s=100" width="100px;" alt="zklmcookle"/><br /><sub><b>zklmcookle</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zklmcookle" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DevilX5"><img src="https://avatars.githubusercontent.com/u/13269921?v=4?s=100" width="100px;" alt="DevilX5"/><br /><sub><b>DevilX5</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=DevilX5" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=DevilX5" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/djzeng"><img src="https://avatars.githubusercontent.com/u/14074864?v=4?s=100" width="100px;" alt="tea"/><br /><sub><b>tea</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=djzeng" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yangshihui"><img src="https://avatars.githubusercontent.com/u/28550208?v=4?s=100" width="100px;" alt="yangshihui"/><br /><sub><b>yangshihui</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yangshihui" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ayangshihui" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DreamGirl524"><img src="https://avatars.githubusercontent.com/u/81132838?v=4?s=100" width="100px;" alt="DreamGirl524"/><br /><sub><b>DreamGirl524</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=DreamGirl524" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=DreamGirl524" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gzwlly"><img src="https://avatars.githubusercontent.com/u/83171907?v=4?s=100" width="100px;" alt="gzwlly"/><br /><sub><b>gzwlly</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gzwlly" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cuipiheqiuqiu"><img src="https://avatars.githubusercontent.com/u/76642201?v=4?s=100" width="100px;" alt="cuipiheqiuqiu"/><br /><sub><b>cuipiheqiuqiu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cuipiheqiuqiu" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=cuipiheqiuqiu" title="Tests">⚠️</a> <a href="#design-cuipiheqiuqiu" title="Design">🎨</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/oyiyou"><img src="https://avatars.githubusercontent.com/u/39228891?v=4?s=100" width="100px;" alt="lambert"/><br /><sub><b>lambert</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=oyiyou" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://mroldx.xyz/"><img src="https://avatars.githubusercontent.com/u/34847828?v=4?s=100" width="100px;" alt="mroldx"/><br /><sub><b>mroldx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mroldx" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/woshiniusange"><img src="https://avatars.githubusercontent.com/u/91513022?v=4?s=100" width="100px;" alt="woshiniusange"/><br /><sub><b>woshiniusange</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=woshiniusange" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://vampireachao.github.io/"><img src="https://avatars.githubusercontent.com/u/52746628?v=4?s=100" width="100px;" alt="VampireAchao"/><br /><sub><b>VampireAchao</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=VampireAchao" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Ceilzcx"><img src="https://avatars.githubusercontent.com/u/48920254?v=4?s=100" width="100px;" alt="zcx"/><br /><sub><b>zcx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Ceilzcx" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACeilzcx" title="Bug reports">🐛</a> <a href="#design-Ceilzcx" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/commits?author=Ceilzcx" title="Tests">⚠️</a> <a href="#blog-Ceilzcx" title="Blogposts">📝</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/CharlieXCL"><img src="https://avatars.githubusercontent.com/u/91540487?v=4?s=100" width="100px;" alt="CharlieXCL"/><br /><sub><b>CharlieXCL</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=CharlieXCL" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Privauto"><img src="https://avatars.githubusercontent.com/u/36581456?v=4?s=100" width="100px;" alt="Privauto"/><br /><sub><b>Privauto</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Privauto" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Privauto" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/emrys-he"><img src="https://avatars.githubusercontent.com/u/5848915?v=4?s=100" width="100px;" alt="emrys"/><br /><sub><b>emrys</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=emrys-he" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/SxLiuYu"><img src="https://avatars.githubusercontent.com/u/95198625?v=4?s=100" width="100px;" alt="SxLiuYu"/><br /><sub><b>SxLiuYu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3ASxLiuYu" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://allcontributors.org"><img src="https://avatars.githubusercontent.com/u/46410174?v=4?s=100" width="100px;" alt="All Contributors"/><br /><sub><b>All Contributors</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=all-contributors" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gxc-myh"><img src="https://avatars.githubusercontent.com/u/85919258?v=4?s=100" width="100px;" alt="铁甲小宝"/><br /><sub><b>铁甲小宝</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gxc-myh" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=gxc-myh" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/click33"><img src="https://avatars.githubusercontent.com/u/36243476?v=4?s=100" width="100px;" alt="click33"/><br /><sub><b>click33</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=click33" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://jpom.io/"><img src="https://avatars.githubusercontent.com/u/16408873?v=4?s=100" width="100px;" alt="蒋小小"/><br /><sub><b>蒋小小</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bwcx-jzy" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.zhihu.com/people/kevinbauer"><img src="https://avatars.githubusercontent.com/u/28581579?v=4?s=100" width="100px;" alt="Kevin Huang"/><br /><sub><b>Kevin Huang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=kevinhuangwl" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/TJxiaobao"><img src="https://avatars.githubusercontent.com/u/85919258?v=4?s=100" width="100px;" alt="铁甲小宝"/><br /><sub><b>铁甲小宝</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3ATJxiaobao" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=TJxiaobao" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=TJxiaobao" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=TJxiaobao" title="Tests">⚠️</a> <a href="#design-TJxiaobao" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Jack-123-power"><img src="https://avatars.githubusercontent.com/u/84333501?v=4?s=100" width="100px;" alt="Captain Jack"/><br /><sub><b>Captain Jack</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Jack-123-power" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/haibo-duan"><img src="https://avatars.githubusercontent.com/u/7974845?v=4?s=100" width="100px;" alt="haibo.duan"/><br /><sub><b>haibo.duan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=haibo-duan" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/commits?author=haibo-duan" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/assassinfym"><img src="https://avatars.githubusercontent.com/u/15188754?v=4?s=100" width="100px;" alt="assassin"/><br /><sub><b>assassin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3Aassassinfym" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=assassinfym" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/csyshu"><img src="https://avatars.githubusercontent.com/u/46591658?v=4?s=100" width="100px;" alt="Reverse wind"/><br /><sub><b>Reverse wind</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=csyshu" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/commits?author=csyshu" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/luxx-lq"><img src="https://avatars.githubusercontent.com/u/58515565?v=4?s=100" width="100px;" alt="luxx"/><br /><sub><b>luxx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=luxx-lq" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://bandism.net/"><img src="https://avatars.githubusercontent.com/u/22633385?v=4?s=100" width="100px;" alt="Ikko Ashimine"/><br /><sub><b>Ikko Ashimine</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=eltociear" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zenan08"><img src="https://avatars.githubusercontent.com/u/80514991?v=4?s=100" width="100px;" alt="leizenan"/><br /><sub><b>leizenan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zenan08" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BKing2020"><img src="https://avatars.githubusercontent.com/u/28869121?v=4?s=100" width="100px;" alt="BKing"/><br /><sub><b>BKing</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=BKing2020" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xingshuaiLi"><img src="https://avatars.githubusercontent.com/u/119487588?v=4?s=100" width="100px;" alt="xingshuaiLi"/><br /><sub><b>xingshuaiLi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xingshuaiLi" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wangke6666"><img src="https://avatars.githubusercontent.com/u/113656595?v=4?s=100" width="100px;" alt="wangke6666"/><br /><sub><b>wangke6666</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wangke6666" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/LWBobo"><img src="https://avatars.githubusercontent.com/u/50368698?v=4?s=100" width="100px;" alt="刺猬"/><br /><sub><b>刺猬</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3ALWBobo" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=LWBobo" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.zanglikun.com"><img src="https://avatars.githubusercontent.com/u/61591648?v=4?s=100" width="100px;" alt="Haste"/><br /><sub><b>Haste</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zanglikun" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/SuitSmile"><img src="https://avatars.githubusercontent.com/u/38679717?v=4?s=100" width="100px;" alt="zhongshi.yi"/><br /><sub><b>zhongshi.yi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=SuitSmile" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://www.smallq.cn"><img src="https://avatars.githubusercontent.com/u/39754275?v=4?s=100" width="100px;" alt="Qi Zhang"/><br /><sub><b>Qi Zhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zzzhangqi" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MrAndyMing"><img src="https://avatars.githubusercontent.com/u/49541483?v=4?s=100" width="100px;" alt="MrAndyMing"/><br /><sub><b>MrAndyMing</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MrAndyMing" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://idongliming.github.io/"><img src="https://avatars.githubusercontent.com/u/31564353?v=4?s=100" width="100px;" alt="idongliming"/><br /><sub><b>idongliming</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=idongliming" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://earthjasonlin.github.io"><img src="https://avatars.githubusercontent.com/u/83632110?v=4?s=100" width="100px;" alt="Zichao Lin"/><br /><sub><b>Zichao Lin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=earthjasonlin" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=earthjasonlin" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://blog.liudonghua.com"><img src="https://avatars.githubusercontent.com/u/2276718?v=4?s=100" width="100px;" alt="liudonghua"/><br /><sub><b>liudonghua</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=liudonghua123" title="Code">💻</a> <a href="#ideas-liudonghua123" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/orangeyts"><img src="https://avatars.githubusercontent.com/u/4250869?v=4?s=100" width="100px;" alt="Jerry"/><br /><sub><b>Jerry</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=orangeyts" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=orangeyts" title="Tests">⚠️</a> <a href="#ideas-orangeyts" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://dynamictp.cn"><img src="https://avatars.githubusercontent.com/u/13051908?v=4?s=100" width="100px;" alt="yanhom"/><br /><sub><b>yanhom</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yanhom1314" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://www.jianshu.com/u/a8f822c04f67"><img src="https://avatars.githubusercontent.com/u/18587688?v=4?s=100" width="100px;" alt="fsl"/><br /><sub><b>fsl</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=fengshunli" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xttttv"><img src="https://avatars.githubusercontent.com/u/116323904?v=4?s=100" width="100px;" alt="xttttv"/><br /><sub><b>xttttv</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xttttv" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/NavinKumarBarnwal"><img src="https://avatars.githubusercontent.com/u/44504274?v=4?s=100" width="100px;" alt="NavinKumarBarnwal"/><br /><sub><b>NavinKumarBarnwal</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=NavinKumarBarnwal" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/z641205699"><img src="https://avatars.githubusercontent.com/u/45276423?v=4?s=100" width="100px;" alt="Zakkary"/><br /><sub><b>Zakkary</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=z641205699" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/898349230"><img src="https://avatars.githubusercontent.com/u/21972532?v=4?s=100" width="100px;" alt="sunxinbo"/><br /><sub><b>sunxinbo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=898349230" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=898349230" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ldzbook"><img src="https://avatars.githubusercontent.com/u/13903790?v=4?s=100" width="100px;" alt="ldzbook"/><br /><sub><b>ldzbook</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ldzbook" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aldzbook" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/SurryChen"><img src="https://avatars.githubusercontent.com/u/91116490?v=4?s=100" width="100px;" alt="余与雨"/><br /><sub><b>余与雨</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=SurryChen" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=SurryChen" title="Tests">⚠️</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MysticalDream"><img src="https://avatars.githubusercontent.com/u/78899028?v=4?s=100" width="100px;" alt="MysticalDream"/><br /><sub><b>MysticalDream</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MysticalDream" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=MysticalDream" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zhouyoulin12"><img src="https://avatars.githubusercontent.com/u/17086633?v=4?s=100" width="100px;" alt="zhouyoulin12"/><br /><sub><b>zhouyoulin12</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhouyoulin12" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=zhouyoulin12" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jerjjj"><img src="https://avatars.githubusercontent.com/u/93431283?v=4?s=100" width="100px;" alt="jerjjj"/><br /><sub><b>jerjjj</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jerjjj" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://wjl110.xyz/"><img src="https://avatars.githubusercontent.com/u/53851034?v=4?s=100" width="100px;" alt="wjl110"/><br /><sub><b>wjl110</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wjl110" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ngyhd"><img src="https://avatars.githubusercontent.com/u/29095207?v=4?s=100" width="100px;" alt="Sean"/><br /><sub><b>Sean</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ngyhd" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Daydreamer-ia"><img src="https://avatars.githubusercontent.com/u/83362909?v=4?s=100" width="100px;" alt="chenyiqin"/><br /><sub><b>chenyiqin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Daydreamer-ia" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Daydreamer-ia" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hudongdong129"><img src="https://avatars.githubusercontent.com/u/34374227?v=4?s=100" width="100px;" alt="hudongdong129"/><br /><sub><b>hudongdong129</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hudongdong129" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=hudongdong129" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/commits?author=hudongdong129" title="Documentation">📖</a> <a href="#design-hudongdong129" title="Design">🎨</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/TherChenYang"><img src="https://avatars.githubusercontent.com/u/124348939?v=4?s=100" width="100px;" alt="TherChenYang"/><br /><sub><b>TherChenYang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=TherChenYang" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=TherChenYang" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/HattoriHenzo"><img src="https://avatars.githubusercontent.com/u/5141285?v=4?s=100" width="100px;" alt="HattoriHenzo"/><br /><sub><b>HattoriHenzo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=HattoriHenzo" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=HattoriHenzo" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ycilry"><img src="https://avatars.githubusercontent.com/u/63967101?v=4?s=100" width="100px;" alt="ycilry"/><br /><sub><b>ycilry</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ycilry" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aoshiguchen"><img src="https://avatars.githubusercontent.com/u/10580997?v=4?s=100" width="100px;" alt="aoshiguchen"/><br /><sub><b>aoshiguchen</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=aoshiguchen" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=aoshiguchen" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/caibenxiang"><img src="https://avatars.githubusercontent.com/u/4568241?v=4?s=100" width="100px;" alt="蔡本祥"/><br /><sub><b>蔡本祥</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=caibenxiang" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.fckeverything.cn:4000/"><img src="https://avatars.githubusercontent.com/u/13827124?v=4?s=100" width="100px;" alt="浮游"/><br /><sub><b>浮游</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lifefloating" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Grass-Life"><img src="https://avatars.githubusercontent.com/u/114381513?v=4?s=100" width="100px;" alt="Grass-Life"/><br /><sub><b>Grass-Life</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Grass-Life" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaohe428"><img src="https://avatars.githubusercontent.com/u/99130317?v=4?s=100" width="100px;" alt="xiaohe428"/><br /><sub><b>xiaohe428</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaohe428" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaohe428" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/baiban114"><img src="https://avatars.githubusercontent.com/u/59152619?v=4?s=100" width="100px;" alt="TableRow"/><br /><sub><b>TableRow</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=baiban114" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=baiban114" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ByteIDance"><img src="https://avatars.githubusercontent.com/u/100207562?v=4?s=100" width="100px;" alt="ByteIDance"/><br /><sub><b>ByteIDance</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ByteIDance" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mangel2002"><img src="https://avatars.githubusercontent.com/u/9348020?v=4?s=100" width="100px;" alt="Jangfe"/><br /><sub><b>Jangfe</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mangel2002" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zqr10159"><img src="https://avatars.githubusercontent.com/u/30048352?v=4?s=100" width="100px;" alt="zqr10159"/><br /><sub><b>zqr10159</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zqr10159" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=zqr10159" title="Code">💻</a> <a href="#blog-zqr10159" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Azqr10159" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=zqr10159" title="Tests">⚠️</a> <a href="#design-zqr10159" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vinci-897"><img src="https://avatars.githubusercontent.com/u/55838224?v=4?s=100" width="100px;" alt="vinci"/><br /><sub><b>vinci</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=vinci-897" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=vinci-897" title="Documentation">📖</a> <a href="#design-vinci-897" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/js110"><img src="https://avatars.githubusercontent.com/u/51191863?v=4?s=100" width="100px;" alt="js110"/><br /><sub><b>js110</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=js110" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JavaLionLi"><img src="https://avatars.githubusercontent.com/u/31852897?v=4?s=100" width="100px;" alt="CrazyLionLi"/><br /><sub><b>CrazyLionLi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=JavaLionLi" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.banmajio.com"><img src="https://avatars.githubusercontent.com/u/53471385?v=4?s=100" width="100px;" alt="banmajio"/><br /><sub><b>banmajio</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=banmajio" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://suder.fun"><img src="https://avatars.githubusercontent.com/u/69955165?v=4?s=100" width="100px;" alt="topsuder"/><br /><sub><b>topsuder</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=topsuder" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/richar2022"><img src="https://avatars.githubusercontent.com/u/129016397?v=4?s=100" width="100px;" alt="richar2022"/><br /><sub><b>richar2022</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=richar2022" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fcb-xiaobo"><img src="https://avatars.githubusercontent.com/u/60566194?v=4?s=100" width="100px;" alt="fcb-xiaobo"/><br /><sub><b>fcb-xiaobo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=fcb-xiaobo" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wenkyzhang"><img src="https://avatars.githubusercontent.com/u/13983669?v=4?s=100" width="100px;" alt="wenkyzhang"/><br /><sub><b>wenkyzhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wenkyzhang" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ZangJuxy"><img src="https://avatars.githubusercontent.com/u/71380295?v=4?s=100" width="100px;" alt="ZangJuxy"/><br /><sub><b>ZangJuxy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ZangJuxy" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/l646505418"><img src="https://avatars.githubusercontent.com/u/50475131?v=4?s=100" width="100px;" alt="l646505418"/><br /><sub><b>l646505418</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=l646505418" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Al646505418" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.carpewang.com"><img src="https://avatars.githubusercontent.com/u/78642589?v=4?s=100" width="100px;" alt="Carpe-Wang"/><br /><sub><b>Carpe-Wang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Carpe-Wang" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACarpe-Wang" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/moshu023"><img src="https://avatars.githubusercontent.com/u/48593205?v=4?s=100" width="100px;" alt="莫枢"/><br /><sub><b>莫枢</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=moshu023" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/huangcanda"><img src="https://avatars.githubusercontent.com/u/4470566?v=4?s=100" width="100px;" alt="huangcanda"/><br /><sub><b>huangcanda</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=huangcanda" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.zrkizzy.com"><img src="https://avatars.githubusercontent.com/u/85340613?v=4?s=100" width="100px;" alt="世纪末的架构师"/><br /><sub><b>世纪末的架构师</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Architect-Java" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ShuningWan"><img src="https://avatars.githubusercontent.com/u/31086770?v=4?s=100" width="100px;" alt="ShuningWan"/><br /><sub><b>ShuningWan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ShuningWan" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MrYZhou"><img src="https://avatars.githubusercontent.com/u/44339602?v=4?s=100" width="100px;" alt="MrYZhou"/><br /><sub><b>MrYZhou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MrYZhou" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/suncqujsj"><img src="https://avatars.githubusercontent.com/u/8012932?v=4?s=100" width="100px;" alt="suncqujsj"/><br /><sub><b>suncqujsj</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=suncqujsj" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sunqinbo"><img src="https://avatars.githubusercontent.com/u/1428540?v=4?s=100" width="100px;" alt="sunqinbo"/><br /><sub><b>sunqinbo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sunqinbo" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/haoww"><img src="https://avatars.githubusercontent.com/u/32739294?v=4?s=100" width="100px;" alt="haoww"/><br /><sub><b>haoww</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=haoww" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/i-mayuan"><img src="https://avatars.githubusercontent.com/u/101498477?v=4?s=100" width="100px;" alt="i-mayuan"/><br /><sub><b>i-mayuan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=i-mayuan" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fengruge"><img src="https://avatars.githubusercontent.com/u/85803831?v=4?s=100" width="100px;" alt="fengruge"/><br /><sub><b>fengruge</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=fengruge" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aystzh"><img src="https://avatars.githubusercontent.com/u/38125392?v=4?s=100" width="100px;" alt="zhanghuan"/><br /><sub><b>zhanghuan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=aystzh" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/shenyumin"><img src="https://avatars.githubusercontent.com/u/8438506?v=4?s=100" width="100px;" alt="shenymin"/><br /><sub><b>shenymin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=shenyumin" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dhruva1995"><img src="https://avatars.githubusercontent.com/u/12976351?v=4?s=100" width="100px;" alt="Dhruva Chandra"/><br /><sub><b>Dhruva Chandra</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dhruva1995" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/weiwang988"><img src="https://avatars.githubusercontent.com/u/58241726?v=4?s=100" width="100px;" alt="miss_z"/><br /><sub><b>miss_z</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=weiwang988" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wyt990"><img src="https://avatars.githubusercontent.com/u/86013697?v=4?s=100" width="100px;" alt="wyt990"/><br /><sub><b>wyt990</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wyt990" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/licocon"><img src="https://avatars.githubusercontent.com/u/36863277?v=4?s=100" width="100px;" alt="licocon"/><br /><sub><b>licocon</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=licocon" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/2406450951"><img src="https://avatars.githubusercontent.com/u/48074721?v=4?s=100" width="100px;" alt="Mi Na"/><br /><sub><b>Mi Na</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=2406450951" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Kylin-Guo"><img src="https://avatars.githubusercontent.com/u/131239856?v=4?s=100" width="100px;" alt="Kylin-Guo"/><br /><sub><b>Kylin-Guo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Kylin-Guo" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1797899698"><img src="https://avatars.githubusercontent.com/u/40411650?v=4?s=100" width="100px;" alt="Mr灬Dong先生"/><br /><sub><b>Mr灬Dong先生</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1797899698" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://neilblaze.live"><img src="https://avatars.githubusercontent.com/u/48355572?v=4?s=100" width="100px;" alt="Pratyay Banerjee"/><br /><sub><b>Pratyay Banerjee</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Neilblaze" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Neilblaze" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yujianzhong520"><img src="https://avatars.githubusercontent.com/u/63705063?v=4?s=100" width="100px;" alt="yujianzhong520"/><br /><sub><b>yujianzhong520</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yujianzhong520" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://sppan24.github.io/"><img src="https://avatars.githubusercontent.com/u/15795173?v=4?s=100" width="100px;" alt="SPPan"/><br /><sub><b>SPPan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sppan24" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1130600015"><img src="https://avatars.githubusercontent.com/u/67859663?v=4?s=100" width="100px;" alt="ZhangJiashu"/><br /><sub><b>ZhangJiashu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1130600015" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/QZmp236478"><img src="https://avatars.githubusercontent.com/u/56623162?v=4?s=100" width="100px;" alt="impress"/><br /><sub><b>impress</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=QZmp236478" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jx3775250"><img src="https://avatars.githubusercontent.com/u/40455946?v=4?s=100" width="100px;" alt="凌晨一点半"/><br /><sub><b>凌晨一点半</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jx3775250" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/eeshaanSA"><img src="https://avatars.githubusercontent.com/u/100678386?v=4?s=100" width="100px;" alt="Eeshaan Sawant"/><br /><sub><b>Eeshaan Sawant</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=eeshaanSA" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nandofromthebando"><img src="https://avatars.githubusercontent.com/u/87321214?v=4?s=100" width="100px;" alt="nandofromthebando"/><br /><sub><b>nandofromthebando</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=nandofromthebando" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/caiboking"><img src="https://avatars.githubusercontent.com/u/6509883?v=4?s=100" width="100px;" alt="caiboking"/><br /><sub><b>caiboking</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=caiboking" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/baixing99"><img src="https://avatars.githubusercontent.com/u/73473087?v=4?s=100" width="100px;" alt="baixing99"/><br /><sub><b>baixing99</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=baixing99" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ifrenzyc"><img src="https://avatars.githubusercontent.com/u/543927?v=4?s=100" width="100px;" alt="Yang Chuang"/><br /><sub><b>Yang Chuang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ifrenzyc" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wlin20"><img src="https://avatars.githubusercontent.com/u/20657577?v=4?s=100" width="100px;" alt="wlin20"/><br /><sub><b>wlin20</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wlin20" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/guojing1983"><img src="https://avatars.githubusercontent.com/u/60596094?v=4?s=100" width="100px;" alt="guojing1983"/><br /><sub><b>guojing1983</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=guojing1983" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/itxxq"><img src="https://avatars.githubusercontent.com/u/46962357?v=4?s=100" width="100px;" alt="moxi"/><br /><sub><b>moxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=itxxq" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/qq471754603"><img src="https://avatars.githubusercontent.com/u/23146592?v=4?s=100" width="100px;" alt="qq471754603"/><br /><sub><b>qq471754603</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=qq471754603" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/san346596324"><img src="https://avatars.githubusercontent.com/u/30828520?v=4?s=100" width="100px;" alt="渭雨"/><br /><sub><b>渭雨</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=san346596324" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/luoxuanzao"><img src="https://avatars.githubusercontent.com/u/44692579?v=4?s=100" width="100px;" alt="liuxuezhuo"/><br /><sub><b>liuxuezhuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=luoxuanzao" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lisongning"><img src="https://avatars.githubusercontent.com/u/93140178?v=4?s=100" width="100px;" alt="lisongning"/><br /><sub><b>lisongning</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lisongning" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/YutingNie"><img src="https://avatars.githubusercontent.com/u/104416402?v=4?s=100" width="100px;" alt="YutingNie"/><br /><sub><b>YutingNie</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=YutingNie" title="Documentation">📖</a> <a href="#design-YutingNie" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mikezzb"><img src="https://avatars.githubusercontent.com/u/23418428?v=4?s=100" width="100px;" alt="Mike Zhou"/><br /><sub><b>Mike Zhou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=mikezzb" title="Documentation">📖</a> <a href="#design-mikezzb" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/a-little-fool"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="小笨蛋"/><br /><sub><b>小笨蛋</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=a-little-fool" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=a-little-fool" title="Documentation">📖</a> <a href="#blog-a-little-fool" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aa-little-fool" title="Bug reports">🐛</a> <a href="#design-a-little-fool" title="Design">🎨</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/littlezhongzer"><img src="https://avatars.githubusercontent.com/u/33685289?v=4?s=100" width="100px;" alt="littlezhongzer"/><br /><sub><b>littlezhongzer</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=littlezhongzer" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ChenXiangxxxxx"><img src="https://avatars.githubusercontent.com/u/90089594?v=4?s=100" width="100px;" alt="ChenXiangxxxxx"/><br /><sub><b>ChenXiangxxxxx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ChenXiangxxxxx" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Mr-zhou315"><img src="https://avatars.githubusercontent.com/u/10276100?v=4?s=100" width="100px;" alt="Mr.zhou"/><br /><sub><b>Mr.zhou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Mr-zhou315" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/XimfengYao"><img src="https://avatars.githubusercontent.com/u/17541537?v=4?s=100" width="100px;" alt="姚贤丰"/><br /><sub><b>姚贤丰</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=XimfengYao" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/LINGLUOJUN"><img src="https://avatars.githubusercontent.com/u/16778977?v=4?s=100" width="100px;" alt="lingluojun"/><br /><sub><b>lingluojun</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LINGLUOJUN" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.luelueking.com"><img src="https://avatars.githubusercontent.com/u/93204032?v=4?s=100" width="100px;" alt="1ue"/><br /><sub><b>1ue</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=luelueking" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.jimmyqiao.top"><img src="https://avatars.githubusercontent.com/u/67301054?v=4?s=100" width="100px;" alt="qyaaaa"/><br /><sub><b>qyaaaa</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=qyaaaa" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aqyaaaa" title="Bug reports">🐛</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://novohit.top"><img src="https://avatars.githubusercontent.com/u/101090395?v=4?s=100" width="100px;" alt="novohit"/><br /><sub><b>novohit</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=novohit" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rbsrcy"><img src="https://avatars.githubusercontent.com/u/4798540?v=4?s=100" width="100px;" alt="zhuoshangyi"/><br /><sub><b>zhuoshangyi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=rbsrcy" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ruanliang-hualun"><img src="https://avatars.githubusercontent.com/u/65543716?v=4?s=100" width="100px;" alt="ruanliang"/><br /><sub><b>ruanliang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ruanliang-hualun" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=ruanliang-hualun" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Eden4701"><img src="https://avatars.githubusercontent.com/u/68422437?v=4?s=100" width="100px;" alt="Eden4701"/><br /><sub><b>Eden4701</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Eden4701" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Eden4701" title="Documentation">📖</a> <a href="#design-Eden4701" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/XiaTian688"><img src="https://avatars.githubusercontent.com/u/111830921?v=4?s=100" width="100px;" alt="XiaTian688"/><br /><sub><b>XiaTian688</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=XiaTian688" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/liyin"><img src="https://avatars.githubusercontent.com/u/863169?v=4?s=100" width="100px;" alt="liyinjiang"/><br /><sub><b>liyinjiang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=liyin" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jiashu1024"><img src="https://avatars.githubusercontent.com/u/67859663?v=4?s=100" width="100px;" alt="ZhangJiashu"/><br /><sub><b>ZhangJiashu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jiashu1024" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1036664317"><img src="https://avatars.githubusercontent.com/u/7696697?v=4?s=100" width="100px;" alt="moghn"/><br /><sub><b>moghn</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1036664317" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaoguolong"><img src="https://avatars.githubusercontent.com/u/33684988?v=4?s=100" width="100px;" alt="xiaoguolong"/><br /><sub><b>xiaoguolong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaoguolong" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Clownsw"><img src="https://avatars.githubusercontent.com/u/28394742?v=4?s=100" width="100px;" alt="Smliexx"/><br /><sub><b>Smliexx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Clownsw" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AClownsw" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Calvin979"><img src="https://avatars.githubusercontent.com/u/131688897?v=4?s=100" width="100px;" alt="Naruse"/><br /><sub><b>Naruse</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Code">💻</a> <a href="#design-Calvin979" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACalvin979" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bbelide2"><img src="https://avatars.githubusercontent.com/u/26840796?v=4?s=100" width="100px;" alt="Bala Sukesh"/><br /><sub><b>Bala Sukesh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bbelide2" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jinyaoMa"><img src="https://avatars.githubusercontent.com/u/25066570?v=4?s=100" width="100px;" alt="Jinyao Ma"/><br /><sub><b>Jinyao Ma</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jinyaoMa" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://linuxsuren.github.io/open-source-best-practice/"><img src="https://avatars.githubusercontent.com/u/1450685?v=4?s=100" width="100px;" alt="Rick"/><br /><sub><b>Rick</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Tests">⚠️</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ZY945"><img src="https://avatars.githubusercontent.com/u/74083801?v=4?s=100" width="100px;" alt="东风"/><br /><sub><b>东风</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ZY945" title="Code">💻</a> <a href="#design-ZY945" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/commits?author=ZY945" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AZY945" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/prolevel1"><img src="https://avatars.githubusercontent.com/u/51995525?v=4?s=100" width="100px;" alt="sonam singh"/><br /><sub><b>sonam singh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=prolevel1" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ZhangZixuan1994"><img src="https://avatars.githubusercontent.com/u/20011653?v=4?s=100" width="100px;" alt="ZhangZixuan1994"/><br /><sub><b>ZhangZixuan1994</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ZhangZixuan1994" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hurenjie1"><img src="https://avatars.githubusercontent.com/u/40120355?v=4?s=100" width="100px;" alt="SHIG"/><br /><sub><b>SHIG</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hurenjie1" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://tslj1024.github.io/"><img src="https://avatars.githubusercontent.com/u/155222677?v=4?s=100" width="100px;" alt="泰上老菌"/><br /><sub><b>泰上老菌</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tslj1024" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ldysdu"><img src="https://avatars.githubusercontent.com/u/15815338?v=4?s=100" width="100px;" alt="ldysdu"/><br /><sub><b>ldysdu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ldysdu" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/GEM0816g"><img src="https://avatars.githubusercontent.com/u/85116017?v=4?s=100" width="100px;" alt="梁同学"/><br /><sub><b>梁同学</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=GEM0816g" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/avvCode"><img src="https://avatars.githubusercontent.com/u/113538532?v=4?s=100" width="100px;" alt="avv"/><br /><sub><b>avv</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=avvCode" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yqxxgh"><img src="https://avatars.githubusercontent.com/u/42080876?v=4?s=100" width="100px;" alt="yqxxgh"/><br /><sub><b>yqxxgh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yqxxgh" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=yqxxgh" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ayqxxgh" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/CharlieShi46"><img src="https://avatars.githubusercontent.com/u/149798885?v=4?s=100" width="100px;" alt="CharlieShi46"/><br /><sub><b>CharlieShi46</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=CharlieShi46" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Nctllnty"><img src="https://avatars.githubusercontent.com/u/33241818?v=4?s=100" width="100px;" alt="Nctllnty"/><br /><sub><b>Nctllnty</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Nctllnty" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Wang-Yonghao"><img src="https://avatars.githubusercontent.com/u/48146606?v=4?s=100" width="100px;" alt="Wang-Yonghao"/><br /><sub><b>Wang-Yonghao</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Wang-Yonghao" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.yuque.com/dudiao/yy"><img src="https://avatars.githubusercontent.com/u/38355949?v=4?s=100" width="100px;" alt="读钓"/><br /><sub><b>读钓</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dudiao" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/starmilkxin"><img src="https://avatars.githubusercontent.com/u/55646681?v=4?s=100" width="100px;" alt="Xin"/><br /><sub><b>Xin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=starmilkxin" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Astarmilkxin" title="Bug reports">🐛</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/handy-git"><img src="https://avatars.githubusercontent.com/u/32837980?v=4?s=100" width="100px;" alt="handy"/><br /><sub><b>handy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=handy-git" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/LiuTianyou"><img src="https://avatars.githubusercontent.com/u/30208283?v=4?s=100" width="100px;" alt="LiuTianyou"/><br /><sub><b>LiuTianyou</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LiuTianyou" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=LiuTianyou" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ALiuTianyou" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=LiuTianyou" title="Tests">⚠️</a> <a href="#blog-LiuTianyou" title="Blogposts">📝</a> <a href="#design-LiuTianyou" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/WinterKi1ler"><img src="https://avatars.githubusercontent.com/u/160592092?v=4?s=100" width="100px;" alt="WinterKi1ler"/><br /><sub><b>WinterKi1ler</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=WinterKi1ler" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://sharehoo.cn/"><img src="https://avatars.githubusercontent.com/u/45377370?v=4?s=100" width="100px;" alt="miki"/><br /><sub><b>miki</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=miki-hmt" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://codeflex.substack.com/"><img src="https://avatars.githubusercontent.com/u/85513042?v=4?s=100" width="100px;" alt="Keshav Carpenter"/><br /><sub><b>Keshav Carpenter</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=alpha951" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=alpha951" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/makechoicenow"><img src="https://avatars.githubusercontent.com/u/9911918?v=4?s=100" width="100px;" alt="makechoicenow"/><br /><sub><b>makechoicenow</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=makechoicenow" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gjjjj0101"><img src="https://avatars.githubusercontent.com/u/71874373?v=4?s=100" width="100px;" alt="Gao Jian"/><br /><sub><b>Gao Jian</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gjjjj0101" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/commits?author=gjjjj0101" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=gjjjj0101" title="Documentation">📖</a> <a href="#design-gjjjj0101" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Agjjjj0101" title="Bug reports">🐛</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://jangto.tistory.com/"><img src="https://avatars.githubusercontent.com/u/37864182?v=4?s=100" width="100px;" alt="Hyeon Sung"/><br /><sub><b>Hyeon Sung</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dukbong" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=dukbong" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://crossoverjie.top/"><img src="https://avatars.githubusercontent.com/u/15684156?v=4?s=100" width="100px;" alt="crossoverJie"/><br /><sub><b>crossoverJie</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=crossoverJie" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=crossoverJie" title="Documentation">📖</a> <a href="#blog-crossoverJie" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/commits?author=crossoverJie" title="Tests">⚠️</a> <a href="#design-crossoverJie" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PeixyJ"><img src="https://avatars.githubusercontent.com/u/45998593?v=4?s=100" width="100px;" alt="PeixyJ"/><br /><sub><b>PeixyJ</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=PeixyJ" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Hi-Mr-Wind"><img src="https://avatars.githubusercontent.com/u/85803831?v=4?s=100" width="100px;" alt="风如歌"/><br /><sub><b>风如歌</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Hi-Mr-Wind" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MananPoojara"><img src="https://avatars.githubusercontent.com/u/104253184?v=4?s=100" width="100px;" alt="Manan Pujara"/><br /><sub><b>Manan Pujara</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MananPoojara" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xuziyang"><img src="https://avatars.githubusercontent.com/u/8465969?v=4?s=100" width="100px;" alt="xuziyang"/><br /><sub><b>xuziyang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xuziyang" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xuziyang" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Axuziyang" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lwqzz"><img src="https://avatars.githubusercontent.com/u/62584513?v=4?s=100" width="100px;" alt="lwqzz"/><br /><sub><b>lwqzz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lwqzz" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/YxYL6125"><img src="https://avatars.githubusercontent.com/u/91076160?v=4?s=100" width="100px;" alt="YxYL"/><br /><sub><b>YxYL</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=YxYL6125" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tomorrowshipyltm"><img src="https://avatars.githubusercontent.com/u/61336903?v=4?s=100" width="100px;" alt="tomorrowshipyltm"/><br /><sub><b>tomorrowshipyltm</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tomorrowshipyltm" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/15613060203"><img src="https://avatars.githubusercontent.com/u/41351615?v=4?s=100" width="100px;" alt="栗磊"/><br /><sub><b>栗磊</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=15613060203" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Alanxtl"><img src="https://avatars.githubusercontent.com/u/25652981?v=4?s=100" width="100px;" alt="Alan"/><br /><sub><b>Alan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Alanxtl" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.hadoop.wiki/"><img src="https://avatars.githubusercontent.com/u/29418975?v=4?s=100" width="100px;" alt="Jast"/><br /><sub><b>Jast</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhangshenghang" title="Code">💻</a> <a href="#ideas-zhangshenghang" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/apache/hertzbeat/commits?author=zhangshenghang" title="Documentation">📖</a> <a href="#blog-zhangshenghang" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Azhangshenghang" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=zhangshenghang" title="Tests">⚠️</a> <a href="#design-zhangshenghang" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zuobiao-zhou"><img src="https://avatars.githubusercontent.com/u/61108539?v=4?s=100" width="100px;" alt="Zhang Yuxuan"/><br /><sub><b>Zhang Yuxuan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zuobiao-zhou" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=zuobiao-zhou" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Azuobiao-zhou" title="Bug reports">🐛</a> <a href="#blog-zuobiao-zhou" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/commits?author=zuobiao-zhou" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Pzz-2021"><img src="https://avatars.githubusercontent.com/u/118056735?v=4?s=100" width="100px;" alt="P.P."/><br /><sub><b>P.P.</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Pzz-2021" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/LLP2333"><img src="https://avatars.githubusercontent.com/u/61670545?v=4?s=100" width="100px;" alt="llp2333"/><br /><sub><b>llp2333</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LLP2333" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/HeartLinked"><img src="https://avatars.githubusercontent.com/u/78212101?v=4?s=100" width="100px;" alt="feiyang li"/><br /><sub><b>feiyang li</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=HeartLinked" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Aias00"><img src="https://avatars.githubusercontent.com/u/25810623?v=4?s=100" width="100px;" alt="aias00"/><br /><sub><b>aias00</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Aias00" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Aias00" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AAias00" title="Bug reports">🐛</a> <a href="#ideas-Aias00" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/apache/hertzbeat/commits?author=Aias00" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/westboy"><img src="https://avatars.githubusercontent.com/u/6385565?v=4?s=100" width="100px;" alt="Jin"/><br /><sub><b>Jin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=westboy" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://blog.csdn.net/qq_52397471"><img src="https://avatars.githubusercontent.com/u/77964041?v=4?s=100" width="100px;" alt="YuLuo"/><br /><sub><b>YuLuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yuluo-yx" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ayuluo-yx" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=yuluo-yx" title="Tests">⚠️</a> <a href="#blog-yuluo-yx" title="Blogposts">📝</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Yanshuming1"><img src="https://avatars.githubusercontent.com/u/118667222?v=4?s=100" width="100px;" alt="linDong"/><br /><sub><b>linDong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Yanshuming1" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Yanshuming1" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AYanshuming1" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lwjxy"><img src="https://avatars.githubusercontent.com/u/52726400?v=4?s=100" width="100px;" alt="lwjxy"/><br /><sub><b>lwjxy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lwjxy" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://thespica.github.io/"><img src="https://avatars.githubusercontent.com/u/119573640?v=4?s=100" width="100px;" alt="John"/><br /><sub><b>John</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Thespica" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Thespica" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/boatrainlsz"><img src="https://avatars.githubusercontent.com/u/18243785?v=4?s=100" width="100px;" alt="boatrainlsz"/><br /><sub><b>boatrainlsz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=boatrainlsz" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.yitianyigexiangfa.com/"><img src="https://avatars.githubusercontent.com/u/3973419?v=4?s=100" width="100px;" alt="Bill Lau"/><br /><sub><b>Bill Lau</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=JavaProgrammerLB" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lw-yang"><img src="https://avatars.githubusercontent.com/u/23456873?v=4?s=100" width="100px;" alt="lwyang"/><br /><sub><b>lwyang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lw-yang" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xfl12345"><img src="https://avatars.githubusercontent.com/u/17960863?v=4?s=100" width="100px;" alt="xfl12345"/><br /><sub><b>xfl12345</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xfl12345" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yykaue"><img src="https://avatars.githubusercontent.com/u/22905143?v=4?s=100" width="100px;" alt="Limbo"/><br /><sub><b>Limbo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yykaue" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/irenhongyan"><img src="https://avatars.githubusercontent.com/u/53438321?v=4?s=100" width="100px;" alt="哈哈哈哈哈哈哈哈哈"/><br /><sub><b>哈哈哈哈哈哈哈哈哈</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=irenhongyan" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ileonli"><img src="https://avatars.githubusercontent.com/u/45332412?v=4?s=100" width="100px;" alt="Leon Li"/><br /><sub><b>Leon Li</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ileonli" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://fnil.net/"><img src="https://avatars.githubusercontent.com/u/14142?v=4?s=100" width="100px;" alt="dennis zhuang"/><br /><sub><b>dennis zhuang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=killme2008" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kerwin612"><img src="https://avatars.githubusercontent.com/u/3371163?v=4?s=100" width="100px;" alt="Kerwin Bryant"/><br /><sub><b>Kerwin Bryant</b></sub></a><br /><a href="#design-kerwin612" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/commits?author=kerwin612" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=kerwin612" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Akerwin612" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ShineDevelopment"><img src="https://avatars.githubusercontent.com/u/59306780?v=4?s=100" width="100px;" alt="daixianglong"/><br /><sub><b>daixianglong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ShineDevelopment" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mchgood"><img src="https://avatars.githubusercontent.com/u/38482005?v=4?s=100" width="100px;" alt="mchgood"/><br /><sub><b>mchgood</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mchgood" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pwallk"><img src="https://avatars.githubusercontent.com/u/69385076?v=4?s=100" width="100px;" alt="kangli"/><br /><sub><b>kangli</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pwallk" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=pwallk" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Apwallk" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cdphantom"><img src="https://avatars.githubusercontent.com/u/12674795?v=4?s=100" width="100px;" alt="cdphantom"/><br /><sub><b>cdphantom</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cdphantom" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/asd108908382"><img src="https://avatars.githubusercontent.com/u/77717999?v=4?s=100" width="100px;" alt="jiawei.guo"/><br /><sub><b>jiawei.guo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=asd108908382" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/QBH-insist"><img src="https://avatars.githubusercontent.com/u/39401478?v=4?s=100" width="100px;" alt="QBH-insist"/><br /><sub><b>QBH-insist</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=QBH-insist" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jiangsh-ui"><img src="https://avatars.githubusercontent.com/u/86990361?v=4?s=100" width="100px;" alt="jiangsh"/><br /><sub><b>jiangsh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jiangsh-ui" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/keaifafafa"><img src="https://avatars.githubusercontent.com/u/83876361?v=4?s=100" width="100px;" alt="Keaifa"/><br /><sub><b>Keaifa</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=keaifafafa" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Akeaifafafa" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/loong95"><img src="https://avatars.githubusercontent.com/u/16333958?v=4?s=100" width="100px;" alt="Loong"/><br /><sub><b>Loong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=loong95" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ceekay47"><img src="https://avatars.githubusercontent.com/u/104664857?v=4?s=100" width="100px;" alt="Chandrakant Vankayalapati"/><br /><sub><b>Chandrakant Vankayalapati</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ceekay47" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MRgenial"><img src="https://avatars.githubusercontent.com/u/49973336?v=4?s=100" width="100px;" alt="b_mountain"/><br /><sub><b>b_mountain</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MRgenial" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/TemirlanBasitov"><img src="https://avatars.githubusercontent.com/u/57500808?v=4?s=100" width="100px;" alt="TemirlanBasitov"/><br /><sub><b>TemirlanBasitov</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=TemirlanBasitov" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wyfvsfy"><img src="https://avatars.githubusercontent.com/u/11973517?v=4?s=100" width="100px;" alt="wyfvsfy"/><br /><sub><b>wyfvsfy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wyfvsfy" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sherry-peng2333"><img src="https://avatars.githubusercontent.com/u/70619577?v=4?s=100" width="100px;" alt="sherry-peng2333"/><br /><sub><b>sherry-peng2333</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sherry-peng2333" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lixiaobaivv"><img src="https://avatars.githubusercontent.com/u/39290771?v=4?s=100" width="100px;" alt="Yzzz"/><br /><sub><b>Yzzz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lixiaobaivv" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.bckf.cn/"><img src="https://avatars.githubusercontent.com/u/13309008?v=4?s=100" width="100px;" alt="puruidong"/><br /><sub><b>puruidong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pruidong" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/shinestare"><img src="https://avatars.githubusercontent.com/u/13570619?v=4?s=100" width="100px;" alt="shinestare"/><br /><sub><b>shinestare</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=shinestare" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/po-168"><img src="https://avatars.githubusercontent.com/u/185745593?v=4?s=100" width="100px;" alt="po-168"/><br /><sub><b>po-168</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=po-168" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/All-The-Best-for"><img src="https://avatars.githubusercontent.com/u/76414672?v=4?s=100" width="100px;" alt="wbs99"/><br /><sub><b>wbs99</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=All-The-Best-for" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/starryCoder"><img src="https://avatars.githubusercontent.com/u/46510059?v=4?s=100" width="100px;" alt="starryCoder"/><br /><sub><b>starryCoder</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=starryCoder" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hasimmollah"><img src="https://avatars.githubusercontent.com/u/32538599?v=4?s=100" width="100px;" alt="hasimmollah"/><br /><sub><b>hasimmollah</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hasimmollah" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ayu-v0"><img src="https://avatars.githubusercontent.com/u/127600988?v=4?s=100" width="100px;" alt="Ayu"/><br /><sub><b>Ayu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ayu-v0" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Rancho-7"><img src="https://avatars.githubusercontent.com/u/59016860?v=4?s=100" width="100px;" alt="Nick Guo"/><br /><sub><b>Nick Guo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Rancho-7" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Rancho-7" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ARancho-7" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/doveLin0818"><img src="https://avatars.githubusercontent.com/u/190927907?v=4?s=100" width="100px;" alt="doveLin"/><br /><sub><b>doveLin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=doveLin0818" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://zzrl.cc/"><img src="https://avatars.githubusercontent.com/u/91836599?v=4?s=100" width="100px;" alt="yunfan24"/><br /><sub><b>yunfan24</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yunfan24" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=yunfan24" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Ayunfan24" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=yunfan24" title="Tests">⚠️</a> <a href="#blog-yunfan24" title="Blogposts">📝</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lctking"><img src="https://avatars.githubusercontent.com/u/168249998?v=4?s=100" width="100px;" alt="nullwli"/><br /><sub><b>nullwli</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lctking" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://simonsigre.com/"><img src="https://avatars.githubusercontent.com/u/14932913?v=4?s=100" width="100px;" alt="Simon Sigré"/><br /><sub><b>Simon Sigré</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=simonsigre" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=simonsigre" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.ponfee.cn/"><img src="https://avatars.githubusercontent.com/u/46117331?v=4?s=100" width="100px;" alt="ponfee"/><br /><sub><b>ponfee</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ponfee" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Vedant7789"><img src="https://avatars.githubusercontent.com/u/147625492?v=4?s=100" width="100px;" alt="Vedant7789"/><br /><sub><b>Vedant7789</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Vedant7789" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Craaaaazy77"><img src="https://avatars.githubusercontent.com/u/23025522?v=4?s=100" width="100px;" alt="Craaaaazy77"/><br /><sub><b>Craaaaazy77</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Craaaaazy77" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Suvrat1629"><img src="https://avatars.githubusercontent.com/u/140749446?v=4?s=100" width="100px;" alt="Suvrat1629"/><br /><sub><b>Suvrat1629</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Suvrat1629" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://ghyghoo8.github.io/"><img src="https://avatars.githubusercontent.com/u/363129?v=4?s=100" width="100px;" alt="ghy"/><br /><sub><b>ghy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ghyghoo8" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/helei1030"><img src="https://avatars.githubusercontent.com/u/11839080?v=4?s=100" width="100px;" alt="helei1030"/><br /><sub><b>helei1030</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=helei1030" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://medium.com/@pjfanning"><img src="https://avatars.githubusercontent.com/u/11783444?v=4?s=100" width="100px;" alt="PJ Fanning"/><br /><sub><b>PJ Fanning</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pjfanning" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Apjfanning" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=pjfanning" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MonsterChenzhuo"><img src="https://avatars.githubusercontent.com/u/60029759?v=4?s=100" width="100px;" alt="monster"/><br /><sub><b>monster</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MonsterChenzhuo" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MasamiYui"><img src="https://avatars.githubusercontent.com/u/22274133?v=4?s=100" width="100px;" alt="Sherlock Yin"/><br /><sub><b>Sherlock Yin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MasamiYui" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=MasamiYui" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AMasamiYui" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wanhao23"><img src="https://avatars.githubusercontent.com/u/29560961?v=4?s=100" width="100px;" alt="wanhao"/><br /><sub><b>wanhao</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wanhao23" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=wanhao23" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jonasHanhan"><img src="https://avatars.githubusercontent.com/u/130035609?v=4?s=100" width="100px;" alt="jonasHanhan"/><br /><sub><b>jonasHanhan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jonasHanhan" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/NikhilMurugesan"><img src="https://avatars.githubusercontent.com/u/49281792?v=4?s=100" width="100px;" alt="NikhilMurugesan"/><br /><sub><b>NikhilMurugesan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=NikhilMurugesan" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/myangle1120"><img src="https://avatars.githubusercontent.com/u/19237013?v=4?s=100" width="100px;" alt="myangle1120"/><br /><sub><b>myangle1120</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=myangle1120" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yasminvo"><img src="https://avatars.githubusercontent.com/u/107528848?v=4?s=100" width="100px;" alt="yasminvo"/><br /><sub><b>yasminvo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yasminvo" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/notbugggg"><img src="https://avatars.githubusercontent.com/u/147966331?v=4?s=100" width="100px;" alt="不关银渐层的事哦"/><br /><sub><b>不关银渐层的事哦</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=notbugggg" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Anotbugggg" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yyahang"><img src="https://avatars.githubusercontent.com/u/90464876?v=4?s=100" width="100px;" alt="yyahang"/><br /><sub><b>yyahang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yyahang" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JuJinPark"><img src="https://avatars.githubusercontent.com/u/44892459?v=4?s=100" width="100px;" alt="jujin"/><br /><sub><b>jujin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=JuJinPark" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=JuJinPark" title="Documentation">📖</a> <a href="#ideas-JuJinPark" title="Ideas, Planning, & Feedback">🤔</a> <a href="#blog-JuJinPark" title="Blogposts">📝</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/LL-LIN"><img src="https://avatars.githubusercontent.com/u/43002118?v=4?s=100" width="100px;" alt="LL-LIN"/><br /><sub><b>LL-LIN</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LL-LIN" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ALL-LIN" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://bigcyy.github.io/"><img src="https://avatars.githubusercontent.com/u/73413979?v=4?s=100" width="100px;" alt="Yang Chen"/><br /><sub><b>Yang Chen</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bigcyy" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=bigcyy" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Abigcyy" title="Bug reports">🐛</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sarthakeash"><img src="https://avatars.githubusercontent.com/u/74091160?v=4?s=100" width="100px;" alt="Sarthak Arora"/><br /><sub><b>Sarthak Arora</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sarthakeash" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=sarthakeash" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PengJingzhao"><img src="https://avatars.githubusercontent.com/u/97368949?v=4?s=100" width="100px;" alt="彭镜肇"/><br /><sub><b>彭镜肇</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=PengJingzhao" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gagaradio"><img src="https://avatars.githubusercontent.com/u/18532370?v=4?s=100" width="100px;" alt="Walter Jia"/><br /><sub><b>Walter Jia</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=gagaradio" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/boyucjz"><img src="https://avatars.githubusercontent.com/u/18730041?v=4?s=100" width="100px;" alt="boyucjz"/><br /><sub><b>boyucjz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=boyucjz" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Cyanty"><img src="https://avatars.githubusercontent.com/u/153884653?v=4?s=100" width="100px;" alt="Cyanty"/><br /><sub><b>Cyanty</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KevinLLF"><img src="https://avatars.githubusercontent.com/u/85452733?v=4?s=100" width="100px;" alt="Jay丿167"/><br /><sub><b>Jay丿167</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=KevinLLF" title="Code">💻</a></td>
</tr>
</tbody>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
## 💬 コミュニティ交流
##### チャネル
[メール](https://lists.apache.org/list.html?dev@hertzbeat.apache.org) : メーリングリストに登録するために、```dev-subscribe@hertzbeat.apache.org```にメールを送ってください。
[Chat On Discord](https://discord.gg/Fb6M73htGr)
WeChatグループ : `ahertzbeat` を検索.
WeChat公式アカウント : `usthecom`を検索.
[QQグループ](https://qm.qq.com/q/xxqecSC2cw) : グループ番号 `1035688434`
[Github Discussion](https://github.com/apache/hertzbeat/discussions)
[Reddit Community](https://www.reddit.com/r/hertzbeat/)
[Follow Us Twitter](https://x.com/hertzbeat1024)
[Subscribe YouTube](https://www.youtube.com/channel/UCri75zfWX0GHqJFPENEbLow)
##### Open-Source Project Build From Open-Source
HertzBeat is built on so many great open source projects, thanks to them!
- `Java Spring SpringBoot Jpa Maven Assembly Netty Lombok Sureness Protobuf HttpClient Guava SnakeYaml JsonPath ...`
- `TypeScript Angular NG-ZORRO NG-ALAIN NodeJs Npm Html Less Echarts Rxjs ZoneJs MonacoEditor SlickCarousel Docusaurus ...`
## Landscape
<p align="left">
<img src="./home/static/img/home/cncf-landscape-left-logo.svg" width="300">&nbsp;&nbsp;<img src="./home/static/img/home/cncf-right-logo.svg" width="345" />
<br /><br />
HertzBeat has been included in the <a href="https://landscape.cncf.io/guide?item=observability-and-analysis--monitoring--hertzbeat#observability-and-analysis--monitoring">
CNCF Observability And Analysis - Monitoring Landscape.</a>
</p>
## 🛡️ License
[`Apache License, Version 2.0`](https://www.apache.org/licenses/LICENSE-2.0.html)
+39
View File
@@ -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.
# This workflow will build a Java project with Maven
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJkakNDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdGMyVnkKZG1WeUxXTmhRREUzTkRVeU5ESTROamd3SGhjTk1qVXdOREl4TVRNME1UQTRXaGNOTXpVd05ERTVNVE0wTVRBNApXakFqTVNFd0h3WURWUVFEREJock0zTXRjMlZ5ZG1WeUxXTmhRREUzTkRVeU5ESTROamd3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFUR2U2SW5hQko4RGRiQVdmZTRNM09yS3ZTR1IvWkROS0JJV2lMejZXckcKYmh4Mm5ncHJFQjhRYThEWDZocGRCVkI1RmN3L1hwTXZjRnNCeFh3MThPNkdvMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVWRXOEFySENLNVQ2OFdJeG1BUnlTCkJFcHJ6N1F3Q2dZSUtvWkl6ajBFQXdJRFJ3QXdSQUlnYlhwYXV0c2RlTlJHNm4zRmhBRkkzbncvWUErYTBBdi8KUE9HZzNhYWRCSDBDSUU5NW4vQW5yOXV2Z0tyczVrakpGKzAwci96SWpFdnQxNmhkOGdabFEzb1IKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
server: https://172.29.0.11:6443
name: default
contexts:
- context:
cluster: default
user: default
name: default
current-context: default
kind: Config
preferences: {}
users:
- name: default
user:
client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJrVENDQVRlZ0F3SUJBZ0lJZm5OdzUyby9ybDB3Q2dZSUtvWkl6ajBFQXdJd0l6RWhNQjhHQTFVRUF3d1kKYXpOekxXTnNhV1Z1ZEMxallVQXhOelExTWpReU9EWTRNQjRYRFRJMU1EUXlNVEV6TkRFd09Gb1hEVEkyTURReQpNVEV6TkRFd09Gb3dNREVYTUJVR0ExVUVDaE1PYzNsemRHVnRPbTFoYzNSbGNuTXhGVEFUQmdOVkJBTVRESE41CmMzUmxiVHBoWkcxcGJqQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJHa3UxWDQvRGNMU1h4cXcKSzczdGhMUFd5Y3JCendrL3ZMbzkzMTZ0dXNXRWE4WnNMdytnYk5wWEVOSFNxQk8zdGcrYUFSdGhWakF3OU5GaApZWXNtaTQyalNEQkdNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUtCZ2dyQmdFRkJRY0RBakFmCkJnTlZIU01FR0RBV2dCU1B5dEJrYlFaRmtxSEpOaHlHNjZ1K3ZqbjNhakFLQmdncWhrak9QUVFEQWdOSUFEQkYKQWlFQXZQVC9ENGs2S29tUUxaZjRNMTEvd1psT3QyZkpGTGhIY3EyOHMvUUVLcndDSUZVMWVzTGFFTnRWc2xUeAo0QTFtNkt4eEpUbmdNM0M3UXJ5OUs0Z2hzVDA0Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0KLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJkekNDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdFkyeHAKWlc1MExXTmhRREUzTkRVeU5ESTROamd3SGhjTk1qVXdOREl4TVRNME1UQTRXaGNOTXpVd05ERTVNVE0wTVRBNApXakFqTVNFd0h3WURWUVFEREJock0zTXRZMnhwWlc1MExXTmhRREUzTkRVeU5ESTROamd3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFSeENGL2FuNVUvS0xEUk9VUlZxdllWTHFsdWxXaE8rMzJpc1lRU1hlYUgKdnRBWHo3dFE2MkhrazNIdXZudnFmditvQlBKVUlzS3ExTyt6aytLT3JWa2dvMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVWo4clFaRzBHUlpLaHlUWWNodXVyCnZyNDU5Mm93Q2dZSUtvWkl6ajBFQXdJRFNBQXdSUUlnWmxiRkpoZWwzQiszQ2FyZjUrdE5taDhkWkRUOC9yVDAKR01VVXlBNzlWSUVDSVFDSUQ1NkVQbmtOa0xmZExEZXloclBGWks3VWRTc2x2QWZGN2N4czF1MDBxZz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
client-key-data: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUpMOGJrS1FMbEdZWUE0Ny9Lek9CRm5iZnI0NGQ5VndUWXlPT0dJdnNLOXNvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFYVM3VmZqOE53dEpmR3JBcnZlMkVzOWJKeXNIUENUKzh1ajNmWHEyNnhZUnJ4bXd2RDZCcwoybGNRMGRLb0U3ZTJENW9CRzJGV01ERDAwV0ZoaXlhTGpRPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=
+1
View File
@@ -0,0 +1 @@
K10800b7e6b1f6390067e433bd30dff5f925893b60074f8c36f7725bfdce6a45917::server:ca7449eb0dddd8a4bb6e5ca86e2d0cdd
+37
View File
@@ -0,0 +1,37 @@
{
"detected": true,
"monitor": {
"intervals": 10,
"tags": [],
"app": "kubernetes",
"host": "172.29.0.11",
"name": "Brave_Stingray_55yR"
},
"collector": "",
"params": [
{
"display": true,
"field": "host",
"type": 1,
"paramValue": "172.29.0.11"
},
{
"display": true,
"field": "port",
"type": 0,
"paramValue": 6443
},
{
"display": true,
"field": "authType",
"type": 1,
"paramValue": "Bearer Token"
},
{
"display": true,
"field": "token",
"type": 1,
"paramValue": "#TOKEN#"
}
]
}
+69
View File
@@ -0,0 +1,69 @@
#!api-testing
# 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.
# yaml-language-server: $schema=https://linuxsuren.github.io/api-testing/api-testing-schema.json
name: hertzbeat
api: |
{{default "http://localhost:1157" (env "SERVER")}}
param:
monitorHTTP: "{{randAlpha 6}}"
monitorSiteMap: "{{randAlpha 6}}"
monitorPort: "{{randAlpha 6}}"
monitorSSL: "{{randAlpha 6}}"
monitorPing: "{{randAlpha 6}}"
monitorUDP: "{{randAlpha 6}}"
monitorWebsite: "{{randAlpha 6}}"
monitorFTP: "{{randAlpha 6}}"
tagName: "{{randAlpha 3}}"
items:
- name: login
request:
api: /api/account/auth/form
method: POST
header:
Content-type: application/json
body: |
{
"type": 0,
"identifier": "admin",
"credential": "hertzbeat"
}
expect:
bodyFieldsExpect:
code: "0"
- name: detectMonitor
request:
api: /api/monitor/detect
method: POST
header:
Authorization: Bearer {{.login.data.token}}
Content-type: application/json
bodyFromFile: data/tmp.json
expect:
statusCode: 200
- name: createMonitor
request:
api: /api/monitor
method: POST
header:
Authorization: Bearer {{.login.data.token}}
Content-type: application/json
bodyFromFile: data/tmp.json
expect:
statusCode: 200
+123
View File
@@ -0,0 +1,123 @@
# 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.
version: '3.8'
services:
init:
image: bitnami/kubectl:latest
privileged: true
depends_on:
k3s:
condition: service_healthy
entrypoint: /bin/bash
networks:
k3s_network:
ipv4_address: 172.29.0.10
volumes:
- ./data/config/k3s.yaml:/.kube/config
- ./data:/testcase
command:
- -c
- |
# Wait for k3s to start
until kubectl cluster-info; do sleep 1; done
sleep 5
# Create serviceaccount and clusterrolebinding
kubectl create serviceaccount -n kube-system cluster-admin-sa
kubectl create clusterrolebinding -n kube-system cluster-admin-binding --clusterrole=cluster-admin --serviceaccount=kube-system:cluster-admin-sa
cd /testcase
kubectl create token cluster-admin-sa -n kube-system --duration=24h
echo $(sed "s/#TOKEN#/$(echo $(kubectl create token cluster-admin-sa -n kube-system --duration=24h))/g" create_k8s_monitor_data.json)>tmp.json
# install metrics server
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
sleep 30
tail -f /dev/null
k3s:
image: rancher/k3s:v1.31.0-k3s1
container_name: k3s
privileged: true
ports:
- "6443:6443"
- "80:80"
command: server --bind-address 172.29.0.11
networks:
k3s_network:
ipv4_address: 172.29.0.11
healthcheck:
test: [ "CMD-SHELL",
"kubectl cluster-info && echo $(cat /var/lib/rancher/k3s/server/node-token)>node-token.txt" ]
interval: 3s
retries: 30
start_period: 10s
timeout: 10s
volumes:
- ./data/config/k3s.yaml:/etc/rancher/k3s/k3s.yaml
- ./data/config/node-token.txt:/node-token.txt
testing:
image: ghcr.io/linuxsuren/api-testing:v0.0.17
environment:
SERVER: http://hertzbeat:1157
container_name: e2e-testing
volumes:
- ./data/:/work/data/
- ./data/testsuite.yaml:/work/testsuite.yaml
command: atest run -p /work/testsuite.yaml --level debug --thread 3
networks:
k3s_network:
ipv4_address: 172.29.0.12
depends_on:
checker:
condition: service_healthy
k3s:
condition: service_healthy
links:
- hertzbeat
- checker
- k3s
checker:
image: alpine:latest
container_name: e2e-checker
command: sh -c "apk add --update curl && tail -f /dev/null"
healthcheck:
test: [ "CMD-SHELL",
"curl -s -X POST -H \"Content-Type: application/json\" -d '{\"type\": 0,\"identifier\": \"admin\",\"credential\": \"hertzbeat\"}' http://hertzbeat:1157/api/account/auth/form | grep -q token"
]
interval: 3s
timeout: 60s
retries: 10
start_period: 10s
networks:
k3s_network:
ipv4_address: 172.29.0.13
depends_on:
- init
hertzbeat:
image: apache/hertzbeat:test
container_name: e2e-hertzbeat
volumes:
- ./logs/:/opt/hertzbeat/logs/
ports:
- "1157:1157"
networks:
k3s_network:
ipv4_address: 172.29.0.14
networks:
k3s_network:
driver: bridge
ipam:
config:
- subnet: 172.29.0.0/16
@@ -220,11 +220,11 @@ public class RealTimeAlertCalculator {
fingerPrints.putAll(commonFingerPrints);
for (int index = 0; index < valueRow.getColumnsList().size(); index++) {
String valueStr = valueRow.getColumns(index);
final CollectRep.Field field = fields.get(index);
if (CommonConstants.NULL_VALUE.equals(valueStr)) {
fieldValueMap.put(field.getName(), null);
continue;
}
final CollectRep.Field field = fields.get(index);
final int fieldType = field.getType();
if (fieldType == CommonConstants.TYPE_NUMBER) {
@@ -0,0 +1,133 @@
/*
* 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 lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
/**
* uptime-kuma alert content entity
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class UptimeKumaExternAlert {
private Heartbeat heartbeat;
private Monitor monitor;
private String msg;
/**
* Monitor information
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Heartbeat {
@JsonProperty("monitorID")
private int monitorId;
private int status;
private String time;
private String msg;
private boolean important;
private int duration;
private String timezone;
private String timezoneOffset;
private String localDateTime;
}
/**
* Monitor information
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Monitor {
private int id;
private String name;
private String description;
private String pathName;
private String parent;
@JsonProperty("childrenIDs")
private List<String> childrenIds;
private String url;
private String method;
private String hostname;
private String port;
@JsonProperty("maxretries")
private int maxRetries;
private int weight;
private boolean active;
private boolean forceInactive;
private String type;
private int timeout;
private int interval;
private int retryInterval;
private int resendInterval;
private String keyword;
private boolean invertKeyword;
private boolean expiryNotification;
private boolean ignoreTls;
private boolean upsideDown;
private int packetSize;
@JsonProperty("maxredirects")
private int maxRedirects;
@JsonProperty("accepted_statuscodes")
private List<String> acceptedStatusCodes;
private String dnsResolveType;
private String dnsResolveServer;
private String dnsLastResult;
private String dockerContainer;
private String dockerHost;
private String proxyId;
private Map<String, Object> notificationIDList;
private List<String> tags;
private boolean maintenance;
private String mqttTopic;
private String mqttSuccessMessage;
private String databaseQuery;
private String authMethod;
private String grpcUrl;
private String grpcProtobuf;
private String grpcMethod;
private String grpcServiceName;
private boolean grpcEnableTls;
private String radiusCalledStationId;
private String radiusCallingStationId;
private String game;
private boolean gamedigGivenPortOnly;
private String httpBodyEncoding;
private String jsonPath;
private String expectedValue;
private String kafkaProducerTopic;
private List<String> kafkaProducerBrokers;
private boolean kafkaProducerSsl;
private boolean kafkaProducerAllowAutoTopicCreation;
private String kafkaProducerMessage;
private String screenshot;
private boolean includeSensitiveData;
}
}
@@ -52,86 +52,84 @@ final class DbAlertStoreHandlerImpl implements AlertStoreHandler {
log.error("The Group Alerts is empty, ignore store");
return groupAlert;
}
// 1. Find existing alert group
GroupAlert existGroupAlert = groupAlertDao.findByGroupKey(groupAlert.getGroupKey());
// 2. Process individual alerts
// Process individual alerts
Set<String> alertFingerprints = new HashSet<>(8);
List<SingleAlert> originalAlerts = groupAlert.getAlerts();
List<SingleAlert> newAlerts = new ArrayList<>();
for (SingleAlert singleAlert : originalAlerts) {
SingleAlert existAlert = singleAlertDao.findByFingerprint(singleAlert.getFingerprint());
if (existAlert != null) {
// Update the existing alert with the ID and creation time from the database
singleAlert.setId(existAlert.getId());
singleAlert.setGmtCreate(existAlert.getGmtCreate());
// Status transition logic
if (CommonConstants.ALERT_STATUS_FIRING.equals(singleAlert.getStatus())) {
// If the alert is firing and the existing alert is not resolved, update the start time and trigger times
if (!CommonConstants.ALERT_STATUS_RESOLVED.equals(existAlert.getStatus())) {
synchronized (singleAlert.getFingerprint().intern()) {
SingleAlert existAlert = singleAlertDao.findByFingerprint(singleAlert.getFingerprint());
if (existAlert != null) {
// Update the existing alert with the ID and creation time from the database
singleAlert.setId(existAlert.getId());
singleAlert.setGmtCreate(existAlert.getGmtCreate());
// Status transition logic
if (CommonConstants.ALERT_STATUS_FIRING.equals(singleAlert.getStatus())) {
// If the alert is firing and the existing alert is not resolved, update the start time and trigger times
if (!CommonConstants.ALERT_STATUS_RESOLVED.equals(existAlert.getStatus())) {
singleAlert.setStartAt(existAlert.getStartAt());
int triggerTimes = Optional.ofNullable(existAlert.getTriggerTimes()).orElse(1)
+ Optional.ofNullable(singleAlert.getTriggerTimes()).orElse(1);
singleAlert.setTriggerTimes(triggerTimes);
}
} else if (CommonConstants.ALERT_STATUS_RESOLVED.equals(singleAlert.getStatus())) {
// If the alert is resolved, set the end time (if not already set) and copy other fields from the existing alert
if (singleAlert.getEndAt() == null) {
singleAlert.setEndAt(System.currentTimeMillis());
}
singleAlert.setStartAt(existAlert.getStartAt());
int triggerTimes = Optional.ofNullable(existAlert.getTriggerTimes()).orElse(1)
+ Optional.ofNullable(singleAlert.getTriggerTimes()).orElse(1);
singleAlert.setTriggerTimes(triggerTimes);
singleAlert.setActiveAt(existAlert.getActiveAt());
singleAlert.setTriggerTimes(existAlert.getTriggerTimes());
}
} else if (CommonConstants.ALERT_STATUS_RESOLVED.equals(singleAlert.getStatus())) {
// If the alert is resolved, set the end time (if not already set) and copy other fields from the existing alert
if (singleAlert.getEndAt() == null) {
singleAlert.setEndAt(System.currentTimeMillis());
}
singleAlert.setStartAt(existAlert.getStartAt());
singleAlert.setActiveAt(existAlert.getActiveAt());
singleAlert.setTriggerTimes(existAlert.getTriggerTimes());
}
SingleAlert savedSingleAlert = singleAlertDao.save(singleAlert);
newAlerts.add(savedSingleAlert);
alertFingerprints.add(savedSingleAlert.getFingerprint());
}
SingleAlert savedSingleAlert = singleAlertDao.save(singleAlert);
newAlerts.add(savedSingleAlert);
alertFingerprints.add(savedSingleAlert.getFingerprint());
}
groupAlert.setAlerts(newAlerts);
// 3. Process resolved alerts
if (existGroupAlert != null) {
List<String> existFingerprints = existGroupAlert.getAlertFingerprints();
if (existFingerprints != null) {
alertFingerprints.addAll(existFingerprints);
}
// Merge group information
groupAlert.setId(existGroupAlert.getId());
groupAlert.setGmtCreate(existGroupAlert.getGmtCreate());
// Merge other historical information to preserve
Map<String, String> existCommonLabels = existGroupAlert.getCommonLabels();
if (existCommonLabels != null) {
Map<String, String> commonLabels = groupAlert.getCommonLabels();
if (commonLabels != null) {
// filter common label in commonLabels and existCommonLabels
commonLabels = commonLabels.entrySet().stream()
.filter(entry -> existCommonLabels.containsKey(entry.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
groupAlert.setCommonLabels(commonLabels);
// Find existing alert group
synchronized (groupAlert.getGroupKey().intern()) {
GroupAlert existGroupAlert = groupAlertDao.findByGroupKey(groupAlert.getGroupKey());
// Process resolved alerts
if (existGroupAlert != null) {
List<String> existFingerprints = existGroupAlert.getAlertFingerprints();
if (existFingerprints != null) {
alertFingerprints.addAll(existFingerprints);
}
}
Map<String, String> existCommonAnnotations = existGroupAlert.getCommonAnnotations();
if (existCommonAnnotations != null) {
Map<String, String> commonAnnotations = groupAlert.getCommonAnnotations();
if (commonAnnotations != null) {
// filter common annotation in commonAnnotations and existCommonAnnotations
commonAnnotations = commonAnnotations.entrySet().stream()
.filter(entry -> existCommonAnnotations.containsKey(entry.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
groupAlert.setCommonAnnotations(commonAnnotations);
// Merge group information
groupAlert.setId(existGroupAlert.getId());
groupAlert.setGmtCreate(existGroupAlert.getGmtCreate());
// Merge other historical information to preserve
Map<String, String> existCommonLabels = existGroupAlert.getCommonLabels();
if (existCommonLabels != null) {
Map<String, String> commonLabels = groupAlert.getCommonLabels();
if (commonLabels != null) {
// filter common label in commonLabels and existCommonLabels
commonLabels = commonLabels.entrySet().stream()
.filter(entry -> existCommonLabels.containsKey(entry.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
groupAlert.setCommonLabels(commonLabels);
}
}
Map<String, String> existCommonAnnotations = existGroupAlert.getCommonAnnotations();
if (existCommonAnnotations != null) {
Map<String, String> commonAnnotations = groupAlert.getCommonAnnotations();
if (commonAnnotations != null) {
// filter common annotation in commonAnnotations and existCommonAnnotations
commonAnnotations = commonAnnotations.entrySet().stream()
.filter(entry -> existCommonAnnotations.containsKey(entry.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
groupAlert.setCommonAnnotations(commonAnnotations);
}
}
}
// Save alert group
groupAlert.setAlertFingerprints(alertFingerprints.stream().toList());
GroupAlert savedGroupAlert = groupAlertDao.save(groupAlert);
savedGroupAlert.setAlerts(groupAlert.getAlerts());
return savedGroupAlert;
}
// 4. Save alert group
groupAlert.setAlertFingerprints(alertFingerprints.stream().toList());
GroupAlert savedGroupAlert = groupAlertDao.save(groupAlert);
savedGroupAlert.setAlerts(groupAlert.getAlerts());
return savedGroupAlert;
}
}
@@ -31,6 +31,8 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* Send alarm information through Server
*/
@@ -54,7 +56,16 @@ public class ServerChanAlertNotifyHandlerImpl extends AbstractAlertNotifyHandler
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<ServerChanAlertNotifyHandlerImpl.ServerChanWebHookDto> httpEntity = new HttpEntity<>(serverChanWebHookDto, headers);
String webHookUrl = String.format(alerterProperties.getServerChanWebhookUrl(), receiver.getServerChanToken());
String sanitizedToken = receiver.getServerChanToken().replaceAll("[^a-zA-Z0-9_-]", "");
String webHookUrl = String.format(alerterProperties.getServerChanWebhookUrl(), sanitizedToken);
// Validate the constructed URL against a whitelist
List<String> allowedBaseUrls = List.of("https://api.serverchan.com", "https://serverchan.example.com");
boolean isValidUrl = allowedBaseUrls.stream().anyMatch(webHookUrl::startsWith);
if (!isValidUrl) {
throw new AlertNoticeException("Invalid webhook URL: " + webHookUrl);
}
ResponseEntity<CommonRobotNotifyResp> responseEntity = restTemplate.postForEntity(webHookUrl,
httpEntity, CommonRobotNotifyResp.class);
if (responseEntity.getStatusCode() == HttpStatus.OK) {
@@ -17,6 +17,7 @@
package org.apache.hertzbeat.alert.notice.impl;
import java.net.URI;
import java.util.Objects;
import lombok.Builder;
import lombok.Data;
@@ -52,7 +53,12 @@ final class SlackAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<SlackNotifyDTO> slackNotifyEntity = new HttpEntity<>(slackNotify, headers);
var entity = restTemplate.postForEntity(receiver.getSlackWebHookUrl(), slackNotifyEntity, String.class);
String slackWebHookUrl = receiver.getSlackWebHookUrl();
if (!isValidSlackWebHookUrl(slackWebHookUrl)) {
log.warn("Invalid Slack Webhook URL: {}", slackWebHookUrl);
throw new AlertNoticeException("Invalid Slack Webhook URL");
}
var entity = restTemplate.postForEntity(slackWebHookUrl, slackNotifyEntity, String.class);
if (entity.getStatusCode() == HttpStatus.OK && entity.getBody() != null) {
var body = entity.getBody();
if (Objects.equals(SUCCESS, body)) {
@@ -81,4 +87,21 @@ final class SlackAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl {
private String text;
}
/**
* Validate if the Slack Webhook URL belongs to an allowed domain.
*
* @param url the Slack Webhook URL to validate
* @return true if the URL is valid, false otherwise
*/
private boolean isValidSlackWebHookUrl(String url) {
try {
URI uri = new URI(url);
String host = uri.getHost();
return "hooks.slack.com".equals(host);
} catch (Exception e) {
log.warn("Error validating Slack Webhook URL: {}", url, e);
return false;
}
}
}
@@ -42,9 +42,14 @@ import org.springframework.stereotype.Component;
final class TelegramBotAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl {
@Override
public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) throws AlertNoticeException {
public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert)
throws AlertNoticeException {
try {
String url = String.format(alerterProperties.getTelegramWebhookUrl(), receiver.getTgBotToken());
String token = receiver.getTgBotToken();
if (!isValidTelegramToken(token)) {
throw new AlertNoticeException("Invalid Telegram Bot Token");
}
String url = String.format(alerterProperties.getTelegramWebhookUrl(), token);
TelegramBotNotifyDTO notifyBody = TelegramBotNotifyDTO.builder()
.chatId(receiver.getTgUserId())
.text(renderContent(noticeTemplate, alert))
@@ -54,7 +59,8 @@ final class TelegramBotAlertNotifyHandlerImpl extends AbstractAlertNotifyHandler
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<TelegramBotNotifyDTO> telegramEntity = new HttpEntity<>(notifyBody, headers);
ResponseEntity<TelegramBotNotifyResponse> entity = restTemplate.postForEntity(url, telegramEntity, TelegramBotNotifyResponse.class);
ResponseEntity<TelegramBotNotifyResponse> entity = restTemplate.postForEntity(url, telegramEntity,
TelegramBotNotifyResponse.class);
if (entity.getStatusCode() == HttpStatus.OK && entity.getBody() != null) {
TelegramBotNotifyResponse body = entity.getBody();
if (body.ok) {
@@ -99,4 +105,10 @@ final class TelegramBotAlertNotifyHandlerImpl extends AbstractAlertNotifyHandler
private String description;
}
private boolean isValidTelegramToken(String token) {
// Adjusted pattern to match real Telegram Bot tokens like
// 110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw
String tokenPattern = "^[0-9]+:[a-zA-Z0-9_-]+$";
return token != null && token.matches(tokenPattern);
}
}
@@ -57,7 +57,12 @@ final class WeComRobotAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerI
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<WeWorkWebHookDto> httpEntity = new HttpEntity<>(weWorkWebHookDTO, headers);
String webHookUrl = alerterProperties.getWeWorkWebhookUrl() + receiver.getWechatId();
String wechatId = receiver.getWechatId();
if (!isValidWechatId(wechatId)) {
log.warn("Invalid WeChat ID: {}", wechatId);
throw new AlertNoticeException("Invalid WeChat ID provided.");
}
String webHookUrl = alerterProperties.getWeWorkWebhookUrl() + wechatId;
ResponseEntity<CommonRobotNotifyResp> entity = restTemplate.postForEntity(webHookUrl, httpEntity, CommonRobotNotifyResp.class);
if (entity.getStatusCode() == HttpStatus.OK) {
assert entity.getBody() != null;
@@ -170,4 +175,15 @@ final class WeComRobotAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerI
}
}
/**
* Validate the WeChat ID to ensure it meets the expected format.
*
* @param wechatId the WeChat ID to validate
* @return true if valid, false otherwise
*/
private boolean isValidWechatId(String wechatId) {
// Example validation: ensure the ID is alphanumeric and non-empty
return StringUtils.isNotBlank(wechatId) && wechatId.matches("^[a-zA-Z0-9_-]+$");
}
}
@@ -0,0 +1,118 @@
/*
* 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 lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.dto.UptimeKumaExternAlert;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.alert.service.ExternAlertService;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.HashMap;
import java.util.Map;
/**
* uptime-kuma external alarm service impl
*/
@Slf4j
@Service
public class UptimeKumaExternAlertServiceImpl implements ExternAlertService {
@Autowired
private AlarmCommonReduce alarmCommonReduce;
@Override
public void addExternAlert(String content) {
UptimeKumaExternAlert alert = JsonUtil.fromJson(content, UptimeKumaExternAlert.class);
if (alert == null) {
log.warn("parse extern alert content failed! content: {}", content);
return;
}
SingleAlert singleAlert = new UptimeKumaAlertConverter().convert(alert);
alarmCommonReduce.reduceAndSendAlarm(singleAlert);
}
/**
* Converter: UptimeKuma alert to SingleAlert
*/
public static class UptimeKumaAlertConverter {
/**
* Convert UptimeKuma alert to SingleAlert
*/
public SingleAlert convert(UptimeKumaExternAlert alert) {
// build basic info
SingleAlert.SingleAlertBuilder builder = SingleAlert.builder()
.status(convertStatus(alert.getHeartbeat().getStatus()))
.startAt(parseTime(alert.getHeartbeat().getTime()))
.activeAt(parseTime(alert.getHeartbeat().getTime()))
.triggerTimes(1);
// build labels
Map<String, String> labels = new HashMap<>();
labels.put("__source__", "uptime_kuma");
labels.put("monitor_id", String.valueOf(alert.getMonitor().getId()));
labels.put("monitor_name", alert.getMonitor().getName());
// build annotations
Map<String, String> annotations = new HashMap<>();
annotations.put("description", alert.getMonitor().getDescription());
annotations.put("message", alert.getHeartbeat().getMsg());
annotations.put("important", String.valueOf(alert.getHeartbeat().isImportant()));
return builder
.labels(labels)
.annotations(annotations)
.content(buildContent(alert))
.build();
}
private String buildContent(UptimeKumaExternAlert alert) {
return String.format("Monitor [%s] %s: %s",
alert.getMonitor().getName(),
alert.getMonitor().getDescription(),
alert.getHeartbeat().getMsg());
}
private String convertStatus(int status) {
// uptime kuma status: 1-up, 0-down, 2-pending
return status == 1 ? CommonConstants.ALERT_STATUS_RESOLVED : CommonConstants.ALERT_STATUS_FIRING;
}
private Long parseTime(String timeStr) {
try {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.parse(timeStr).getTime();
} catch (ParseException e) {
log.error("Failed to parse time: {}", timeStr);
throw new IllegalArgumentException("Failed to parse time: " + timeStr, e);
}
}
}
@Override
public String supportSource() {
return "uptime-kuma";
}
}
@@ -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.impl;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.alert.service.ExternAlertService;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* zabbix external alarm service impl
*/
@Slf4j
@Service
public class ZabbixExternAlertServiceImpl implements ExternAlertService {
@Autowired
private AlarmCommonReduce alarmCommonReduce;
@Override
public void addExternAlert(String content) {
SingleAlert alert = JsonUtil.fromJson(content, SingleAlert.class);
if (alert == null) {
log.warn("parse extern alert content failed! content: {}", content);
return;
}
alarmCommonReduce.reduceAndSendAlarm(alert);
}
@Override
public String supportSource() {
return "zabbix";
}
}
@@ -21,6 +21,7 @@ import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
/**
* Alarm template keyword matching replacement engine tool
@@ -38,11 +39,11 @@ public final class AlertTemplateUtil {
private static final Pattern PATTERN = Pattern.compile("\\$\\{(\\w+)\\}");
public static String render(String template, Map<String, Object> replaceData) {
if (template == null) {
return null;
if (!StringUtils.hasText(template)) {
return template;
}
if (replaceData == null) {
log.warn("The replaceData is null.");
log.warn("The render replace data is null.");
return template;
}
try {
@@ -50,7 +51,7 @@ public final class AlertTemplateUtil {
StringBuilder builder = new StringBuilder();
while (matcher.find()) {
Object objectValue = replaceData.getOrDefault(matcher.group(1), "NullValue");
String value = objectValue.toString();
String value = objectValue != null ? objectValue.toString() : "NullValue";
matcher.appendReplacement(builder, Matcher.quoteReplacement(value));
}
matcher.appendTail(builder);
@@ -0,0 +1,154 @@
/*
* 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 org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.alert.service.DataSourceService;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
/**
* Test case for {@link PeriodicAlertCalculator}
*/
@ExtendWith(MockitoExtension.class)
class PeriodicAlertCalculatorTest {
@Mock
private DataSourceService dataSourceService;
@Mock
private AlarmCommonReduce alarmCommonReduce;
@Mock
private AlarmCacheManager alarmCacheManager;
@InjectMocks
private PeriodicAlertCalculator periodicAlertCalculator;
private AlertDefine rule;
@BeforeEach
void setUp() {
rule = AlertDefine.builder()
.id(1L)
.name("high_cpu_usage")
.type("periodic")
.expr("usage>90")
.times(1)
.labels(Map.of("team", "test-team", "priority", "P1"))
.annotations(Map.of("summary", "High CPU usage detected"))
.template("High CPU usage (${__value__}%)")
.datasource("PROMETHEUS")
.enable(true)
.period(300)
.build();
}
@Test
void testTriggerAlertWhenMatchThreshold() {
reset(alarmCacheManager);
Map<String, Object> result = new HashMap<>();
result.put("__value__", 95.0); // Non-null, matched with threshold
result.put("__timestamp__", System.currentTimeMillis());
when(dataSourceService.calculate(anyString(), anyString())).thenReturn(List.of(result));
when(alarmCacheManager.getPending(anyString())).thenReturn(null);
periodicAlertCalculator.calculate(rule);
// Verify that putFiring is called
ArgumentCaptor<String> idCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<SingleAlert> alertCaptor = ArgumentCaptor.forClass(SingleAlert.class);
verify(alarmCacheManager).putFiring(idCaptor.capture(), alertCaptor.capture());
// Assertion alarm status and content
SingleAlert alert = alertCaptor.getValue();
assertAll(() -> assertEquals(CommonConstants.ALERT_STATUS_FIRING, alert.getStatus()),
() -> assertEquals("High CPU usage (95.0%)", alert.getContent()));
}
@Test
void testNoTriggerAlertWhenNotMatchThreshold() {
reset(alarmCacheManager);
Map<String, Object> result = new HashMap<>();
result.put("__value__", null); // null, not matched with threshold
result.put("__timestamp__", System.currentTimeMillis());
when(dataSourceService.calculate(anyString(), anyString())).thenReturn(List.of(result));
periodicAlertCalculator.calculate(rule);
verify(alarmCacheManager, times(0)).putFiring(any(), any());
}
@Test
void testResolveFiringAlert() {
reset(alarmCacheManager);
Map<String, Object> result = new HashMap<>();
result.put("__value__", null); // null, not matched with threshold
result.put("__timestamp__", System.currentTimeMillis());
SingleAlert pendingAlert = SingleAlert.builder()
.status(CommonConstants.ALERT_STATUS_FIRING)
.triggerTimes(2).startAt(System.currentTimeMillis() - 60000)
.activeAt(System.currentTimeMillis() - 30000)
.build();
when(alarmCacheManager.removeFiring(anyString())).thenReturn(pendingAlert);
when(dataSourceService.calculate(anyString(), anyString())).thenReturn(List.of(result));
periodicAlertCalculator.calculate(rule);
ArgumentCaptor<SingleAlert> resolvedCaptor = ArgumentCaptor.forClass(SingleAlert.class);
verify(alarmCommonReduce).reduceAndSendAlarm(resolvedCaptor.capture());
SingleAlert resolvedAlert = resolvedCaptor.getValue();
assertAll(() -> assertEquals(CommonConstants.ALERT_STATUS_RESOLVED, resolvedAlert.getStatus()),
() -> assertNotNull(resolvedAlert.getEndAt()),
() -> assertTrue(resolvedAlert.getEndAt() > resolvedAlert.getStartAt()));
}
@ParameterizedTest
@NullAndEmptySource
void testSkipWhenDataSourceResultInvalid(List<Map<String, Object>> results) {
when(dataSourceService.calculate(anyString(), anyString())).thenReturn(results);
periodicAlertCalculator.calculate(rule);
verifyNoInteractions(alarmCacheManager);
}
@Test
void testHandleDisabledRule() {
rule.setEnable(false);
periodicAlertCalculator.calculate(rule);
verifyNoInteractions(dataSourceService);
}
}
@@ -57,6 +57,7 @@ class DbAlertStoreHandlerImplTest {
public void setUp() {
groupAlert = new GroupAlert();
singleAlert = new SingleAlert();
singleAlert.setFingerprint("test-fingerprint");
List<SingleAlert> alerts = new ArrayList<>();
alerts.add(singleAlert);
groupAlert.setAlerts(alerts);
@@ -77,11 +78,13 @@ class DbAlertStoreHandlerImplTest {
when(groupAlertDao.findByGroupKey(groupKey)).thenReturn(null);
SingleAlert savedSingleAlert = new SingleAlert();
savedSingleAlert.setFingerprint("test-finger");
when(singleAlertDao.save(any(SingleAlert.class))).thenReturn(savedSingleAlert);
GroupAlert savedGroupAlert = new GroupAlert();
savedGroupAlert.setGroupKey(groupKey);
when(groupAlertDao.save(any(GroupAlert.class))).thenReturn(savedGroupAlert);
dbAlertStoreHandler.store(groupAlert);
verify(singleAlertDao).save(any(SingleAlert.class));
@@ -20,6 +20,7 @@ package org.apache.hertzbeat.alert.notice.impl;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.when;
import org.apache.hertzbeat.alert.AlerterProperties;
@@ -48,7 +49,7 @@ import java.util.ResourceBundle;
*/
@ExtendWith(MockitoExtension.class)
class ServerChanAlertNotifyHandlerImplTest {
@Mock
private RestTemplate restTemplate;
@@ -57,20 +58,21 @@ class ServerChanAlertNotifyHandlerImplTest {
@Mock
private ResourceBundle bundle;
@InjectMocks
private ServerChanAlertNotifyHandlerImpl serverChanAlertNotifyHandler;
private NoticeReceiver receiver;
private GroupAlert groupAlert;
private NoticeTemplate template;
@BeforeEach
public void setUp() {
receiver = new NoticeReceiver();
receiver.setId(1L);
receiver.setName("test-receiver");
receiver.setAccessToken("test-token");
receiver.setServerChanToken("SCT193569TSNm6xIabdjqeZPtOGOWcvU1e");
groupAlert = new GroupAlert();
SingleAlert singleAlert = new SingleAlert();
@@ -87,43 +89,31 @@ class ServerChanAlertNotifyHandlerImplTest {
template.setName("test-template");
template.setContent("test content");
when(alerterProperties.getServerChanWebhookUrl()).thenReturn("http://test.url/");
when(bundle.getString("alerter.notify.title")).thenReturn("Alert Notification");
lenient().when(alerterProperties.getServerChanWebhookUrl())
.thenReturn("https://api.serverchan.com/send/%s");
lenient().when(bundle.getString("alerter.notify.title")).thenReturn("Alert Notification");
}
@Test
public void testNotifyAlertSuccess() {
CommonRobotNotifyResp successResp = new CommonRobotNotifyResp();
successResp.setErrCode(0);
successResp.setMsg("success");
ResponseEntity<CommonRobotNotifyResp> responseEntity =
new ResponseEntity<>(successResp, HttpStatus.OK);
ResponseEntity<CommonRobotNotifyResp> responseEntity = new ResponseEntity<>(successResp, HttpStatus.OK);
when(restTemplate.postForEntity(
any(String.class),
any(),
eq(CommonRobotNotifyResp.class)
)).thenReturn(responseEntity);
eq(CommonRobotNotifyResp.class))).thenReturn(responseEntity);
serverChanAlertNotifyHandler.send(receiver, template, groupAlert);
}
@Test
public void testNotifyAlertFailure() {
CommonRobotNotifyResp failResp = new CommonRobotNotifyResp();
failResp.setCode(1);
failResp.setErrMsg("Test Error");
ResponseEntity<CommonRobotNotifyResp> responseEntity =
new ResponseEntity<>(failResp, HttpStatus.BAD_REQUEST);
when(restTemplate.postForEntity(
any(String.class),
any(),
eq(CommonRobotNotifyResp.class)
)).thenReturn(responseEntity);
public void testNotifyAlertWithInvalidUrl() {
when(alerterProperties.getServerChanWebhookUrl()).thenReturn("http://invalid-url.com/%s");
assertThrows(AlertNoticeException.class,
assertThrows(AlertNoticeException.class,
() -> serverChanAlertNotifyHandler.send(receiver, template, groupAlert));
}
}
@@ -51,7 +51,7 @@ class SlackAlertNotifyHandlerImplTest {
@Mock
private RestTemplate restTemplate;
@Mock
private ResourceBundle bundle;
@@ -68,18 +68,18 @@ class SlackAlertNotifyHandlerImplTest {
receiver.setId(1L);
receiver.setName("test-receiver");
receiver.setAccessToken("test-token");
receiver.setSlackWebHookUrl("http://localhost:8080");
receiver.setSlackWebHookUrl("https://hooks.slack.com/services/ABCDEF/GHIJKL/mnopqrstuvwxyz");
groupAlert = new GroupAlert();
SingleAlert singleAlert = new SingleAlert();
singleAlert.setLabels(new HashMap<>());
singleAlert.getLabels().put("severity", "critical");
singleAlert.getLabels().put("alertname", "Test Alert");
List<SingleAlert> alerts = new ArrayList<>();
alerts.add(singleAlert);
groupAlert.setAlerts(alerts);
template = new NoticeTemplate();
template.setId(1L);
template.setName("test-template");
@@ -90,22 +90,18 @@ class SlackAlertNotifyHandlerImplTest {
@Test
public void testNotifyAlertSuccess() {
ResponseEntity<String> responseEntity =
new ResponseEntity<>("ok", HttpStatus.OK);
ResponseEntity<String> responseEntity = new ResponseEntity<>("ok", HttpStatus.OK);
when(restTemplate.postForEntity(any(String.class), any(), eq(String.class))).thenReturn(responseEntity);
slackAlertNotifyHandler.send(receiver, template, groupAlert);
}
@Test
public void testNotifyAlertFailure() {
ResponseEntity<String> responseEntity =
new ResponseEntity<>("invalid_payload", HttpStatus.BAD_REQUEST);
public void testNotifyAlertWithInvalidUrl() {
receiver.setSlackWebHookUrl("http://localhost:8080");
when(restTemplate.postForEntity(any(String.class), any(), eq(String.class))).thenReturn(responseEntity);
assertThrows(AlertNoticeException.class,
assertThrows(AlertNoticeException.class,
() -> slackAlertNotifyHandler.send(receiver, template, groupAlert));
}
}
@@ -20,6 +20,7 @@ package org.apache.hertzbeat.alert.notice.impl;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.when;
import org.apache.hertzbeat.alert.AlerterProperties;
@@ -51,10 +52,10 @@ class TelegramBotAlertNotifyHandlerImplTest {
@Mock
private RestTemplate restTemplate;
@Mock
private AlerterProperties alerterProperties;
@Mock
private ResourceBundle bundle;
@@ -70,58 +71,57 @@ class TelegramBotAlertNotifyHandlerImplTest {
receiver = new NoticeReceiver();
receiver.setId(1L);
receiver.setName("test-receiver");
receiver.setAccessToken("test-token");
receiver.setTgBotToken("123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11");
receiver.setTgUserId("123456789"); // Telegram specific - chat ID
groupAlert = new GroupAlert();
SingleAlert singleAlert = new SingleAlert();
singleAlert.setLabels(new HashMap<>());
singleAlert.getLabels().put("severity", "critical");
singleAlert.getLabels().put("alertname", "Test Alert");
List<SingleAlert> alerts = new ArrayList<>();
alerts.add(singleAlert);
groupAlert.setAlerts(alerts);
template = new NoticeTemplate();
template.setId(1L);
template.setName("test-template");
template.setContent("test content");
when(alerterProperties.getTelegramWebhookUrl()).thenReturn("https://api.telegram.org/bot%s/sendMessage");
when(bundle.getString("alerter.notify.title")).thenReturn("Alert Notification");
lenient().when(alerterProperties.getTelegramWebhookUrl())
.thenReturn("https://api.telegram.org/bot%s/sendMessage");
lenient().when(bundle.getString("alerter.notify.title")).thenReturn("Alert Notification");
}
@Test
public void testNotifyAlertSuccess() {
TelegramBotAlertNotifyHandlerImpl.TelegramBotNotifyResponse successResp =
new TelegramBotAlertNotifyHandlerImpl.TelegramBotNotifyResponse();
TelegramBotAlertNotifyHandlerImpl.TelegramBotNotifyResponse successResp = new TelegramBotAlertNotifyHandlerImpl.TelegramBotNotifyResponse();
successResp.setOk(true);
successResp.setDescription("Test Success");
ResponseEntity<TelegramBotAlertNotifyHandlerImpl.TelegramBotNotifyResponse> responseEntity =
new ResponseEntity<>(successResp, HttpStatus.OK);
when(restTemplate.postForEntity(any(String.class), any(),
ResponseEntity<TelegramBotAlertNotifyHandlerImpl.TelegramBotNotifyResponse> responseEntity = new ResponseEntity<>(
successResp, HttpStatus.OK);
when(restTemplate.postForEntity(any(String.class), any(),
eq(TelegramBotAlertNotifyHandlerImpl.TelegramBotNotifyResponse.class))).thenReturn(responseEntity);
telegramBotAlertNotifyHandler.send(receiver, template, groupAlert);
}
@Test
public void testNotifyAlertFailure() {
TelegramBotAlertNotifyHandlerImpl.TelegramBotNotifyResponse successResp =
new TelegramBotAlertNotifyHandlerImpl.TelegramBotNotifyResponse();
successResp.setOk(false);
successResp.setDescription("Test failed");
TelegramBotAlertNotifyHandlerImpl.TelegramBotNotifyResponse failureResp = new TelegramBotAlertNotifyHandlerImpl.TelegramBotNotifyResponse();
failureResp.setOk(false);
failureResp.setDescription("Test failed");
ResponseEntity<TelegramBotAlertNotifyHandlerImpl.TelegramBotNotifyResponse> responseEntity =
new ResponseEntity<>(successResp, HttpStatus.BAD_REQUEST);
ResponseEntity<TelegramBotAlertNotifyHandlerImpl.TelegramBotNotifyResponse> responseEntity = new ResponseEntity<>(
failureResp, HttpStatus.OK);
when(restTemplate.postForEntity(any(String.class), any(),
eq(TelegramBotAlertNotifyHandlerImpl.TelegramBotNotifyResponse.class))).thenReturn(responseEntity);
assertThrows(AlertNoticeException.class,
assertThrows(AlertNoticeException.class,
() -> telegramBotAlertNotifyHandler.send(receiver, template, groupAlert));
}
}
@@ -48,7 +48,7 @@ import java.util.ResourceBundle;
*/
@ExtendWith(MockitoExtension.class)
class WeComRobotAlertNotifyHandlerImplTest {
@Mock
private RestTemplate restTemplate;
@@ -57,20 +57,21 @@ class WeComRobotAlertNotifyHandlerImplTest {
@Mock
private ResourceBundle bundle;
@InjectMocks
private WeComRobotAlertNotifyHandlerImpl weComRobotAlertNotifyHandler;
private NoticeReceiver receiver;
private GroupAlert groupAlert;
private NoticeTemplate template;
@BeforeEach
public void setUp() {
receiver = new NoticeReceiver();
receiver.setId(1L);
receiver.setName("test-receiver");
receiver.setAccessToken("test-token");
receiver.setWechatId("test-wechat-id");
groupAlert = new GroupAlert();
SingleAlert singleAlert = new SingleAlert();
@@ -95,33 +96,29 @@ class WeComRobotAlertNotifyHandlerImplTest {
public void testNotifyAlertSuccess() {
CommonRobotNotifyResp successResp = new CommonRobotNotifyResp();
successResp.setErrCode(0);
ResponseEntity<CommonRobotNotifyResp> responseEntity =
new ResponseEntity<>(successResp, HttpStatus.OK);
ResponseEntity<CommonRobotNotifyResp> responseEntity = new ResponseEntity<>(successResp, HttpStatus.OK);
when(restTemplate.postForEntity(
any(String.class),
any(),
eq(CommonRobotNotifyResp.class)
)).thenReturn(responseEntity);
eq(CommonRobotNotifyResp.class))).thenReturn(responseEntity);
weComRobotAlertNotifyHandler.send(receiver, template, groupAlert);
}
@Test
public void testNotifyAlertFailure() {
CommonRobotNotifyResp failResp = new CommonRobotNotifyResp();
failResp.setCode(1);
failResp.setErrMsg("Test Error");
ResponseEntity<CommonRobotNotifyResp> responseEntity =
new ResponseEntity<>(failResp, HttpStatus.OK);
ResponseEntity<CommonRobotNotifyResp> responseEntity = new ResponseEntity<>(failResp, HttpStatus.OK);
when(restTemplate.postForEntity(
any(String.class),
any(),
eq(CommonRobotNotifyResp.class)
)).thenReturn(responseEntity);
eq(CommonRobotNotifyResp.class))).thenReturn(responseEntity);
assertThrows(AlertNoticeException.class,
assertThrows(AlertNoticeException.class,
() -> weComRobotAlertNotifyHandler.send(receiver, template, groupAlert));
}
}
@@ -69,8 +69,8 @@
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>provided</scope>
</dependency>
<!-- clickhouse -->
@@ -164,5 +164,11 @@
<groupId>org.apache.sshd</groupId>
<artifactId>sshd-sftp</artifactId>
</dependency>
<!-- Zookeeper -->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>${zookeeper.version}</version>
</dependency>
</dependencies>
</project>
@@ -66,7 +66,7 @@ public class JmxCollectImpl extends AbstractCollect {
private static final String JMX_URL_PREFIX = "service:jmx:rmi:///jndi/rmi://";
private static final String JMX_URL_SUFFIX = "/jmxrmi";
private static final String IGNORED_STUB = "/stub/";
private static final String SUB_ATTRIBUTE = "->";
@@ -75,7 +75,6 @@ public class JmxCollectImpl extends AbstractCollect {
private final ClassLoader jmxClassLoader;
public JmxCollectImpl() {
jmxClassLoader = new JmxClassLoader(ClassLoader.getSystemClassLoader());
}
@@ -83,13 +82,66 @@ public class JmxCollectImpl extends AbstractCollect {
@Override
public void preCheck(Metrics metrics) throws IllegalArgumentException {
Assert.isTrue(metrics != null && metrics.getJmx() != null, "JMX collect must have JMX params");
JmxProtocol jmxProtocol = metrics.getJmx();
String url = metrics.getJmx().getUrl();
// Validate JMX URL if provided
String url = jmxProtocol.getUrl();
if (StringUtils.hasText(url)) {
Assert.doesNotContain(url, IGNORED_STUB, "JMX url prohibit contains stub, please check");
// Prevent JNDI injection by validating URL format
validateJmxUrl(url);
} else {
// Validate host and port inputs
String host = jmxProtocol.getHost();
int port = Integer.parseInt(jmxProtocol.getPort());
// Validate host format (only allow valid hostnames or IP addresses)
Assert.isTrue(isValidHostname(host), "Invalid hostname format");
Assert.isTrue(port > 0 && port <= 65535, "Port must be between 1 and 65535");
}
}
/**
* Validate JMX URL
*
* @param url JMX URL to validate
* @throws IllegalArgumentException if URL is potentially malicious
*/
private void validateJmxUrl(String url) throws IllegalArgumentException {
// Only allow service:jmx:rmi protocol
Assert.isTrue(url.startsWith("service:jmx:rmi:"), "Only service:jmx:rmi protocol is supported");
String[] disallowedPatterns = { "ldap:", "rmi:", "iiop:", "nis:", "dns:", "corbaname:", "http:", "https:" };
for (String pattern : disallowedPatterns) {
if (url.contains(pattern) && !pattern.equals("rmi:///jndi/rmi:")) {
throw new IllegalArgumentException("Potentially unsafe JNDI protocol detected in URL: " + pattern);
}
}
// Check for suspicious patterns
if (url.contains("${") || url.contains("$[") || url.contains(":#") || url.contains(":/")) {
throw new IllegalArgumentException("Potentially malicious pattern detected in JMX URL");
}
}
/**
* Validate hostname format
*
* @param hostname Hostname to validate
* @return true if hostname is valid
*/
private boolean isValidHostname(String hostname) {
if (hostname == null || hostname.isEmpty()) {
return false;
}
// Simplified hostname/IP validation regex
// This regex accepts valid hostnames, IPv4 and IPv6 addresses
String hostnameRegex = "^([a-zA-Z0-9][-a-zA-Z0-9]*\\.)+[a-zA-Z0-9][-a-zA-Z0-9]*$|^(\\d{1,3}\\.){3}\\d{1,3}$|^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$";
return hostname.matches(hostnameRegex);
}
@Override
public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) {
ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
@@ -172,16 +224,18 @@ public class JmxCollectImpl extends AbstractCollect {
log.info("attribute {} value is null.", attribute.getName());
continue;
}
if (value instanceof Number || value instanceof String || value instanceof ObjectName
if (value instanceof Number || value instanceof String || value instanceof ObjectName
|| value instanceof Boolean || value instanceof Date || value instanceof TimeUnit) {
attributeValueMap.put(attribute.getName(), value.toString());
} else if (value instanceof CompositeData compositeData) {
} else if (value instanceof CompositeData) {
CompositeData compositeData = (CompositeData) value;
CompositeType compositeType = compositeData.getCompositeType();
for (String typeKey : compositeType.keySet()) {
Object fieldValue = compositeData.get(typeKey);
attributeValueMap.put(attribute.getName() + SUB_ATTRIBUTE + typeKey, fieldValue.toString());
}
} else if (value instanceof String[] values) {
} else if (value instanceof String[]) {
String[] values = (String[]) value;
StringBuilder builder = new StringBuilder();
for (int index = 0; index < values.length; index++) {
builder.append(values[index]);
@@ -219,12 +273,33 @@ public class JmxCollectImpl extends AbstractCollect {
String url;
if (jmxProtocol.getUrl() != null) {
url = jmxProtocol.getUrl();
// Double check URL format for security
if (!url.startsWith("service:jmx:rmi:")) {
throw new IOException("Unsupported JMX URL protocol. Only service:jmx:rmi: is allowed.");
}
} else {
url = JMX_URL_PREFIX + jmxProtocol.getHost() + ":" + jmxProtocol.getPort() + JMX_URL_SUFFIX;
// More strict formatting with proper escaping
String host = jmxProtocol.getHost();
int port = Integer.parseInt(jmxProtocol.getPort());
// Additional validation at connection time
if (!isValidHostname(host)) {
throw new IOException("Invalid hostname format for JMX connection: " + host);
}
if (port <= 0 || port > 65535) {
throw new IOException("Invalid port for JMX connection: " + port);
}
url = JMX_URL_PREFIX + host + ":" + port + JMX_URL_SUFFIX;
}
// Set security properties to prevent remote class loading
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "false");
System.setProperty("com.sun.jndi.cosnaming.object.trustURLCodebase", "false");
Map<String, Object> environment = new HashMap<>(4);
if (StringUtils.hasText(jmxProtocol.getUsername()) && StringUtils.hasText(jmxProtocol.getPassword())) {
String[] credential = new String[] {jmxProtocol.getUsername(), jmxProtocol.getPassword()};
String[] credential = new String[] { jmxProtocol.getUsername(), jmxProtocol.getPassword() };
environment.put(javax.management.remote.JMXConnector.CREDENTIALS, credential);
}
if (Boolean.TRUE.toString().equals(jmxProtocol.getSsl())) {
@@ -233,10 +308,20 @@ public class JmxCollectImpl extends AbstractCollect {
environment.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, clientSocketFactory);
environment.put("com.sun.jndi.rmi.factory.socket", clientSocketFactory);
}
JMXServiceURL jmxServiceUrl = new JMXServiceURL(url);
conn = JMXConnectorFactory.connect(jmxServiceUrl, environment);
connectionCommonCache.addCache(identifier, new JmxConnect(conn));
return conn;
// Limit JMX connection timeout
environment.put("jmx.remote.x.client.connection.timeout", 10000);
environment.put("jmx.remote.x.server.connection.timeout", 10000);
try {
JMXServiceURL jmxServiceUrl = new JMXServiceURL(url);
conn = JMXConnectorFactory.connect(jmxServiceUrl, environment);
connectionCommonCache.addCache(identifier, new JmxConnect(conn));
return conn;
} catch (Exception e) {
log.error("Failed to connect to JMX server: {}", e.getMessage());
throw new IOException("Failed to connect to JMX server: " + e.getMessage(), e);
}
}
}
@@ -179,7 +179,7 @@ public class PrometheusAutoCollectImpl {
builder.addField(CollectRep.Field.newBuilder().setName(label.getName())
.setType(CommonConstants.TYPE_STRING).setLabel(true).build());
});
builder.addField(CollectRep.Field.newBuilder().setName("value")
builder.addField(CollectRep.Field.newBuilder().setName("metric_value")
.setType(CommonConstants.TYPE_NUMBER).setLabel(false).build());
}
Map<String, String> labelMap = metric.getLabels()
@@ -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.collector.collect.sd;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.collect.AbstractCollect;
import org.apache.hertzbeat.collector.collect.registry.constant.DiscoveryClientInstance;
import org.apache.hertzbeat.collector.collect.registry.discovery.DiscoveryClient;
import org.apache.hertzbeat.collector.collect.registry.discovery.DiscoveryClientManagement;
import org.apache.hertzbeat.collector.collect.registry.discovery.entity.ServiceInstance;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.job.protocol.ConsulSdProtocol;
import org.apache.hertzbeat.common.entity.job.protocol.RegistryProtocol;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.springframework.util.CollectionUtils;
import java.util.List;
/**
* consul sd collector
*/
@Slf4j
public class ConsulSdCollectImpl extends AbstractCollect {
private static final DiscoveryClientManagement discoveryClientManagement = new DiscoveryClientManagement();
@Override
public void preCheck(Metrics metrics) throws IllegalArgumentException {
ConsulSdProtocol consulSd = metrics.getConsul_sd();
if (consulSd == null || consulSd.isInvalid()) {
throw new IllegalArgumentException("Consul Service Discovery params is required.");
}
}
@Override
public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) {
RegistryProtocol registryProtocol = RegistryProtocol.builder()
.host(metrics.getConsul_sd().getHost())
.port(metrics.getConsul_sd().getPort())
.discoveryClientTypeName(DiscoveryClientInstance.CONSUL.name())
.build();
try (DiscoveryClient client = discoveryClientManagement.getClient(registryProtocol)){
List<ServiceInstance> services = client.getServices();
if (CollectionUtils.isEmpty(services)) {
return;
}
services.forEach(instance -> {
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
valueRowBuilder.addColumn(instance.getAddress());
valueRowBuilder.addColumn(String.valueOf(instance.getPort()));
builder.addValueRow(valueRowBuilder.build());
});
} catch (Exception e) {
String errorMsg = CommonUtil.getMessageFromThrowable(e);
log.warn("Failed to fetch consul sd... {}", errorMsg);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
}
}
@Override
public String supportProtocol() {
return DispatchConstants.PROTOCOL_CONSUL_SD;
}
}
@@ -0,0 +1,181 @@
/*
* 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.sd;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.collect.AbstractCollect;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.xbill.DNS.AAAARecord;
import org.xbill.DNS.ARecord;
import org.xbill.DNS.Lookup;
import org.xbill.DNS.MXRecord;
import org.xbill.DNS.NSRecord;
import org.xbill.DNS.Record;
import org.xbill.DNS.SRVRecord;
import org.xbill.DNS.SimpleResolver;
import org.xbill.DNS.TextParseException;
import org.xbill.DNS.Type;
import java.time.Duration;
import java.util.Arrays;
/**
* DNS SD collector supporting multiple record types
*/
@Slf4j
public class DnsSdCollectImpl extends AbstractCollect {
private static final int DEFAULT_TIME_OUT = 3;
@Override
public void preCheck(Metrics metrics) throws IllegalArgumentException {
if (metrics.getDns_sd() == null) {
throw new IllegalArgumentException("DNS SD configuration cannot be null");
}
if (metrics.getDns_sd().getHost() == null || metrics.getDns_sd().getHost().isEmpty()) {
throw new IllegalArgumentException("DNS host cannot be null or empty");
}
if (metrics.getDns_sd().getPort() == null || metrics.getDns_sd().getPort().isEmpty()) {
throw new IllegalArgumentException("DNS port cannot be null or empty");
}
if (metrics.getDns_sd().getRecordType() == null || metrics.getDns_sd().getRecordType().isEmpty()) {
throw new IllegalArgumentException("DNS record type cannot be null or empty");
}
if (metrics.getDns_sd().getRecordName() == null || metrics.getDns_sd().getRecordName().isEmpty()) {
throw new IllegalArgumentException("DNS record name cannot be null or empty");
}
}
@Override
public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) {
String hostName = metrics.getDns_sd().getHost();
int type = Integer.parseInt(metrics.getDns_sd().getRecordType());
Type.check(type);
String recordName = metrics.getDns_sd().getRecordName();
try {
Lookup lookup = new Lookup(recordName, type);
SimpleResolver resolver = new SimpleResolver(metrics.getDns_sd().getHost());
resolver.setPort(Integer.parseInt(metrics.getDns_sd().getPort()));
resolver.setTimeout(Duration.ofMillis(DEFAULT_TIME_OUT));
lookup.setResolver(resolver);
lookup.setCache(null);
lookup.run();
if (lookup.getResult() != Lookup.SUCCESSFUL) {
String msg = String.format("DNS lookup failed for: %s, error: %s", recordName, lookup.getErrorString());
log.warn(msg);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(msg);
return;
}
Record[] records = lookup.getAnswers();
if (records == null || records.length == 0) {
log.info("No record type: {} records found for host: {}", type, hostName);
builder.setCode(CollectRep.Code.SUCCESS);
return;
}
processRecords(builder, records, type);
} catch (TextParseException e) {
String errorMsg = CommonUtil.getMessageFromThrowable(e);
log.warn("Failed to parse dns query... {}", errorMsg);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
} catch (Exception e) {
String errorMsg = CommonUtil.getMessageFromThrowable(e);
log.error("Failed to fetch dns sd...{}", errorMsg);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
}
}
private void processRecords(CollectRep.MetricsData.Builder builder, Record[] records, int recordType) {
switch (recordType) {
case Type.A:
processARecords(builder, records);
break;
case Type.AAAA:
processAAAARecords(builder, records);
break;
case Type.SRV:
processSrvRecords(builder, records);
break;
case Type.MX:
processMxRecords(builder, records);
break;
case Type.NS:
processNsRecords(builder, records);
break;
default:
throw new IllegalStateException("Invalid record type: " + recordType);
}
}
@SuppressWarnings({"checkstyle:AbbreviationAsWordInName", "checkstyle:LambdaParameterName"})
private void processARecords(CollectRep.MetricsData.Builder builder, Record[] records) {
Arrays.stream(records).filter(ARecord.class::isInstance).map(ARecord.class::cast).forEach(aRecord -> {
CollectRep.ValueRow.Builder row = CollectRep.ValueRow.newBuilder();
row.addColumn(aRecord.getAddress().getHostAddress());
row.addColumn(""); //A record has no port
builder.addValueRow(row.build());
});
}
@SuppressWarnings("checkstyle:AbbreviationAsWordInName")
private void processAAAARecords(CollectRep.MetricsData.Builder builder, Record[] records) {
Arrays.stream(records).filter(AAAARecord.class::isInstance).map(AAAARecord.class::cast).forEach(aaaaRecord -> {
CollectRep.ValueRow.Builder row = CollectRep.ValueRow.newBuilder();
row.addColumn(aaaaRecord.getAddress().getHostAddress());
row.addColumn(""); //AAAA record has no port
builder.addValueRow(row.build());
});
}
private void processSrvRecords(CollectRep.MetricsData.Builder builder, Record[] records) {
Arrays.stream(records).filter(SRVRecord.class::isInstance).map(SRVRecord.class::cast).forEach(srvRecord -> {
CollectRep.ValueRow.Builder row = CollectRep.ValueRow.newBuilder();
row.addColumn(srvRecord.getTarget().toString(true));
row.addColumn(String.valueOf(srvRecord.getPort()));
builder.addValueRow(row.build());
});
}
private void processMxRecords(CollectRep.MetricsData.Builder builder, Record[] records) {
Arrays.stream(records).filter(MXRecord.class::isInstance).map(MXRecord.class::cast).forEach(mxRecord -> {
CollectRep.ValueRow.Builder row = CollectRep.ValueRow.newBuilder();
row.addColumn(mxRecord.getTarget().toString(true));
row.addColumn(""); //MX record has no port
builder.addValueRow(row.build());
});
}
private void processNsRecords(CollectRep.MetricsData.Builder builder, Record[] records) {
Arrays.stream(records).filter(NSRecord.class::isInstance).map(NSRecord.class::cast).forEach(nsRecord -> {
CollectRep.ValueRow.Builder row = CollectRep.ValueRow.newBuilder();
row.addColumn(nsRecord.getTarget().toString(true));
row.addColumn(""); //NS record has no port
builder.addValueRow(row.build());
});
}
@Override
public String supportProtocol() {
return DispatchConstants.PROTOCOL_DNS_SD;
}
}
@@ -0,0 +1,111 @@
/*
* 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.sd;
import com.google.common.collect.Lists;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.collect.AbstractCollect;
import org.apache.hertzbeat.collector.collect.common.http.CommonHttpClient;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.job.protocol.EurekaSdProtocol;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.entity.sd.ConnectionConfig;
import org.apache.hertzbeat.common.entity.sd.EurekaDiscoveryResponseEntity;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.hertzbeat.common.util.XmlUtil;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.util.EntityUtils;
import org.springframework.util.CollectionUtils;
/**
* eureka service discovery collect impl
*/
@Slf4j
public class EurekaSdCollectImpl extends AbstractCollect {
private static final String APP_LIST_PATH = "/apps";
@Override
public void preCheck(Metrics metrics) throws IllegalArgumentException {
EurekaSdProtocol eurekaSd = metrics.getEureka_sd();
if (eurekaSd == null || eurekaSd.getUrl() == null) {
throw new IllegalArgumentException("Eureka Service Discovery url is required.");
}
}
@Override
public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) {
List<ConnectionConfig> configList = Lists.newArrayList();
HttpUriRequest request = RequestBuilder.get().setUri(metrics.getEureka_sd().getUrl() + APP_LIST_PATH).build();
try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(request)) {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != 200) {
log.warn("Failed to fetch eureka sd...");
builder.setMsg("StatusCode " + statusCode);
builder.setCode(CollectRep.Code.FAIL);
return;
}
String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
EurekaDiscoveryResponseEntity responseEntity =
XmlUtil.fromXml(responseBody, EurekaDiscoveryResponseEntity.class);
if (responseEntity == null || CollectionUtils.isEmpty(responseEntity.getApplications())) {
return;
}
responseEntity.getApplications()
.stream()
.filter(application -> !CollectionUtils.isEmpty(application.getInstances()))
.forEach(application -> convertTarget(configList, application));
configList.forEach(config -> {
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
valueRowBuilder.addColumn(config.getHost());
valueRowBuilder.addColumn(config.getPort());
builder.addValueRow(valueRowBuilder.build());
});
} catch (IOException e) {
String errorMsg = CommonUtil.getMessageFromThrowable(e);
log.warn("Failed to fetch eureka sd... {}", errorMsg);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
}
}
private void convertTarget(List<ConnectionConfig> configList,
EurekaDiscoveryResponseEntity.Application application) {
application.getInstances().forEach(instance -> {
String host = instance.getIpAddr();
int port = instance.getPort() == null ? 80 : instance.getPort();
configList.add(ConnectionConfig.builder()
.host(host)
.port(String.valueOf(port))
.build());
});
}
@Override
public String supportProtocol() {
return DispatchConstants.PROTOCOL_EUREKA_SD;
}
}
@@ -20,24 +20,42 @@ package org.apache.hertzbeat.collector.collect.sd;
import com.fasterxml.jackson.core.type.TypeReference;
import com.google.common.collect.Lists;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.collector.collect.AbstractCollect;
import org.apache.hertzbeat.collector.collect.common.http.CommonHttpClient;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
import org.apache.hertzbeat.collector.util.CollectUtil;
import org.apache.hertzbeat.common.constants.NetworkConstants;
import org.apache.hertzbeat.common.constants.SignConstants;
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.entity.sd.ConnectionConfig;
import org.apache.hertzbeat.common.entity.sd.ServiceDiscoveryResponseEntity;
import org.apache.hertzbeat.common.util.Base64Util;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.http.HttpHeaders;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
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.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.springframework.http.MediaType;
import org.springframework.util.CollectionUtils;
import org.springframework.web.util.UriUtils;
/**
@@ -52,9 +70,10 @@ public class HttpSdCollectImpl extends AbstractCollect {
@Override
public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) {
List<ConnectionConfig> configList = Lists.newArrayList();
HttpUriRequest request = RequestBuilder.get().setUri(metrics.getSdProtocol().getSdSource()).build();
try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(request)) {
HttpUriRequest request = createHttpRequest(metrics.getHttp_sd());
HttpContext httpContext = createHttpContext(metrics.getHttp_sd());
try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(request, httpContext)) {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != 200) {
log.warn("Failed to fetch sd...");
@@ -113,4 +132,111 @@ public class HttpSdCollectImpl extends AbstractCollect {
public String supportProtocol() {
return DispatchConstants.PROTOCOL_HTTP_SD;
}
/**
* create httpContext
*
* @param httpSdProtocol http sd protocol
* @return context
*/
public HttpContext createHttpContext(HttpProtocol httpSdProtocol) {
HttpProtocol.Authorization auth = httpSdProtocol.getAuthorization();
if (auth != null && DispatchConstants.DIGEST_AUTH.equals(auth.getType())) {
HttpClientContext clientContext = new HttpClientContext();
if (org.springframework.util.StringUtils.hasText(auth.getDigestAuthUsername())
&& org.springframework.util.StringUtils.hasText(auth.getDigestAuthPassword())) {
CredentialsProvider provider = new BasicCredentialsProvider();
UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(auth.getDigestAuthUsername(),
auth.getDigestAuthPassword());
URL url;
try {
url = new URL(httpSdProtocol.getUrl());
} catch (MalformedURLException e) {
throw new IllegalArgumentException("Invalid URI format.", e);
}
int port = url.getPort() != -1 ? url.getPort() : ("https".equals(url.getProtocol()) ? 443 : 80);
AuthScope authScope = new AuthScope(url.getHost(), port);
provider.setCredentials(authScope, credentials);
clientContext.setCredentialsProvider(provider);
return clientContext;
}
}
return null;
}
/**
* create http request
*
* @param httpSdProtocol http request set
* @return http uri request
*/
public HttpUriRequest createHttpRequest(HttpProtocol httpSdProtocol) {
RequestBuilder requestBuilder = RequestBuilder.get();
// The default request header can be overridden if customized
// keep-alive
requestBuilder.addHeader(HttpHeaders.CONNECTION, NetworkConstants.KEEP_ALIVE);
requestBuilder.addHeader(HttpHeaders.USER_AGENT, NetworkConstants.USER_AGENT);
// headers The custom request header is overwritten here
Map<String, String> headers = httpSdProtocol.getHeaders();
if (headers != null && !headers.isEmpty()) {
for (Map.Entry<String, String> header : headers.entrySet()) {
if (org.springframework.util.StringUtils.hasText(header.getValue())) {
requestBuilder.addHeader(header.getKey(), header.getValue());
}
}
}
// add accept
requestBuilder.addHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
// add authorization
if (httpSdProtocol.getAuthorization() != null) {
HttpProtocol.Authorization authorization = httpSdProtocol.getAuthorization();
if (DispatchConstants.BEARER_TOKEN.equalsIgnoreCase(authorization.getType())) {
String value = DispatchConstants.BEARER + SignConstants.BLANK + authorization.getBearerTokenToken();
requestBuilder.addHeader(HttpHeaders.AUTHORIZATION, value);
} else if (DispatchConstants.BASIC_AUTH.equals(authorization.getType())) {
if (org.springframework.util.StringUtils.hasText(authorization.getBasicAuthUsername())
&& org.springframework.util.StringUtils.hasText(authorization.getBasicAuthPassword())) {
String authStr = authorization.getBasicAuthUsername() + SignConstants.DOUBLE_MARK + authorization.getBasicAuthPassword();
String encodedAuth = Base64Util.encode(authStr);
requestBuilder.addHeader(HttpHeaders.AUTHORIZATION, DispatchConstants.BASIC + SignConstants.BLANK + encodedAuth);
}
}
}
// uri encode, default true
boolean enableUrlEncoding = Boolean.parseBoolean(httpSdProtocol.getEnableUrlEncoding());
if (enableUrlEncoding) {
// if the url contains parameters directly
if (httpSdProtocol.getUrl().contains("?")) {
String path = httpSdProtocol.getUrl().substring(0, httpSdProtocol.getUrl().indexOf("?"));
String query = httpSdProtocol.getUrl().substring(httpSdProtocol.getUrl().indexOf("?") + 1);
httpSdProtocol.setUrl(UriUtils.encodePath(path, "UTF-8") + "?" + UriUtils.encodeQuery(query, "UTF-8"));
} else {
httpSdProtocol.setUrl(UriUtils.encodePath(httpSdProtocol.getUrl(), "UTF-8"));
}
}
// set uri
try {
requestBuilder.setUri(httpSdProtocol.getUrl());
} catch (IllegalArgumentException e) {
log.warn("Invalid URI with illegal characters: {}. User has disabled URL encoding, not applying any encoding.", httpSdProtocol.getUrl());
throw e;
}
// custom timeout
int timeout = CollectUtil.getTimeout(httpSdProtocol.getTimeout(), 0);
if (timeout > 0) {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(timeout)
.setSocketTimeout(timeout)
.setRedirectsEnabled(true)
.build();
requestBuilder.setConfig(requestConfig);
}
return requestBuilder.build();
}
}
@@ -0,0 +1,114 @@
/*
* 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.sd;
import java.util.List;
import org.apache.hertzbeat.collector.collect.AbstractCollect;
import org.apache.hertzbeat.collector.collect.registry.constant.DiscoveryClientInstance;
import org.apache.hertzbeat.collector.collect.registry.discovery.DiscoveryClient;
import org.apache.hertzbeat.collector.collect.registry.discovery.DiscoveryClientManagement;
import org.apache.hertzbeat.collector.collect.registry.discovery.entity.ServiceInstance;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.job.protocol.RegistryProtocol;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.springframework.util.CollectionUtils;
import lombok.extern.slf4j.Slf4j;
/**
* Nacos Service Discovery Collector
* This collector integrates with Nacos to discover registered services
*/
@Slf4j
public class NacosSdCollectImpl extends AbstractCollect {
/**
* Client management to interact with discovery services
*/
private final DiscoveryClientManagement discoveryClientManagement = new DiscoveryClientManagement();
@Override
public void preCheck(Metrics metrics) throws IllegalArgumentException {
// Validate the required configuration is present
if (metrics == null || metrics.getNacos_sd() == null) {
throw new IllegalArgumentException("Nacos service discovery monitoring, the config is null");
}
if (metrics.getNacos_sd().isInvalid()) {
throw new IllegalArgumentException("Nacos service discovery monitoring, the config is invalid");
}
}
@Override
public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) {
// Create Registry protocol from NacosSd protocol
RegistryProtocol registryProtocol = RegistryProtocol.builder()
.host(metrics.getNacos_sd().getHost())
.port(metrics.getNacos_sd().getPort())
.discoveryClientTypeName(DiscoveryClientInstance.NACOS.name())
.build();
DiscoveryClient discoveryClient = null;
try {
// Use the existing NacosDiscoveryClient through DiscoveryClientManagement
discoveryClient = discoveryClientManagement.getClient(registryProtocol);
if (discoveryClient == null) {
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg("Failed to get Nacos discovery client");
return;
}
// Get all services registered in Nacos
List<ServiceInstance> services = discoveryClient.getServices();
if (CollectionUtils.isEmpty(services)) {
return;
}
// Populate the response data with service information
services.forEach(service -> {
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
valueRowBuilder.addColumn(service.getAddress());
valueRowBuilder.addColumn(String.valueOf(service.getPort()));
valueRowBuilder.addColumn(service.getServiceName());
valueRowBuilder.addColumn(service.getHealthStatus());
builder.addValueRow(valueRowBuilder.build());
});
} catch (Exception e) {
String errorMsg = CommonUtil.getMessageFromThrowable(e);
log.warn("Failed to fetch services from Nacos: {}", errorMsg);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
} finally {
// Close the discovery client to release resources
if (discoveryClient != null) {
try {
discoveryClient.close();
} catch (Exception e) {
log.warn("Failed to close Nacos discovery client: {}", CommonUtil.getMessageFromThrowable(e));
}
}
}
}
@Override
public String supportProtocol() {
return DispatchConstants.PROTOCOL_NACOS_SD;
}
}
@@ -0,0 +1,95 @@
/*
* 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.sd;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.collect.AbstractCollect;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.entity.job.protocol.ZookeeperSdProtocol;
import org.apache.hertzbeat.common.entity.sd.ConnectionConfig;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooKeeper;
import org.apache.hertzbeat.common.util.CommonUtil;
import java.io.IOException;
import java.util.List;
/**
* zookeeper sd collector
*/
@Slf4j
public class ZookeeperSdCollectImpl extends AbstractCollect {
private static int TIMEOUT = 30000;
@Override
public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) {
ZookeeperSdProtocol zookeeperSdProtocol = metrics.getZookeeper_sd();
String url = zookeeperSdProtocol.getUrl();
String pathPrefix = zookeeperSdProtocol.getPathPrefix();
try (ZooKeeper zk = new ZooKeeper(url, TIMEOUT, event -> {})){
List<String> children = zk.getChildren(pathPrefix, false);
List<ConnectionConfig> connectionConfigs = children.stream().map(node -> {
String[] split = node.split(":");
if (split.length != 2) log.warn("Node format is incorrect: {}, expected format is 'host:port'", node);
return new ConnectionConfig(split[0], split[1]);
}).toList();
connectionConfigs.forEach(config -> {
CollectRep.ValueRow valueRow = CollectRep.ValueRow.newBuilder()
.addColumn(config.getHost())
.addColumn(config.getPort())
.build();
builder.addValueRow(valueRow);
});
} catch (IOException e){
String errorMsg = CommonUtil.getMessageFromThrowable(e);
log.error("Failed to connect to Zookeeper: {}", errorMsg);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
} catch (InterruptedException e) {
String errorMsg = CommonUtil.getMessageFromThrowable(e);
log.error("Zookeeper connection interrupted: {}", errorMsg);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
} catch (KeeperException e) {
String errorMsg = CommonUtil.getMessageFromThrowable(e);
log.error("Zookeeper operation failed: {}", errorMsg);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
}
}
@Override
public void preCheck(Metrics metrics) throws IllegalArgumentException {
if (metrics.getZookeeper_sd() == null) {
throw new IllegalArgumentException("Zookeeper SD configuration cannot be null");
}
if (metrics.getZookeeper_sd().getUrl() == null || metrics.getZookeeper_sd().getUrl().isEmpty()) {
throw new IllegalArgumentException("Zookeeper URL cannot be null or empty");
}
if (metrics.getZookeeper_sd().getPathPrefix() == null || metrics.getZookeeper_sd().getPathPrefix().isEmpty()) {
throw new IllegalArgumentException("Zookeeper path prefix cannot be null or empty");
}
}
@Override
public String supportProtocol() {
return "zookeeper_sd";
}
}
@@ -0,0 +1,200 @@
/*
* 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.sd;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import lombok.SneakyThrows;
import org.apache.hertzbeat.collector.collect.common.http.CommonHttpClient;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
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.entity.sd.ServiceDiscoveryResponseEntity;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.http.HttpEntity;
import org.apache.http.ProtocolVersion;
import org.apache.http.StatusLine;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.message.BasicStatusLine;
import org.apache.http.protocol.HttpContext;
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.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
/**
* Test case for {@link HttpSdCollectImpl}
*/
@ExtendWith(MockitoExtension.class)
class HttpSdCollectImplTest {
@InjectMocks
private HttpSdCollectImpl httpSdCollectImpl;
@Mock
private CloseableHttpClient httpClient;
@Mock
private CloseableHttpResponse httpResponse;
private Metrics metrics;
private HttpProtocol httpProtocol;
private CollectRep.MetricsData.Builder builder;
@BeforeEach
void setUp() {
httpProtocol = HttpProtocol.builder()
.url("http://localhost:8080/services")
.build();
metrics = Metrics.builder()
.http_sd(httpProtocol)
.build();
builder = CollectRep.MetricsData.newBuilder();
}
@Test
void testCollectSuccess() throws IOException {
List<ServiceDiscoveryResponseEntity> responseEntities = new ArrayList<>();
ServiceDiscoveryResponseEntity entity1 = new ServiceDiscoveryResponseEntity();
entity1.setTarget(Arrays.asList("192.168.1.10:8080", "192.168.1.11:8081"));
ServiceDiscoveryResponseEntity entity2 = new ServiceDiscoveryResponseEntity();
entity2.setTarget(Arrays.asList("192.168.1.12:8082"));
responseEntities.add(entity1);
responseEntities.add(entity2);
String jsonResponse = JsonUtil.toJson(responseEntities);
try (MockedStatic<CommonHttpClient> mockedHttpClient = Mockito.mockStatic(CommonHttpClient.class)) {
mockedHttpClient.when(CommonHttpClient::getHttpClient).thenReturn(httpClient);
when(httpClient.execute(any(HttpUriRequest.class), nullable(HttpContext.class))).thenReturn(httpResponse);
StatusLine statusLine = new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK");
when(httpResponse.getStatusLine()).thenReturn(statusLine);
HttpEntity entity = new StringEntity(jsonResponse, ContentType.APPLICATION_JSON);
when(httpResponse.getEntity()).thenReturn(entity);
httpSdCollectImpl.collect(builder, metrics);
assertEquals(3, builder.getValuesCount());
CollectRep.ValueRow firstRow = builder.getValues(0);
assertEquals("192.168.1.10", firstRow.getColumns(0));
assertEquals("8080", firstRow.getColumns(1));
CollectRep.ValueRow secondRow = builder.getValues(1);
assertEquals("192.168.1.11", secondRow.getColumns(0));
assertEquals("8081", secondRow.getColumns(1));
CollectRep.ValueRow thirdRow = builder.getValues(2);
assertEquals("192.168.1.12", thirdRow.getColumns(0));
assertEquals("8082", thirdRow.getColumns(1));
}
}
@Test
void testCollectWithNon200Status() throws IOException {
try (MockedStatic<CommonHttpClient> mockedHttpClient = Mockito.mockStatic(CommonHttpClient.class)) {
mockedHttpClient.when(CommonHttpClient::getHttpClient).thenReturn(httpClient);
when(httpClient.execute(any(HttpUriRequest.class), nullable(HttpContext.class))).thenReturn(httpResponse);
StatusLine statusLine = new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 404, "Not Found");
when(httpResponse.getStatusLine()).thenReturn(statusLine);
httpSdCollectImpl.collect(builder, metrics);
assertEquals(CollectRep.Code.FAIL, builder.getCode());
assertEquals("StatusCode 404", builder.getMsg());
}
}
@SneakyThrows
@Test
void testCollectWithException() {
String exceptionMessage = "Connection refused";
IOException testException = new IOException(exceptionMessage);
try (MockedStatic<CommonHttpClient> mockedHttpClient = Mockito.mockStatic(CommonHttpClient.class);
MockedStatic<CommonUtil> mockedCommonUtil = Mockito.mockStatic(CommonUtil.class)) {
mockedHttpClient.when(CommonHttpClient::getHttpClient).thenReturn(httpClient);
when(httpClient.execute(any(HttpUriRequest.class), nullable(HttpContext.class))).thenThrow(testException);
mockedCommonUtil.when(() -> CommonUtil.getMessageFromThrowable(any(Throwable.class)))
.thenReturn(exceptionMessage);
httpSdCollectImpl.collect(builder, metrics);
assertEquals(CollectRep.Code.FAIL, builder.getCode());
assertEquals(exceptionMessage, builder.getMsg());
}
}
@Test
void testCollectWithEmptyServiceList() throws IOException {
List<ServiceDiscoveryResponseEntity> emptyList = Collections.emptyList();
String jsonResponse = JsonUtil.toJson(emptyList);
try (MockedStatic<CommonHttpClient> mockedHttpClient = Mockito.mockStatic(CommonHttpClient.class)) {
mockedHttpClient.when(CommonHttpClient::getHttpClient).thenReturn(httpClient);
when(httpClient.execute(any(HttpUriRequest.class), nullable(HttpContext.class))).thenReturn(httpResponse);
StatusLine statusLine = new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK");
when(httpResponse.getStatusLine()).thenReturn(statusLine);
HttpEntity entity = new StringEntity(jsonResponse, ContentType.APPLICATION_JSON);
when(httpResponse.getEntity()).thenReturn(entity);
httpSdCollectImpl.collect(builder, metrics);
assertEquals(0, builder.getValuesCount());
}
}
@Test
void testSupportProtocol() {
assertEquals(DispatchConstants.PROTOCOL_HTTP_SD, httpSdCollectImpl.supportProtocol());
}
}
@@ -0,0 +1,233 @@
/*
* 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.sd;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import lombok.SneakyThrows;
import org.apache.hertzbeat.collector.collect.registry.discovery.DiscoveryClient;
import org.apache.hertzbeat.collector.collect.registry.discovery.DiscoveryClientManagement;
import org.apache.hertzbeat.collector.collect.registry.discovery.entity.ServiceInstance;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.job.protocol.NacosSdProtocol;
import org.apache.hertzbeat.common.entity.job.protocol.RegistryProtocol;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.util.CommonUtil;
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.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
/**
* Test case for {@link NacosSdCollectImpl}
*/
@ExtendWith(MockitoExtension.class)
class NacosSdCollectImplTest {
@InjectMocks
private NacosSdCollectImpl nacosSdCollectImpl;
@Mock
private DiscoveryClientManagement discoveryClientManagement;
@Mock
private DiscoveryClient discoveryClient;
private Metrics metrics;
private NacosSdProtocol nacosSdProtocol;
private CollectRep.MetricsData.Builder builder;
@BeforeEach
void setUp() {
nacosSdProtocol = NacosSdProtocol.builder()
.host("localhost")
.port("8848")
.username("nacos")
.password("nacos")
.build();
metrics = Metrics.builder()
.nacos_sd(nacosSdProtocol)
.build();
builder = CollectRep.MetricsData.newBuilder();
ReflectionTestUtils.setField(nacosSdCollectImpl, "discoveryClientManagement", discoveryClientManagement);
}
@Test
void testPreCheckWithNullMetrics() {
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> nacosSdCollectImpl.preCheck(null));
assertEquals("Nacos service discovery monitoring, the config is null", exception.getMessage());
}
@Test
void testPreCheckWithNullNacosSdConfig() {
Metrics metricsWithoutNacosSd = Metrics.builder().build();
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> nacosSdCollectImpl.preCheck(metricsWithoutNacosSd));
assertEquals("Nacos service discovery monitoring, the config is null", exception.getMessage());
}
@Test
void testPreCheckWithInvalidNacosSdConfig() {
NacosSdProtocol invalidNacosSdProtocol = NacosSdProtocol.builder()
.host("")
.port("8848")
.build();
Metrics metricsWithInvalidNacosSd = Metrics.builder()
.nacos_sd(invalidNacosSdProtocol)
.build();
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> nacosSdCollectImpl.preCheck(metricsWithInvalidNacosSd));
assertEquals("Nacos service discovery monitoring, the config is invalid", exception.getMessage());
NacosSdProtocol portInvalidNacosSdProtocol = NacosSdProtocol.builder()
.host("localhost")
.port("")
.build();
Metrics portMetricsWithInvalidNacosSd = Metrics.builder()
.nacos_sd(portInvalidNacosSdProtocol)
.build();
exception = assertThrows(IllegalArgumentException.class,
() -> nacosSdCollectImpl.preCheck(portMetricsWithInvalidNacosSd));
assertEquals("Nacos service discovery monitoring, the config is invalid", exception.getMessage());
}
@Test
void testPreCheckWithValidConfig() {
assertDoesNotThrow(() -> nacosSdCollectImpl.preCheck(metrics));
}
@Test
void testCollectSuccess() throws Exception {
List<ServiceInstance> services = new ArrayList<>();
services.add(ServiceInstance.builder()
.serviceId("service-1")
.serviceName("user-service")
.address("192.168.1.10")
.port(8080)
.healthStatus("UP")
.build());
services.add(ServiceInstance.builder()
.serviceId("service-2")
.serviceName("order-service")
.address("192.168.1.11")
.port(8081)
.healthStatus("UP")
.build());
when(discoveryClientManagement.getClient(any(RegistryProtocol.class))).thenReturn(discoveryClient);
when(discoveryClient.getServices()).thenReturn(services);
nacosSdCollectImpl.collect(builder, metrics);
verify(discoveryClient, times(1)).close();
assertEquals(2, builder.getValuesCount());
CollectRep.ValueRow firstRow = builder.getValues(0);
assertEquals("192.168.1.10", firstRow.getColumns(0));
assertEquals("8080", firstRow.getColumns(1));
assertEquals("user-service", firstRow.getColumns(2));
assertEquals("UP", firstRow.getColumns(3));
CollectRep.ValueRow secondRow = builder.getValues(1);
assertEquals("192.168.1.11", secondRow.getColumns(0));
assertEquals("8081", secondRow.getColumns(1));
assertEquals("order-service", secondRow.getColumns(2));
assertEquals("UP", secondRow.getColumns(3));
}
@Test
void testCollectClientNull() {
when(discoveryClientManagement.getClient(any(RegistryProtocol.class))).thenReturn(null);
nacosSdCollectImpl.collect(builder, metrics);
assertEquals(CollectRep.Code.FAIL, builder.getCode());
assertEquals("Failed to get Nacos discovery client", builder.getMsg());
}
@SneakyThrows
@Test
void testCollectWithException() {
String exceptionMessage = "Connection refused";
Exception testException = new RuntimeException(exceptionMessage);
when(discoveryClientManagement.getClient(any(RegistryProtocol.class))).thenReturn(discoveryClient);
when(discoveryClient.getServices()).thenThrow(testException);
try (MockedStatic<CommonUtil> mockedCommonUtil = Mockito.mockStatic(CommonUtil.class)) {
mockedCommonUtil.when(() -> CommonUtil.getMessageFromThrowable(any(Throwable.class)))
.thenReturn(exceptionMessage);
nacosSdCollectImpl.collect(builder, metrics);
assertEquals(CollectRep.Code.FAIL, builder.getCode());
assertEquals(exceptionMessage, builder.getMsg());
verify(discoveryClient, times(1)).close();
}
}
@Test
void testCollectWithEmptyServiceList() throws Exception {
when(discoveryClientManagement.getClient(any(RegistryProtocol.class))).thenReturn(discoveryClient);
when(discoveryClient.getServices()).thenReturn(Collections.emptyList());
nacosSdCollectImpl.collect(builder, metrics);
assertEquals(0, builder.getValuesCount());
verify(discoveryClient, times(1)).close();
}
@Test
void testSupportProtocol() {
assertEquals(DispatchConstants.PROTOCOL_NACOS_SD, nacosSdCollectImpl.supportProtocol());
}
}
@@ -0,0 +1,113 @@
/*
* 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.sd;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.job.protocol.ZookeeperSdProtocol;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.zookeeper.ZooKeeper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.MockedConstruction;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@ExtendWith(MockitoExtension.class)
class ZookeeperSdCollectImplTest {
private ZookeeperSdCollectImpl zookeeperSdCollect;
private Metrics metrics;
private ZookeeperSdProtocol protocol;
private CollectRep.MetricsData.Builder builder;
@BeforeEach
void setUp() {
zookeeperSdCollect = new ZookeeperSdCollectImpl();
protocol = ZookeeperSdProtocol.builder()
.url("localhost:2181")
.pathPrefix("/services")
.build();
metrics = Metrics.builder()
.zookeeper_sd(protocol)
.build();
builder = CollectRep.MetricsData.newBuilder();
}
@Test
void testPreCheckWithNullConfig() {
Metrics invalid = Metrics.builder().build();
IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
() -> zookeeperSdCollect.preCheck(invalid));
assertEquals("Zookeeper SD configuration cannot be null", e.getMessage());
}
@Test
void testPreCheckWithEmptyUrl() {
ZookeeperSdProtocol badProtocol = ZookeeperSdProtocol.builder().url("").pathPrefix("/path").build();
Metrics m = Metrics.builder().zookeeper_sd(badProtocol).build();
IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
() -> zookeeperSdCollect.preCheck(m));
assertEquals("Zookeeper URL cannot be null or empty", e.getMessage());
}
@Test
void testPreCheckWithEmptyPath() {
ZookeeperSdProtocol badProtocol = ZookeeperSdProtocol.builder().url("host").pathPrefix("").build();
Metrics m = Metrics.builder().zookeeper_sd(badProtocol).build();
IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
() -> zookeeperSdCollect.preCheck(m));
assertEquals("Zookeeper path prefix cannot be null or empty", e.getMessage());
}
@Test
void testPreCheckValid() {
assertDoesNotThrow(() -> zookeeperSdCollect.preCheck(metrics));
}
@Test
void testSupportProtocol() {
assertEquals("zookeeper_sd", zookeeperSdCollect.supportProtocol());
}
@Test
void testCollectSuccess() throws Exception {
try (MockedConstruction<ZooKeeper> mocked = Mockito.mockConstruction(ZooKeeper.class,
(mock, context) -> Mockito.when(mock.getChildren("/services", false)).thenReturn(List.of("host1:8080", "host2:9090")))) {
zookeeperSdCollect.collect(builder, metrics);
assertEquals(2, builder.getValuesCount());
assertEquals("host1", builder.getValues(0).getColumns(0));
assertEquals("8080", builder.getValues(0).getColumns(1));
assertEquals("host2", builder.getValues(1).getColumns(0));
assertEquals("9090", builder.getValues(1).getColumns(1));
}
}
}
@@ -28,7 +28,7 @@ import java.util.concurrent.ScheduledExecutorService;
import org.apache.hertzbeat.collector.dispatch.CollectorInfoProperties;
import org.apache.hertzbeat.collector.dispatch.DispatchProperties;
import org.apache.hertzbeat.collector.dispatch.entrance.internal.CollectJobService;
import org.apache.hertzbeat.collector.dispatch.timer.TimerDispatch;
import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.support.CommonThreadPool;
import org.apache.hertzbeat.remoting.RemotingClient;
@@ -32,7 +32,7 @@ import java.util.concurrent.CountDownLatch;
import org.apache.hertzbeat.collector.dispatch.DispatchProperties;
import org.apache.hertzbeat.collector.dispatch.WorkerPool;
import org.apache.hertzbeat.collector.dispatch.entrance.CollectServer;
import org.apache.hertzbeat.collector.dispatch.timer.TimerDispatch;
import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.common.entity.job.Job;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.entity.message.CollectRep;
@@ -23,9 +23,9 @@ import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.dispatch.entrance.internal.CollectJobService;
import org.apache.hertzbeat.collector.dispatch.timer.Timeout;
import org.apache.hertzbeat.collector.dispatch.timer.TimerDispatch;
import org.apache.hertzbeat.collector.dispatch.timer.WheelTimerTask;
import org.apache.hertzbeat.common.timer.Timeout;
import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.collector.timer.WheelTimerTask;
import org.apache.hertzbeat.collector.dispatch.unit.UnitConvert;
import org.apache.hertzbeat.collector.util.CollectUtil;
import org.apache.hertzbeat.common.entity.job.Configmap;
@@ -281,7 +281,8 @@ public class CommonDispatcher implements MetricsTaskDispatch, CollectDataDispatc
}
// If it is an asynchronous periodic cyclic task, directly response the collected data
if (job.isSd()) {
commonDataQueue.sendServiceDiscoveryData(metricsData);
CollectRep.MetricsData sdMetricsData = CollectRep.MetricsData.newBuilder(metricsData).build();
commonDataQueue.sendServiceDiscoveryData(sdMetricsData);
}
commonDataQueue.sendMetricsData(metricsData);
} else {
@@ -23,8 +23,8 @@ import org.apache.commons.jexl3.JexlExpression;
import org.apache.hertzbeat.collector.collect.AbstractCollect;
import org.apache.hertzbeat.collector.collect.prometheus.PrometheusAutoCollectImpl;
import org.apache.hertzbeat.collector.collect.strategy.CollectStrategyFactory;
import org.apache.hertzbeat.collector.dispatch.timer.Timeout;
import org.apache.hertzbeat.collector.dispatch.timer.WheelTimerTask;
import org.apache.hertzbeat.common.timer.Timeout;
import org.apache.hertzbeat.collector.timer.WheelTimerTask;
import org.apache.hertzbeat.collector.dispatch.unit.UnitConvert;
import org.apache.hertzbeat.collector.util.CollectUtil;
import org.apache.hertzbeat.common.constants.CommonConstants;
@@ -29,5 +29,9 @@ org.apache.hertzbeat.collector.collect.mqtt.MqttCollectImpl
org.apache.hertzbeat.collector.collect.ipmi2.IpmiCollectImpl
org.apache.hertzbeat.collector.collect.kafka.KafkaCollectImpl
org.apache.hertzbeat.collector.collect.sd.HttpSdCollectImpl
org.apache.hertzbeat.collector.collect.sd.NacosSdCollectImpl
org.apache.hertzbeat.collector.collect.sd.DnsSdCollectImpl
org.apache.hertzbeat.collector.collect.sd.EurekaSdCollectImpl
org.apache.hertzbeat.collector.collect.sd.ConsulSdCollectImpl
org.apache.hertzbeat.collector.collect.modbus.ModbusCollectImpl
org.apache.hertzbeat.collector.collect.s7.S7CollectImpl
org.apache.hertzbeat.collector.collect.s7.S7CollectImpl
@@ -39,7 +39,7 @@ spring:
collector:
info:
version: ${COLLECTOR_VERSION:1.7.0}
version: ${COLLECTOR_VERSION:1.7.1}
ip: ${COLLECTOR_IP:}
dispatch:
entrance:
@@ -17,7 +17,7 @@
package org.apache.hertzbeat.collector.dispatch;
import org.apache.hertzbeat.collector.dispatch.timer.Timeout;
import org.apache.hertzbeat.common.timer.Timeout;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.message.CollectRep;
@@ -126,7 +126,24 @@ public interface DispatchConstants {
/**
* protocol http sd
*/
String PROTOCOL_HTTP_SD = "httpsd";
String PROTOCOL_HTTP_SD = "http_sd";
/**
* protocol nacos sd
*/
String PROTOCOL_NACOS_SD = "nacos_sd";
/**
* protocol dns sd
*/
String PROTOCOL_DNS_SD = "dns_sd";
/**
* protocol eureka sd
*/
String PROTOCOL_EUREKA_SD = "eureka_sd";
/**
* protocol consul sd
*/
String PROTOCOL_CONSUL_SD = "consul_sd";
/**
* protocol redfish
*/
@@ -17,7 +17,7 @@
package org.apache.hertzbeat.collector.dispatch;
import org.apache.hertzbeat.collector.dispatch.timer.Timeout;
import org.apache.hertzbeat.common.timer.Timeout;
/**
* Metrics collection task scheduler interface
@@ -31,7 +31,7 @@ import org.apache.hertzbeat.collector.dispatch.entrance.processor.GoCloseProcess
import org.apache.hertzbeat.collector.dispatch.entrance.processor.GoOfflineProcessor;
import org.apache.hertzbeat.collector.dispatch.entrance.processor.GoOnlineProcessor;
import org.apache.hertzbeat.collector.dispatch.entrance.processor.HeartbeatProcessor;
import org.apache.hertzbeat.collector.dispatch.timer.TimerDispatch;
import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.common.entity.dto.CollectorInfo;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.support.CommonThreadPool;
@@ -22,7 +22,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.dispatch.DispatchProperties;
import org.apache.hertzbeat.collector.dispatch.WorkerPool;
import org.apache.hertzbeat.collector.dispatch.entrance.CollectServer;
import org.apache.hertzbeat.collector.dispatch.timer.TimerDispatch;
import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.job.Job;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
@@ -20,7 +20,7 @@ package org.apache.hertzbeat.collector.dispatch.entrance.processor;
import io.netty.channel.ChannelHandlerContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.dispatch.entrance.CollectServer;
import org.apache.hertzbeat.collector.dispatch.timer.TimerDispatch;
import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.support.SpringContextHolder;
@@ -20,7 +20,7 @@ package org.apache.hertzbeat.collector.dispatch.entrance.processor;
import com.google.protobuf.ByteString;
import io.netty.channel.ChannelHandlerContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.dispatch.timer.TimerDispatch;
import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.support.SpringContextHolder;
@@ -20,7 +20,7 @@ package org.apache.hertzbeat.collector.dispatch.entrance.processor;
import com.google.protobuf.ByteString;
import io.netty.channel.ChannelHandlerContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.dispatch.timer.TimerDispatch;
import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.support.SpringContextHolder;
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.collector.dispatch.timer;
package org.apache.hertzbeat.collector.timer;
import org.apache.hertzbeat.collector.dispatch.entrance.internal.CollectResponseEventListener;
import org.apache.hertzbeat.common.entity.job.Job;
@@ -15,12 +15,12 @@
* limitations under the License.
*/
package org.apache.hertzbeat.collector.dispatch.timer;
package org.apache.hertzbeat.collector.timer;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import lombok.extern.slf4j.Slf4j;
@@ -28,6 +28,9 @@ import org.apache.hertzbeat.collector.dispatch.entrance.internal.CollectResponse
import org.apache.hertzbeat.common.entity.job.Job;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.timer.HashedWheelTimer;
import org.apache.hertzbeat.common.timer.Timeout;
import org.apache.hertzbeat.common.timer.Timer;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.stereotype.Component;
@@ -87,7 +90,7 @@ public class TimerDispatcher implements TimerDispatch, DisposableBean {
for (Metrics metric : addJob.getMetrics()) {
metric.setInterval(0L);
}
addJob.setIntervals(new LinkedList<>(List.of(0L)));
addJob.setIntervals(new ConcurrentLinkedDeque<>(List.of(0L)));
Timeout timeout = wheelTimer.newTimeout(timerJob, addJob.getInterval(), TimeUnit.SECONDS);
currentTempTaskMap.put(addJob.getId(), timeout);
eventListeners.put(addJob.getId(), eventListener);
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.collector.dispatch.timer;
package org.apache.hertzbeat.collector.timer;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
@@ -28,6 +28,8 @@ import org.apache.hertzbeat.common.entity.job.Configmap;
import org.apache.hertzbeat.common.entity.job.Job;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.apache.hertzbeat.common.timer.Timeout;
import org.apache.hertzbeat.common.timer.TimerTask;
import org.apache.hertzbeat.common.util.AesUtil;
import java.util.ArrayList;
+4
View File
@@ -72,6 +72,10 @@
<artifactId>jackson-annotations</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
<!-- swagger -->
<dependency>
<groupId>org.springdoc</groupId>
@@ -73,14 +73,14 @@ public interface CommonConstants {
byte MONITOR_DOWN_CODE = 0x02;
/**
* Monitor bind type, 0: monitor that auto-created by sd
* scrape type static
*/
byte MONITOR_BIND_TYPE_SD_SUB_MONITOR = 0x00;
String SCRAPE_STATIC = "static";
/**
* Monitor bind type, 1: the main monitor of sd
* scrape type
*/
byte MONITOR_BIND_TYPE_SD_MAIN_MONITOR = 0x01;
String SCRAPE_HTTP_SD = "http_sd";
/**
* label key: instance
@@ -192,11 +192,6 @@ public interface CommonConstants {
*/
String PROM_VALUE = "value";
/**
* Monitor total availability metrics
*/
String AVAILABILITY = "availability";
/**
* Parameter Type Number
*/
@@ -222,104 +217,6 @@ public interface CommonConstants {
*/
byte PARAM_TYPE_ARRAY = 4;
/**
* Authentication type Account password
*/
byte AUTH_TYPE_PASSWORD = 1;
/**
* Authentication type GitHub three-party login
*/
byte AUTH_TYPE_GITHUB = 2;
/**
* Authentication type WeChat three-party login
*/
byte AUTH_TYPE_WEIXIN = 3;
/**
* Authentication type GITEE three-party login
*/
byte AUTH_TYPE_GITEE = 5;
/**
* inner default label key __instance__
*/
String LABEL_INNER_KEY_INSTANCE = "__instance__";
/**
* Inside the tag: monitorId Monitor task ID
*/
String TAG_MONITOR_ID = "monitorId";
/**
* Inside the tag: monitorName Task name
*/
String TAG_MONITOR_NAME = "monitorName";
/**
* Inside the tag: indicate that this monitor is auto-created by main monitor
*/
String TAG_AUTO_CREATED = "autoCreated";
/**
* Inside the tag: indicate that this monitor is a main monitor which provides service discovery
*/
String TAG_SD_MAIN_MONITOR = "sdMainMonitor";
/**
* Inside the tag: monitorHost Task host
*/
String TAG_MONITOR_HOST = "monitorHost";
/**
* Inside the tag: policyId Alarm threshold rule ID
*/
String TAG_THRESHOLD_ID = "thresholdId";
/**
* Inside the tag: app Type of monitoring
*/
String TAG_MONITOR_APP = "app";
/**
* Inside the tag: metrics
*/
String TAG_METRICS = "metrics";
/**
* Inside the tag: metric
*/
String TAG_METRIC = "metric";
/**
* Inside the tag: code
*/
String TAG_CODE = "code";
/**
* Tag Type: Auto-generated
*/
byte TAG_TYPE_AUTO_GENERATE = 0;
/**
* Tag Type: User-generated
*/
byte TAG_TYPE_USER_GENERATE = 1;
/**
* Tag Type: System preset
*/
byte TAG_TYPE_SYSTEM_PRESET = 2;
/**
* notice_period type Type field, daily type
*/
int NOTICE_PERIOD_DAILY = 0;
/**
* key is receiver.id, value is noticePeriod cache key prefix
*/
String RECEIVER_NOTICE_PERIOD_CACHE_PREFIX = "receiver_notice_period:";
/**
* cache key notice_rule
*/
@@ -86,7 +86,7 @@ public class GroupAlert {
@Schema(title = "Alert Fingerprints", example = "[\"dxsdfdsf\"]")
@Convert(converter = JsonStringListAttributeConverter.class)
@Column(length = 2048)
@Column(length = 8192)
private List<String> alertFingerprints;
@Schema(title = "The creator of this record", example = "tom")
@@ -28,6 +28,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
import lombok.Builder;
@@ -114,7 +115,7 @@ public class Job {
/**
* Refresh time list for one cycle of the job
*/
private LinkedList<Long> intervals;
private ConcurrentLinkedDeque<Long> intervals;
/**
* Whether it is a recurring periodic task true is yes, false is no
*/
@@ -328,7 +329,7 @@ public class Job {
* @param metricsIntervals A unique list composed of intervals for all metrics
* Generate a list of refresh intervals for metric collection
*/
public void generateMetricsIntervals(List<Long> metricsIntervals) {
public synchronized void generateMetricsIntervals(List<Long> metricsIntervals) {
// 1. To find the least common multiple (LCM) of all metric refresh intervals
long lcm = lcm(metricsIntervals);
List<Long> refreshTimes = new LinkedList<>();
@@ -348,14 +349,16 @@ public class Job {
for (int i = 1; i < refreshTimes.size(); i++) {
intervals.add(refreshTimes.get(i) - refreshTimes.get(i - 1));
}
setIntervals(intervals);
setIntervals(new ConcurrentLinkedDeque<>(intervals));
}
public long getInterval() {
if (!CollectionUtils.isEmpty(getIntervals())) {
long interval = getIntervals().remove();
getIntervals().add(interval);
return interval;
public synchronized long getInterval() {
if (!CollectionUtils.isEmpty(this.intervals)) {
Long interval = this.intervals.removeFirst();
if (interval != null) {
this.intervals.addLast(interval);
return interval;
}
}
return getDefaultInterval();
}
@@ -30,9 +30,13 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.entity.job.protocol.DnsProtocol;
import org.apache.hertzbeat.common.entity.job.protocol.DnsSdProtocol;
import org.apache.hertzbeat.common.entity.job.protocol.FtpProtocol;
import org.apache.hertzbeat.common.entity.job.protocol.HttpProtocol;
import org.apache.hertzbeat.common.entity.job.protocol.EurekaSdProtocol;
import org.apache.hertzbeat.common.entity.job.protocol.ConsulSdProtocol;
import org.apache.hertzbeat.common.entity.job.protocol.ModbusProtocol;
import org.apache.hertzbeat.common.entity.job.protocol.NacosSdProtocol;
import org.apache.hertzbeat.common.entity.job.protocol.PlcProtocol;
import org.apache.hertzbeat.common.entity.job.protocol.RegistryProtocol;
import org.apache.hertzbeat.common.entity.job.protocol.IcmpProtocol;
@@ -63,17 +67,18 @@ import org.apache.hertzbeat.common.entity.job.protocol.TelnetProtocol;
import org.apache.hertzbeat.common.entity.job.protocol.UdpProtocol;
import org.apache.hertzbeat.common.entity.job.protocol.WebsocketProtocol;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.entity.sd.ServiceDiscoveryProtocol;
import org.apache.hertzbeat.common.entity.job.protocol.ZookeeperSdProtocol;
/**
* Details of the monitoring metrics collected
* eg: cpu | memory | health
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Slf4j
@SuppressWarnings("checkstyle:MemberName")
public class Metrics {
/**
@@ -260,9 +265,29 @@ public class Metrics {
*/
private KafkaProtocol kclient;
/**
* Collect sd data protocol
* http sd protocol
*/
private ServiceDiscoveryProtocol sdProtocol;
private HttpProtocol http_sd;
/**
* dns sd protocol
*/
private DnsSdProtocol dns_sd;
/**
* eureka sd protocol
*/
private EurekaSdProtocol eureka_sd;
/**
* consul sd protocol
*/
private ConsulSdProtocol consul_sd;
/**
* nacos sd protocol
*/
private NacosSdProtocol nacos_sd;
/*
* zookeeper sd protocol
*/
private ZookeeperSdProtocol zookeeper_sd;
/**
* Monitoring configuration information using the public plc protocol
*/
@@ -21,7 +21,8 @@ package org.apache.hertzbeat.common.entity.job.protocol;
* Define common field method for each protocol in {@link org.apache.hertzbeat.common.entity.job.Metrics}
*/
public interface CommonRequestProtocol {
void setHost(String host);
void setPort(String port);
}
}
@@ -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.common.entity.job.protocol;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
/**
* consul sd protocol
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ConsulSdProtocol implements Protocol{
private String host;
private String port;
public boolean isInvalid() {
return StringUtils.isAnyBlank(host, port);
}
}
@@ -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.common.entity.job.protocol;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Dns protocol
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class DnsSdProtocol implements Protocol {
private String host;
private String port;
private String recordType;
private String recordName;
}
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.common.entity.manager;
package org.apache.hertzbeat.common.entity.job.protocol;
import lombok.AllArgsConstructor;
import lombok.Builder;
@@ -23,15 +23,13 @@ import lombok.Data;
import lombok.NoArgsConstructor;
/**
* sd monitor param
* eureka sd protocol
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class SdMonitorParam {
private String detectedHost;
private String detectedPort;
private Param sdParam;
private Long bizId;
}
public class EurekaSdProtocol implements Protocol{
private String url;
}
@@ -0,0 +1,35 @@
/*
* 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.common.entity.job.protocol;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Dns protocol
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class HttpSdProtocol implements Protocol {
private String url;
}
@@ -15,53 +15,49 @@
* limitations under the License.
*/
package org.apache.hertzbeat.common.entity.sd;
package org.apache.hertzbeat.common.entity.job.protocol;
import org.apache.commons.lang3.StringUtils;
import java.util.Arrays;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* service discovery common field
* Nacos Service Discovery Protocol
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ServiceDiscoveryProtocol {
private Long id;
private List<Long> jobIdList;
@NoArgsConstructor
public class NacosSdProtocol implements Protocol {
/**
* sd type
* Nacos server host
*/
private Type type;
private String host;
/**
* Target sd sources by which collector updates SDCache.
* Nacos server port
*/
private String sdSource;
private String port;
/**
* sd Type
* Nacos namespace
*/
public enum Type {
HTTP_SD("httpsd"),
;
private final String protocolName;
Type(String protocolName) {
this.protocolName = protocolName;
}
public static Type getType(String str) {
return Arrays.stream(Type.values()).filter(t -> t.toString().equalsIgnoreCase(str)).findFirst().orElse(null);
}
public String getProtocolName() {
return protocolName;
}
private String namespace;
/**
* Nacos username for authentication
*/
private String username;
/**
* Nacos password for authentication
*/
private String password;
/**
* Check if the essential protocol parameters are invalid
* @return true if essential parameters are missing
*/
public boolean isInvalid() {
return StringUtils.isAnyBlank(host, port);
}
}
}
@@ -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.common.entity.job.protocol;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
/**
* Zookeeper service discovery protocol
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ZookeeperSdProtocol implements Protocol{
private String url;
private String pathPrefix;
}
@@ -77,6 +77,10 @@ public class Monitor {
@Size(max = 100)
private String app;
@Schema(title = "Scrape type: static | http_sd | dns_sd | zookeeper_sd", example = "static", accessMode = READ_WRITE)
@Size(max = 100)
private String scrape;
@Schema(title = "peer host: ipv4, ipv6, domain name", example = "192.167.25.11", accessMode = READ_WRITE)
@Size(max = 100)
@HostValid
@@ -17,7 +17,6 @@
package org.apache.hertzbeat.common.entity.manager;
import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
@@ -27,8 +26,6 @@ import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.Table;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.Builder;
@@ -60,18 +57,16 @@ public class MonitorBind {
@Schema(title = "primary id", example = "23")
private Long id;
@Schema(title = "collector name", example = "87432674384")
@Schema(title = "key string: ip:port")
private String keyStr;
@Schema(title = "connect bind id", example = "87432674384")
private Long bizId;
@Schema(title = "monitor ID", example = "87432674336")
@Column(name = "monitor_id")
private Long monitorId;
@Schema(title = "Bind type 0: sd", accessMode = READ_WRITE)
@Min(0)
@Max(3)
private byte type;
@Schema(title = "The creator of this record", example = "tom")
@CreatedBy
private String creator;
@@ -87,4 +82,4 @@ public class MonitorBind {
@Schema(title = "Record the latest modification time (timestamp in milliseconds)")
@LastModifiedDate
private LocalDateTime gmtUpdate;
}
}
@@ -454,8 +454,9 @@ public final class CollectRep {
return new MetricsData(new ArrowTable(root));
} catch (Exception e1) {
log.error(e1.getMessage(), e1);
root.close();
throw e1;
} finally {
root.close();
}
} catch (Exception e) {
log.error(e.getMessage(), e);
@@ -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.common.entity.sd;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* eureka service discovery entity
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@JacksonXmlRootElement(localName = "applications")
public class EurekaDiscoveryResponseEntity {
@JacksonXmlProperty(localName = "versions__delta")
private int versionsDelta;
@JacksonXmlProperty(localName = "apps__hashcode")
private String appsHashcode;
@JacksonXmlElementWrapper(useWrapping = false)
@JacksonXmlProperty(localName = "application")
private List<Application> applications;
/**
* eureka application entity
*/
@Data
public static class Application {
@JacksonXmlProperty(localName = "name")
private String name;
@JacksonXmlElementWrapper(useWrapping = false)
@JacksonXmlProperty(localName = "instance")
private List<Instance> instances;
}
/**
* eureka instance entity
*/
@Data
public static class Instance {
@JacksonXmlProperty(localName = "instanceId")
private String instanceId;
@JacksonXmlProperty(localName = "hostName")
private String hostName;
@JacksonXmlProperty(localName = "app")
private String app;
@JacksonXmlProperty(localName = "ipAddr")
private String ipAddr;
@JacksonXmlProperty(localName = "status")
private String status;
@JacksonXmlProperty(localName = "port")
private Integer port;
}
}
@@ -20,6 +20,7 @@ package org.apache.hertzbeat.common.support.valid;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.apache.hertzbeat.common.util.IpDomainUtil;
import org.springframework.util.StringUtils;
/**
* Host Param Validator
@@ -33,10 +34,13 @@ public class HostParamValidator implements ConstraintValidator<HostValid, String
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value != null && value.toLowerCase().contains(HTTP)){
if (!StringUtils.hasText(value)) {
return true;
}
if (value.toLowerCase().contains(HTTP)){
value = value.replaceFirst(PATTERN_HTTP, BLANK);
}
if (value != null && value.toLowerCase().contains(HTTPS)){
if (value.toLowerCase().contains(HTTPS)){
value = value.replaceFirst(PATTERN_HTTPS, BLANK);
}
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.collector.dispatch.timer;
package org.apache.hertzbeat.common.timer;
import org.apache.hertzbeat.common.util.NetworkUtil;
import org.slf4j.Logger;
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.collector.dispatch.timer;
package org.apache.hertzbeat.common.timer;
/**
* Derived from Apache Dubbo org.apache.dubbo.common.timer.Timeout
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.collector.dispatch.timer;
package org.apache.hertzbeat.common.timer;
import java.util.Set;
import java.util.concurrent.RejectedExecutionException;
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.collector.dispatch.timer;
package org.apache.hertzbeat.common.timer;
import java.util.concurrent.TimeUnit;
@@ -64,6 +64,7 @@ public final class ArrowUtil {
writer.writeBatch();
writer.end();
writer.close();
root.close();
}
return out.toByteArray();
} catch (IOException e) {
@@ -1,151 +0,0 @@
/*
* 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.common.util;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.job.Configmap;
import org.apache.hertzbeat.common.entity.job.Job;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.manager.MonitorBind;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.SdMonitorParam;
import org.apache.hertzbeat.common.entity.manager.Label;
import org.apache.hertzbeat.common.entity.sd.ServiceDiscoveryProtocol;
/**
* Operator for service discovery
*/
public class SdMonitorOperator {
public static Optional<Param> getSdParam(List<Param> params) {
return params.stream()
.filter(param -> StringUtils.isNoneBlank(param.getField(), param.getParamValue()))
.filter(param -> Objects.nonNull(ServiceDiscoveryProtocol.Type.getType(param.getField())))
.findFirst();
}
public static List<Param> removeSdParam(List<Param> params) {
return params.stream()
.filter(param -> Objects.isNull(ServiceDiscoveryProtocol.Type.getType(param.getField())))
.collect(Collectors.toList());
}
public static List<Param> cloneParamList(List<Param> params) {
return params.stream()
.map(Param::clone)
.peek(param -> param.setId(null))
.collect(Collectors.toList());
}
public static MonitorBind buildSdSubMonitorBind(SdMonitorParam sdMonitorParam, long monitorId, Map<String, String> labels) {
if (Objects.isNull(sdMonitorParam.getBizId())) {
return null;
}
labels.put(CommonConstants.TAG_AUTO_CREATED, String.valueOf(sdMonitorParam.getBizId()));
return MonitorBind.builder()
.id(SnowFlakeIdGenerator.generateId())
.bizId(sdMonitorParam.getBizId())
.monitorId(monitorId)
.type(CommonConstants.MONITOR_BIND_TYPE_SD_SUB_MONITOR)
.build();
}
public static MonitorBind buildSdMainMonitorBind(SdMonitorParam sdMonitorParam, long monitorId) {
if (Objects.isNull(sdMonitorParam.getSdParam())) {
return null;
}
return MonitorBind.builder()
.id(SnowFlakeIdGenerator.generateId())
.bizId(sdMonitorParam.getBizId())
.monitorId(monitorId)
.type(CommonConstants.MONITOR_BIND_TYPE_SD_MAIN_MONITOR)
.build();
}
public static Job constructSdJobAndTag(SdMonitorParam sdMonitorParam, Map<String, String> labels, Job appDefine) {
if (Objects.isNull(sdMonitorParam.getSdParam())) {
return appDefine;
}
labels.put(CommonConstants.TAG_SD_MAIN_MONITOR, ServiceDiscoveryProtocol.Type.getType(sdMonitorParam.getSdParam().getField()).name());
return constructSdJob(appDefine, sdMonitorParam.getSdParam());
}
public static Job constructSdJob(Job appDefine, Param sdParam) {
final Job sdJob = appDefine.clone();
final ServiceDiscoveryProtocol.Type sdType = ServiceDiscoveryProtocol.Type.getType(sdParam.getField());
List<Metrics> metricsList = Lists.newArrayList();
Map<String, String> i18n = Maps.newHashMap();
i18n.put("zh-CN", "监控目标");
i18n.put("en-US", "Monitor Target");
List<Metrics.Field> fields = Lists.newArrayList();
fields.add(Metrics.Field.builder()
.field("host")
.type(CommonConstants.TYPE_STRING)
.i18n(constructSdFieldI18n("主机", "host"))
.build());
fields.add(Metrics.Field.builder()
.field("port")
.type(CommonConstants.TYPE_STRING)
.i18n(constructSdFieldI18n("端口", "port"))
.build());
metricsList.add(Metrics.builder()
.name("target")
.fields(fields)
.i18n(i18n)
.protocol(sdType.getProtocolName())
.sdProtocol(ServiceDiscoveryProtocol.builder()
.sdSource(sdParam.getParamValue())
.type(sdType)
.build())
.build());
sdJob.setSd(Boolean.TRUE);
sdJob.setMetrics(metricsList);
sdJob.setConfigmap(Lists.newArrayList(new Configmap(sdParam.getField(), sdParam.getParamValue(), sdParam.getType())));
return sdJob;
}
public static List<Label> addMainMonitorTag(List<Label> tagList, Param sdParam) {
tagList = tagList.stream()
.filter(tag -> !StringUtils.equals(tag.getName(), CommonConstants.TAG_SD_MAIN_MONITOR))
.filter(tag -> !StringUtils.equals(tag.getName(), CommonConstants.TAG_AUTO_CREATED))
.collect(Collectors.toList());
tagList.add(Label.builder().name(CommonConstants.TAG_SD_MAIN_MONITOR)
.tagValue(ServiceDiscoveryProtocol.Type.getType(sdParam.getField()).name())
.type(CommonConstants.TAG_TYPE_AUTO_GENERATE)
.build());
return tagList;
}
private static Map<String, String> constructSdFieldI18n(String zh, String en) {
Map<String, String> i18n = Maps.newHashMap();
i18n.put("zh-CN", zh);
i18n.put("en-US", en);
return i18n;
}
}
@@ -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.common.util;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
/**
* xml util
*/
@Slf4j
public class XmlUtil {
private static final XmlMapper XML_MAPPER = new XmlMapper();
static {
XML_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
.registerModule(new JavaTimeModule());
}
public String toXml(Object source) {
if (source == null) {
return null;
}
try {
return XML_MAPPER.writeValueAsString(source);
} catch (JsonProcessingException e) {
log.error(e.getMessage(), e);
return null;
}
}
public static <T> T fromXml(String xml, Class<T> clazz) {
if (!StringUtils.hasText(xml)) {
return null;
}
try {
return XML_MAPPER.readValue(xml, clazz);
} catch (Exception e) {
log.error(e.getMessage(), e);
return null;
}
}
public static <T> T fromXml(String xml, TypeReference<T> type) {
if (!StringUtils.hasText(xml)) {
return null;
}
try {
return XML_MAPPER.readValue(xml, type);
} catch (Exception e) {
log.error(e.getMessage(), e);
return null;
}
}
}
@@ -53,13 +53,13 @@ class HostParamValidatorTest {
@Test
public void testIsValid() {
boolean result = hostParamValidator.isValid(null, context);
assertFalse(result);
assertTrue(result);
result = hostParamValidator.isValid("", context);
assertFalse(result);
assertTrue(result);
result = hostParamValidator.isValid(" ", context);
assertFalse(result);
assertTrue(result);
result = hostParamValidator.isValid("192.168.1.1", context);
assertTrue(result);
@@ -20,8 +20,8 @@ package org.apache.hertzbeat.collector.collect;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.dispatch.CollectDataDispatch;
import org.apache.hertzbeat.collector.dispatch.MetricsCollect;
import org.apache.hertzbeat.collector.dispatch.timer.Timeout;
import org.apache.hertzbeat.collector.dispatch.timer.WheelTimerTask;
import org.apache.hertzbeat.common.timer.Timeout;
import org.apache.hertzbeat.collector.timer.WheelTimerTask;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.job.Job;
import org.apache.hertzbeat.common.entity.job.Metrics;
@@ -1,117 +0,0 @@
/*
* 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.log.config;
import static io.opentelemetry.semconv.ServiceAttributes.SERVICE_NAME;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter;
import io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.logs.SdkLoggerProvider;
import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor;
import io.opentelemetry.sdk.resources.Resource;
import jakarta.annotation.PostConstruct;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.warehouse.store.history.greptime.GreptimeProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
/**
* OpenTelemetryConfig is responsible for initializing OpenTelemetry with the specified service name and GrepTimeDB endpoint.
* It ensures that the initialization is done in a thread-safe manner and includes authentication for GrepTimeDB.
*/
@Configuration
@Slf4j
public class OpenTelemetryConfig {
@Autowired
private GreptimeProperties greptimeProperties;
/**
* Initializes OpenTelemetry with the given service name and GrepTimeDB endpoint.
* Includes authentication if configured in GreptimeProperties.
*/
@PostConstruct
public void initializeOpenTelemetry() {
if (greptimeProperties == null || !greptimeProperties.enabled()) {
log.info("GrepTimeDB logging is disabled, skipping OpenTelemetry configuration.");
return;
}
try {
Resource resource = Resource.getDefault()
.merge(Resource.builder()
.put(SERVICE_NAME, "HertzBeat")
.build());
Map<String, String> headers = new HashMap<>();
headers.put("X-Greptime-DB-Name", "public");
headers.put("X-Greptime-Log-Table-Name", "hzb_log");
addAuthenticationHeaders(headers);
OtlpHttpLogRecordExporter logExporter = OtlpHttpLogRecordExporter.builder()
.setEndpoint(greptimeProperties.httpEndpoint() + "/v1/otlp/v1/logs")
.setHeaders(()-> headers)
.setTimeout(10, TimeUnit.SECONDS)
.build();
SdkLoggerProvider loggerProvider = SdkLoggerProvider.builder()
.setResource(resource)
.addLogRecordProcessor(
BatchLogRecordProcessor.builder(logExporter)
.setScheduleDelay(1000, TimeUnit.MILLISECONDS)
.setMaxExportBatchSize(512)
.build())
.build();
OpenTelemetry openTelemetry = OpenTelemetrySdk.builder()
.setLoggerProvider(loggerProvider)
.build();
OpenTelemetryAppender.install(openTelemetry);
log.info("OpenTelemetry successfully configured with GrepTimeDB exporter.");
} catch (Exception e) {
log.error("Failed to initialize OpenTelemetry with GrepTimeDB", e);
}
}
/**
* Adds authentication headers to the provided map if username and password are configured.
*
* @param headers the map to which authentication headers will be added
*/
private void addAuthenticationHeaders(Map<String, String> headers) {
if (StringUtils.isNotBlank(greptimeProperties.username())
&& StringUtils.isNotBlank(greptimeProperties.password())) {
String credentials = greptimeProperties.username() + ":" + greptimeProperties.password();
String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes());
headers.put("Authorization", "Basic " + encodedCredentials);
}
}
}
+5 -5
View File
@@ -47,7 +47,7 @@
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-base</artifactId>
</dependency>
</dependency>
<!-- data warehouse -->
<dependency>
<groupId>org.apache.hertzbeat</groupId>
@@ -92,7 +92,7 @@
<!-- log -->
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-log</artifactId>
<artifactId>hertzbeat-otel</artifactId>
</dependency>
<!-- spring -->
<dependency>
@@ -150,8 +150,8 @@
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>provided</scope>
</dependency>
<!-- email -->
@@ -409,5 +409,5 @@
</build>
</profile>
</profiles>
</project>
@@ -18,25 +18,22 @@
package org.apache.hertzbeat.manager.component.sd;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.util.HashSet;
import java.time.LocalDateTime;
import java.util.LinkedList;
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.commons.lang3.StringUtils;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.arrow.RowWrapper;
import org.apache.hertzbeat.common.entity.manager.CollectorMonitorBind;
import org.apache.hertzbeat.common.entity.manager.Monitor;
import org.apache.hertzbeat.common.entity.manager.MonitorBind;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.SdMonitorParam;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.queue.CommonDataQueue;
import org.apache.hertzbeat.common.util.SdMonitorOperator;
import org.apache.hertzbeat.manager.dao.CollectorMonitorBindDao;
import org.apache.hertzbeat.manager.dao.MonitorBindDao;
import org.apache.hertzbeat.manager.dao.MonitorDao;
@@ -45,7 +42,6 @@ import org.apache.hertzbeat.manager.scheduler.ManagerWorkerPool;
import org.apache.hertzbeat.manager.service.MonitorService;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
/**
* Service Discovery Worker
@@ -53,6 +49,9 @@ import org.springframework.util.CollectionUtils;
@Slf4j
@Component
public class ServiceDiscoveryWorker implements InitializingBean {
private static final String FILED_HOST = "host";
private static final String FILED_PORT = "port";
private final MonitorService monitorService;
private final ParamDao paramDao;
private final MonitorDao monitorDao;
@@ -83,74 +82,75 @@ public class ServiceDiscoveryWorker implements InitializingBean {
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try (final CollectRep.MetricsData metricsData = dataQueue.pollServiceDiscoveryData()) {
Long monitorId = metricsData.getId();
final Monitor mainMonitor = monitorDao.findMonitorsByIdIn(Sets.newHashSet(monitorId)).get(0);
mainMonitor.getLabels().remove(CommonConstants.TAG_SD_MAIN_MONITOR);
final Monitor mainMonitor = monitorDao.findById(monitorId).orElse(null);
if (mainMonitor == null) {
log.warn("No monitor found for id {}", monitorId);
continue;
}
// collector
final Optional<CollectorMonitorBind> collectorBind = collectorMonitorBindDao.findCollectorMonitorBindByMonitorId(mainMonitor.getId());
final Optional<CollectorMonitorBind> collectorBind = collectorMonitorBindDao.findCollectorMonitorBindByMonitorId(monitorId);
String collector = collectorBind.map(CollectorMonitorBind::getCollector).orElse(null);
// param
List<Param> mainMonitorParamList = paramDao.findParamsByMonitorId(mainMonitor.getId());
mainMonitorParamList = SdMonitorOperator.removeSdParam(mainMonitorParamList);
final Set<Long> subMonitorIdSet = monitorBindDao.findMonitorBindByBizIdAndType(monitorId, CommonConstants.MONITOR_BIND_TYPE_SD_SUB_MONITOR)
.stream()
.map(MonitorBind::getMonitorId)
.collect(Collectors.toSet());
final Map<String, List<Monitor>> hostMonitorMap = CollectionUtils.isEmpty(subMonitorIdSet)
? Maps.newHashMap()
: monitorDao.findMonitorsByIdIn(subMonitorIdSet).stream().collect(Collectors.groupingBy(Monitor::getHost));
// params
List<Param> mainMonitorParams = paramDao.findParamsByMonitorId(monitorId);
final Map<String, MonitorBind> subMonitorBindMap = monitorBindDao.findMonitorBindsByBizId(monitorId)
.stream().collect(Collectors.toMap(MonitorBind::getKeyStr, item -> item));
RowWrapper rowWrapper = metricsData.readRow();
Map<String, String> fieldsValue = Maps.newHashMapWithExpectedSize(8);
String defaultPort = mainMonitorParams.stream()
.filter(param -> FILED_PORT.equals(param.getField()))
.findFirst()
.map(Param::getParamValue)
.orElse("");
while (rowWrapper.hasNextRow()) {
rowWrapper = rowWrapper.nextRow();
final String host = rowWrapper.nextCell().getValue();
final String port = rowWrapper.nextCell().getValue();
final List<Monitor> monitorList = hostMonitorMap.get(host);
if (CollectionUtils.isEmpty(monitorList)) {
monitorService.addAndSaveMonitorJob(mainMonitor.clone(), SdMonitorOperator.cloneParamList(mainMonitorParamList), collector,
SdMonitorParam.builder()
.detectedHost(host)
.detectedPort(port)
.bizId(mainMonitor.getId())
.build(), null);
return;
fieldsValue.clear();
rowWrapper.cellStream().forEach(cell -> {
String value = cell.getValue();
fieldsValue.put(cell.getField().getName(), value);
});
final String host = fieldsValue.get(FILED_HOST);
final String port = fieldsValue.getOrDefault(FILED_PORT, defaultPort);
final String keyStr = host + ":" + port;
if (subMonitorBindMap.containsKey(keyStr)) {
subMonitorBindMap.remove(keyStr);
continue;
}
for (Monitor monitor : monitorList) {
// make sure monitor that has the same host and port is not existed.
final Optional<Param> samePortParam = paramDao.findParamsByMonitorId(monitor.getId()).stream()
.filter(param -> StringUtils.equals(param.getField(), "port"))
.filter(param -> StringUtils.equals(param.getParamValue(), port))
.findFirst();
if (samePortParam.isEmpty()) {
monitorService.addAndSaveMonitorJob(mainMonitor.clone(), SdMonitorOperator.cloneParamList(mainMonitorParamList), collector,
SdMonitorParam.builder()
.detectedHost(host)
.detectedPort(port)
.bizId(mainMonitor.getId())
.build(), null);
} else {
monitorService.enableManageMonitors(Sets.newHashSet(monitor.getId()));
Monitor newMonitor = mainMonitor.clone();
newMonitor.setId(null);
newMonitor.setHost(host);
newMonitor.setName(newMonitor.getName() + "-" + host + ":" + port);
newMonitor.setScrape(CommonConstants.SCRAPE_STATIC);
newMonitor.setGmtCreate(LocalDateTime.now());
newMonitor.setGmtUpdate(LocalDateTime.now());
// replace host port
List<Param> newParams = new LinkedList<>();
for (Param param : mainMonitorParams) {
Param newParam = param.clone();
newParam.setId(null);
newParam.setGmtUpdate(null);
newParam.setGmtCreate(null);
if (FILED_HOST.equals(newParam.getField())) {
newParam.setParamValue(host);
} else if (FILED_PORT.equals(newParam.getField())) {
newParam.setParamValue(port);
}
newParams.add(newParam);
}
// make sure hostMonitorMap contains monitors that have not judged yet.
hostMonitorMap.remove(host);
monitorService.addMonitor(newMonitor, newParams, collector, null);
MonitorBind monitorBind = MonitorBind.builder()
.bizId(monitorId)
.monitorId(newMonitor.getId())
.keyStr(keyStr)
.build();
monitorBindDao.save(monitorBind);
}
// hostMonitorMap only contains monitors which are already existed but not in service discovery data
// due to monitors that coincide with service discovery data are removed.
// Thus, all monitors still in hostMonitorMap need to be cancelled.
final HashSet<Long> needCancelMonitorIdSet = Sets.newHashSet();
hostMonitorMap.forEach((key, value) -> needCancelMonitorIdSet.addAll(value.stream()
.map(Monitor::getId)
.collect(Collectors.toSet())));
monitorService.cancelManageMonitors(needCancelMonitorIdSet);
final Set<Long> needCancelMonitorIdSet = subMonitorBindMap.values().stream()
.map(MonitorBind::getMonitorId).collect(Collectors.toSet());
monitorService.deleteMonitors(needCancelMonitorIdSet);
} catch (Exception exception) {
log.error(exception.getMessage(), exception);
}
@@ -35,7 +35,7 @@ import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
public class JacksonConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer customizer() {
public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() {
return builder -> {
JavaTimeModule javaTimeModule = new JavaTimeModule();
final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSX");
@@ -127,16 +127,4 @@ public class MonitorsController {
monitorService.importConfig(file);
return ResponseEntity.ok(Message.success("Import success"));
}
@PostMapping("/copy")
@Operation(summary = "copy monitors by ids", description = "copy monitors by ids")
public ResponseEntity<Message<Void>> duplicateMonitors(
@Parameter(description = "Monitor ID List", example = "6565463543") @RequestParam List<Long> ids
) {
if (ids != null && !ids.isEmpty()) {
monitorService.copyMonitors(ids);
}
return ResponseEntity.ok(Message.success("copy success"));
}
}
@@ -19,21 +19,29 @@ package org.apache.hertzbeat.manager.dao;
import java.util.List;
import java.util.Set;
import org.apache.hertzbeat.common.entity.manager.MonitorBind;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.transaction.annotation.Transactional;
/**
* AuthResources database operation
* MonitorBind database operation
*/
public interface MonitorBindDao extends JpaRepository<MonitorBind, Long>, JpaSpecificationExecutor<MonitorBind> {
List<MonitorBind> findAllByBizIdInAndType(Set<Long> bizIdList, byte type);
List<MonitorBind> findMonitorBindByBizIdAndType(Long bizId, byte type);
List<MonitorBind> findMonitorBindsByBizId(Long bizId);
MonitorBind findMonitorBindByBizIdAndMonitorIdAndType(Long bizId, Long monitorId, byte type);
List<MonitorBind> findMonitorBindsByBizIdIn(Set<Long> bizIds);
List<MonitorBind> findAllByType(byte type);
void deleteByMonitorId(Long monitorId);
}
@Modifying
@Transactional
void deleteMonitorBindByBizIdAndMonitorId(Long bizId, Long monitorId);
@Modifying
void deleteMonitorBindByBizIdIn(Set<Long> bizIds);
}
@@ -36,12 +36,12 @@ import org.apache.hertzbeat.common.entity.manager.Param;
@Schema(description = "Monitoring information entities")
public class MonitorDto {
@Schema(description = "Monitor Content", accessMode = READ_WRITE)
@Schema(description = "monitor content", accessMode = READ_WRITE)
@NotNull
@Valid
private Monitor monitor;
@Schema(description = "Monitor Params", accessMode = READ_WRITE)
@Schema(description = "monitor params", accessMode = READ_WRITE)
@NotEmpty
@Valid
private List<Param> params;
@@ -19,10 +19,8 @@ package org.apache.hertzbeat.manager.scheduler;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.dto.CollectorInfo;
import org.apache.hertzbeat.common.entity.job.Configmap;
@@ -30,13 +28,10 @@ import org.apache.hertzbeat.common.entity.job.Job;
import org.apache.hertzbeat.common.entity.manager.Collector;
import org.apache.hertzbeat.common.entity.manager.CollectorMonitorBind;
import org.apache.hertzbeat.common.entity.manager.Monitor;
import org.apache.hertzbeat.common.entity.manager.MonitorBind;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.apache.hertzbeat.common.util.SdMonitorOperator;
import org.apache.hertzbeat.manager.dao.CollectorDao;
import org.apache.hertzbeat.manager.dao.CollectorMonitorBindDao;
import org.apache.hertzbeat.manager.dao.MonitorBindDao;
import org.apache.hertzbeat.manager.dao.MonitorDao;
import org.apache.hertzbeat.manager.dao.ParamDao;
import org.apache.hertzbeat.manager.service.AppService;
@@ -45,6 +40,7 @@ import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.util.StringUtils;
/**
* scheduler init
@@ -77,9 +73,6 @@ public class SchedulerInit implements CommandLineRunner {
@Autowired
private CollectorMonitorBindDao collectorMonitorBindDao;
@Autowired
private MonitorBindDao monitorBindDao;
@Override
public void run(String... args) throws Exception {
@@ -98,20 +91,18 @@ public class SchedulerInit implements CommandLineRunner {
// init jobs
List<Monitor> monitors = monitorDao.findMonitorsByStatusNotInAndJobIdNotNull(List.of(CommonConstants.MONITOR_PAUSED_CODE));
List<CollectorMonitorBind> monitorBinds = collectorMonitorBindDao.findAll();
final Set<Long> sdMonitorIds = monitorBindDao.findAllByType(CommonConstants.MONITOR_BIND_TYPE_SD_SUB_MONITOR).stream()
.map(MonitorBind::getBizId)
.collect(Collectors.toSet());
Map<Long, String> monitorIdCollectorMap = monitorBinds.stream().collect(
Collectors.toMap(CollectorMonitorBind::getMonitorId, CollectorMonitorBind::getCollector));
for (Monitor monitor : monitors) {
try {
// build collect job entity
Job appDefine = appService.getAppDefine(monitor.getApp());
List<Param> params = paramDao.findParamsByMonitorId(monitor.getId());
if (sdMonitorIds.contains(monitor.getId())) {
appDefine = SdMonitorOperator.constructSdJob(appDefine, SdMonitorOperator.getSdParam(params).get());
boolean isStatic = CommonConstants.SCRAPE_STATIC.equals(monitor.getScrape()) || !StringUtils.hasText(monitor.getScrape());
String app = isStatic ? monitor.getApp() : monitor.getScrape();
Job appDefine = appService.getAppDefine(app);
if (!isStatic) {
appDefine.setSd(true);
}
List<Param> params = paramDao.findParamsByMonitorId(monitor.getId());
if (CommonConstants.PROMETHEUS.equals(monitor.getApp())) {
appDefine.setApp(CommonConstants.PROMETHEUS_APP_PREFIX + monitor.getName());
}
@@ -129,7 +120,7 @@ public class SchedulerInit implements CommandLineRunner {
.map(param -> new Configmap(param.getField(), param.getParamValue(),
param.getType())).collect(Collectors.toList());
List<ParamDefine> paramDefaultValue = appDefine.getParams().stream()
.filter(item -> StringUtils.isNotBlank(item.getDefaultValue()))
.filter(item -> StringUtils.hasText(item.getDefaultValue()))
.toList();
paramDefaultValue.forEach(defaultVar -> {
if (configmaps.stream().noneMatch(item -> item.getKey().equals(defaultVar.getField()))) {
@@ -18,6 +18,10 @@
package org.apache.hertzbeat.manager.service;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.hertzbeat.common.entity.manager.Label;
import org.springframework.data.domain.Page;
@@ -53,4 +57,11 @@ public interface LabelService {
* @param ids label id list
*/
void deleteLabels(HashSet<Long> ids);
/**
* Identifies new labels from the given entry that are not yet in the database
* @param originLabels k-v entry
* @return new labels
*/
List<Label> determineNewLabels(Set<Map.Entry<String, String>> originLabels);
}
@@ -22,14 +22,12 @@ import org.apache.hertzbeat.common.entity.grafana.GrafanaDashboard;
import org.apache.hertzbeat.common.entity.job.Job;
import org.apache.hertzbeat.common.entity.manager.Monitor;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.SdMonitorParam;
import org.apache.hertzbeat.manager.pojo.dto.AppCount;
import org.apache.hertzbeat.manager.pojo.dto.MonitorDto;
import org.apache.hertzbeat.manager.support.exception.MonitorDetectException;
import org.springframework.data.domain.Page;
import org.springframework.web.multipart.MultipartFile;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -125,14 +123,14 @@ public interface MonitorService {
*
* @param ids Monitoring ID List
*/
void cancelManageMonitors(HashSet<Long> ids);
void cancelManageMonitors(Set<Long> ids);
/**
* Start the managed monitoring items in batches according to the monitoring ID list
*
* @param ids Monitoring ID List
*/
void enableManageMonitors(HashSet<Long> ids);
void enableManageMonitors(Set<Long> ids);
/**
* Query the monitoring category and its corresponding monitoring quantity
@@ -183,22 +181,13 @@ public interface MonitorService {
*/
void importConfig(MultipartFile file) throws Exception;
/**
* Copy monitor in batches based on the id
*
* @param ids monitor id
*/
void copyMonitors(List<Long> ids);
/**
* update app collect job by app
*
* @param job job content
*/
void updateAppCollectJob(Job job);
void addAndSaveMonitorJob(Monitor monitor, List<Param> params, String collector, SdMonitorParam sdMonitorParam, GrafanaDashboard grafanaDashboard);
/**
* Copy monitor by id
*
@@ -270,10 +270,6 @@ public class AppServiceImpl implements AppService, InitializingBean {
public List<Hierarchy> getAllAppHierarchy(String lang) {
LinkedList<Hierarchy> hierarchies = new LinkedList<>();
for (var job : appDefines.values()) {
// TODO temporarily filter out push to solve the front-end problem, and open it after the subsequent design optimization
if (DispatchConstants.PROTOCOL_PUSH.equalsIgnoreCase(job.getApp())) {
continue;
}
queryAppHierarchy(lang, hierarchies, job);
}
return hierarchies;
@@ -22,7 +22,11 @@ import jakarta.persistence.criteria.Predicate;
import java.util.ArrayList;
import java.util.HashSet;
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.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
@@ -117,4 +121,28 @@ public class LabelServiceImpl implements LabelService {
}
labelDao.deleteLabelsByIdIn(ids);
}
public List<Label> determineNewLabels(Set<Map.Entry<String, String>> originLabels){
if (originLabels == null || originLabels.isEmpty()) return List.of();
// Get all labels from the database
Set<Map.Entry<String, String>> allLabels = labelDao.findAll().stream()
.map(label -> Map.entry(label.getName(), label.getTagValue()))
.collect(Collectors.toSet());
// If the bound label (key:value) does not exist, then add it
Set<Map.Entry<String, String>> addLabelsKv = originLabels.stream()
.filter(label -> !allLabels.contains(label))
.collect(Collectors.toCollection(HashSet::new));
return addLabelsKv.stream().map(kv -> {
Label label = new Label();
label.setId(null);
label.setName(kv.getKey());
label.setTagValue(kv.getValue());
label.setType((byte) 0);
return label;
}).toList();
}
}
@@ -23,7 +23,6 @@ import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.Predicate;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.alert.dao.AlertDefineBindDao;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
import org.apache.hertzbeat.common.constants.CommonConstants;
@@ -34,28 +33,27 @@ import org.apache.hertzbeat.common.entity.grafana.GrafanaDashboard;
import org.apache.hertzbeat.common.entity.job.Configmap;
import org.apache.hertzbeat.common.entity.job.Job;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.job.protocol.CommonRequestProtocol;
import org.apache.hertzbeat.common.entity.manager.Collector;
import org.apache.hertzbeat.common.entity.manager.CollectorMonitorBind;
import org.apache.hertzbeat.common.entity.manager.Label;
import org.apache.hertzbeat.common.entity.manager.Monitor;
import org.apache.hertzbeat.common.entity.manager.MonitorBind;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.apache.hertzbeat.common.entity.manager.SdMonitorParam;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.entity.sd.ServiceDiscoveryProtocol;
import org.apache.hertzbeat.common.support.event.MonitorDeletedEvent;
import org.apache.hertzbeat.common.util.AesUtil;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.hertzbeat.common.util.FileUtil;
import org.apache.hertzbeat.common.util.IntervalExpressionUtil;
import org.apache.hertzbeat.common.util.IpDomainUtil;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.hertzbeat.common.util.SdMonitorOperator;
import org.apache.hertzbeat.common.util.SnowFlakeIdGenerator;
import org.apache.hertzbeat.grafana.service.DashboardService;
import org.apache.hertzbeat.manager.config.ManagerSseManager;
import org.apache.hertzbeat.manager.dao.CollectorDao;
import org.apache.hertzbeat.manager.dao.CollectorMonitorBindDao;
import org.apache.hertzbeat.manager.dao.LabelDao;
import org.apache.hertzbeat.manager.dao.MonitorBindDao;
import org.apache.hertzbeat.manager.dao.MonitorDao;
import org.apache.hertzbeat.manager.dao.ParamDao;
@@ -64,8 +62,8 @@ import org.apache.hertzbeat.manager.pojo.dto.MonitorDto;
import org.apache.hertzbeat.manager.scheduler.CollectJobScheduling;
import org.apache.hertzbeat.manager.service.AppService;
import org.apache.hertzbeat.manager.service.ImExportService;
import org.apache.hertzbeat.manager.service.MonitorService;
import org.apache.hertzbeat.manager.service.LabelService;
import org.apache.hertzbeat.manager.service.MonitorService;
import org.apache.hertzbeat.manager.support.exception.MonitorDatabaseException;
import org.apache.hertzbeat.manager.support.exception.MonitorDetectException;
import org.apache.hertzbeat.warehouse.service.WarehouseService;
@@ -81,13 +79,13 @@ import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -109,15 +107,11 @@ public class MonitorServiceImpl implements MonitorService {
private static final Long MONITOR_ID_TMP = 1000000000L;
private static final byte ALL_MONITOR_STATUS = 9;
private static final int TAG_LENGTH = 2;
private static final String CONTENT_VALUE = MediaType.APPLICATION_OCTET_STREAM_VALUE + SignConstants.SINGLE_MARK + "charset=" + StandardCharsets.UTF_8;
private final Map<String, ImExportService> imExportServiceMap = new HashMap<>();
@Autowired
private AppService appService;
@Autowired
private LabelService tagService;
@Autowired
private CollectJobScheduling collectJobScheduling;
@Autowired
private MonitorDao monitorDao;
@@ -139,6 +133,10 @@ public class MonitorServiceImpl implements MonitorService {
private DashboardService dashboardService;
@Autowired
private ManagerSseManager managerSseManager;
@Autowired
private LabelDao labelDao;
@Autowired
private LabelService labelService;
public MonitorServiceImpl(List<ImExportService> imExportServiceList) {
imExportServiceList.forEach(it -> imExportServiceMap.put(it.type(), it));
@@ -147,25 +145,80 @@ public class MonitorServiceImpl implements MonitorService {
@Override
@Transactional(readOnly = true)
public void detectMonitor(Monitor monitor, List<Param> params, String collector) throws MonitorDetectException {
final Optional<Param> sdParam = SdMonitorOperator.getSdParam(params);
if (sdParam.isPresent()) {
collectOneTimeSdData(monitor, collector, sdParam.get());
} else {
if (CommonConstants.SCRAPE_STATIC.equals(monitor.getScrape()) || !StringUtils.hasText(monitor.getScrape())) {
detectMonitorDirectly(monitor, params, collector);
} else {
detectSdMonitor(monitor, params, collector);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void addMonitor(Monitor monitor, List<Param> params, String collector, GrafanaDashboard grafanaDashboard) throws RuntimeException {
final Optional<Param> sdParam = SdMonitorOperator.getSdParam(params);
if (sdParam.isPresent()) {
addAndSaveMonitorJob(monitor.clone(), params, collector, SdMonitorParam.builder().sdParam(sdParam.get()).build(), grafanaDashboard);
return;
// Apply for monitor id
long monitorId = SnowFlakeIdGenerator.generateId();
Map<String, String> labels = monitor.getLabels();
if (labels == null) {
labels = new HashMap<>(8);
monitor.setLabels(labels);
}
List<Label> addLabels = labelService.determineNewLabels(labels.entrySet());
if (!addLabels.isEmpty()) {
labelDao.saveAll(addLabels);
}
addAndSaveMonitorJob(monitor, params, collector, null, grafanaDashboard);
// Construct the collection task Job entity
boolean isStatic = CommonConstants.SCRAPE_STATIC.equals(monitor.getScrape()) || !StringUtils.hasText(monitor.getScrape());
String app = isStatic ? monitor.getApp() : monitor.getScrape();
Job appDefine = appService.getAppDefine(app);
if (!isStatic) {
appDefine.setSd(true);
}
if (CommonConstants.PROMETHEUS.equals(monitor.getApp())) {
appDefine.setApp(CommonConstants.PROMETHEUS_APP_PREFIX + monitor.getName());
}
appDefine.setMonitorId(monitorId);
appDefine.setDefaultInterval(monitor.getIntervals());
appDefine.setCyclic(true);
appDefine.setTimestamp(System.currentTimeMillis());
Map<String, String> metadata = Map.of(CommonConstants.LABEL_INSTANCE_NAME, monitor.getName(),
CommonConstants.LABEL_INSTANCE_HOST, monitor.getHost());
appDefine.setMetadata(metadata);
appDefine.setLabels(monitor.getLabels());
appDefine.setAnnotations(monitor.getAnnotations());
List<Configmap> configmaps = params.stream().map(param -> {
param.setMonitorId(monitorId);
return new Configmap(param.getField(), param.getParamValue(), param.getType());
}).collect(Collectors.toList());
appDefine.setConfigmap(configmaps);
long jobId = collector == null ? collectJobScheduling.addAsyncCollectJob(appDefine, null) :
collectJobScheduling.addAsyncCollectJob(appDefine, collector);
try {
detectMonitor(monitor, params, collector);
} catch (Exception ignored) {}
try {
if (collector != null) {
CollectorMonitorBind collectorMonitorBind = CollectorMonitorBind.builder()
.collector(collector)
.monitorId(monitorId)
.build();
collectorMonitorBindDao.save(collectorMonitorBind);
}
monitor.setId(monitorId);
monitor.setJobId(jobId);
// create grafana dashboard
if (monitor.getApp().equals(CommonConstants.PROMETHEUS) && grafanaDashboard != null && grafanaDashboard.isEnabled()) {
dashboardService.createOrUpdateDashboard(grafanaDashboard.getTemplate(), monitorId);
}
monitorDao.save(monitor);
paramDao.saveAll(params);
} catch (Exception e) {
log.error("Error while adding monitor: {}", e.getMessage(), e);
collectJobScheduling.cancelAsyncCollectJob(jobId);
throw new MonitorDatabaseException(e.getMessage());
}
}
@Override
@@ -233,7 +286,7 @@ public class MonitorServiceImpl implements MonitorService {
}
}
// the dispatch collector must exist if pin
if (StringUtils.isNotBlank(monitorDto.getCollector())) {
if (StringUtils.hasText(monitorDto.getCollector())) {
Optional<Collector> optionalCollector = collectorDao.findCollectorByName(monitorDto.getCollector());
if (optionalCollector.isEmpty()) {
throw new IllegalArgumentException("The pinned collector does not exist.");
@@ -250,7 +303,7 @@ public class MonitorServiceImpl implements MonitorService {
if (paramDefine.isRequired() && (param == null || param.getParamValue() == null)) {
throw new IllegalArgumentException("Params field " + field + " is required.");
}
if (param != null && param.getParamValue() != null && StringUtils.isNotBlank(param.getParamValue())) {
if (param != null && StringUtils.hasText(param.getParamValue())) {
switch (paramDefine.getType()) {
case "number":
double doubleValue;
@@ -376,7 +429,6 @@ public class MonitorServiceImpl implements MonitorService {
@Override
@Transactional(rollbackFor = Exception.class)
public void modifyMonitor(Monitor monitor, List<Param> params, String collector, GrafanaDashboard grafanaDashboard) throws RuntimeException {
final Optional<Param> sdParam = SdMonitorOperator.getSdParam(params);
long monitorId = monitor.getId();
// Check to determine whether the monitor corresponding to the monitor id exists
Optional<Monitor> queryOption = monitorDao.findById(monitorId);
@@ -394,35 +446,20 @@ public class MonitorServiceImpl implements MonitorService {
monitor.setLabels(labels);
}
List<Label> addLabels = labelService.determineNewLabels(labels.entrySet());
if (!addLabels.isEmpty()) {
labelDao.saveAll(addLabels);
}
boolean isStatic = CommonConstants.SCRAPE_STATIC.equals(monitor.getScrape()) || !StringUtils.hasText(monitor.getScrape());
if (preMonitor.getStatus() != CommonConstants.MONITOR_PAUSED_CODE) {
// Construct the collection task Job entity
Job appDefine = appService.getAppDefine(monitor.getApp());
if (sdParam.isPresent()) {
// not allow to modify a sub monitor to main monitor
labels.entrySet()
.stream()
.filter(label -> org.apache.commons.lang3.StringUtils.equals(label.getKey(), CommonConstants.TAG_AUTO_CREATED))
.findFirst()
.ifPresent(label -> {
final MonitorBind isSubMonitorBefore = monitorBindDao.findMonitorBindByBizIdAndMonitorIdAndType(Long.valueOf(label.getValue()),
monitorId, CommonConstants.MONITOR_BIND_TYPE_SD_SUB_MONITOR);
if (Objects.nonNull(isSubMonitorBefore)) {
throw new UnsupportedOperationException("Can not change a auto-created monitor to sd! ");
}
});
// create main monitor bind
final List<MonitorBind> mainMonitorBind = monitorBindDao.findMonitorBindByBizIdAndType(monitorId, CommonConstants.MONITOR_BIND_TYPE_SD_MAIN_MONITOR);
if (CollectionUtils.isEmpty(mainMonitorBind)) {
monitorBindDao.save(Objects.requireNonNull(
SdMonitorOperator.buildSdMainMonitorBind(
SdMonitorParam.builder().sdParam(sdParam.get()).build(), monitorId
)));
}
appDefine = SdMonitorOperator.constructSdJob(appDefine, sdParam.get());
labels.put(CommonConstants.TAG_SD_MAIN_MONITOR, ServiceDiscoveryProtocol.Type.getType(sdParam.get().getField()).name());
String app = isStatic ? monitor.getApp() : monitor.getScrape();
Job appDefine = appService.getAppDefine(app);
if (!isStatic) {
appDefine.setSd(true);
}
if (CommonConstants.PROMETHEUS.equals(monitor.getApp())) {
appDefine.setApp(CommonConstants.PROMETHEUS_APP_PREFIX + monitor.getName());
}
@@ -450,8 +487,7 @@ public class MonitorServiceImpl implements MonitorService {
// execute only in non paused status
try {
detectMonitor(monitor, params, collector);
} catch (Exception ignored) {
}
} catch (Exception ignored) {}
}
// After the update is successfully released, refresh the database
@@ -474,12 +510,6 @@ public class MonitorServiceImpl implements MonitorService {
}
}
monitorDao.save(monitor);
// delete all sub monitor if sd param is removed
final List<MonitorBind> subMonitorBindList = monitorBindDao.findMonitorBindByBizIdAndType(monitorId, CommonConstants.MONITOR_BIND_TYPE_SD_SUB_MONITOR);
if (!CollectionUtils.isEmpty(subMonitorBindList) && sdParam.isEmpty()) {
deleteMonitors(subMonitorBindList.stream().map(MonitorBind::getMonitorId).collect(Collectors.toSet()));
}
paramDao.saveAll(params);
} catch (Exception e) {
log.error(e.getMessage(), e);
@@ -501,18 +531,16 @@ public class MonitorServiceImpl implements MonitorService {
if (CollectionUtils.isEmpty(ids)) {
return;
}
final List<MonitorBind> allSubMonitorBind = monitorBindDao.findAllByBizIdInAndType(ids, CommonConstants.MONITOR_BIND_TYPE_SD_SUB_MONITOR);
if (!CollectionUtils.isEmpty(allSubMonitorBind)) {
ids.addAll(allSubMonitorBind.stream().map(MonitorBind::getMonitorId).collect(Collectors.toSet()));
}
List<Monitor> monitors = monitorDao.findMonitorsByIdIn(ids);
Set<Long> subMonitorIds = monitorBindDao.findMonitorBindsByBizIdIn(ids).stream().map(MonitorBind::getMonitorId).collect(Collectors.toSet());
Set<Long> allMonitorIds = new HashSet<>(ids);
allMonitorIds.addAll(subMonitorIds);
List<Monitor> monitors = monitorDao.findMonitorsByIdIn(allMonitorIds);
if (!monitors.isEmpty()) {
monitorDao.deleteAll(monitors);
paramDao.deleteParamsByMonitorIdIn(ids);
Set<Long> monitorIds = monitors.stream().map(Monitor::getId).collect(Collectors.toSet());
alertDefineBindDao.deleteAlertDefineMonitorBindsByMonitorIdIn(monitorIds);
monitorBindDao.deleteMonitorBindByBizIdIn(monitorIds);
for (Monitor monitor : monitors) {
monitorBindDao.deleteByMonitorId(monitor.getId());
collectorMonitorBindDao.deleteCollectorMonitorBindsByMonitorId(monitor.getId());
@@ -537,7 +565,9 @@ public class MonitorServiceImpl implements MonitorService {
monitorDto.setMetrics(metrics);
monitorDto.setGrafanaDashboard(dashboardService.getDashboardByMonitorId(id));
} else {
Job job = appService.getAppDefine(monitor.getApp());
boolean isStatic = CommonConstants.SCRAPE_STATIC.equals(monitor.getScrape()) || !StringUtils.hasText(monitor.getScrape());
String type = isStatic ? monitor.getApp() : monitor.getScrape();
Job job = appService.getAppDefine(type);
List<String> metrics = job.getMetrics().stream()
.filter(Metrics::isVisible)
.map(Metrics::getName).collect(Collectors.toList());
@@ -563,7 +593,7 @@ public class MonitorServiceImpl implements MonitorService {
}
andList.add(inPredicate);
}
if (StringUtils.isNotBlank(app)) {
if (StringUtils.hasText(app)) {
Predicate predicateApp = criteriaBuilder.equal(root.get("app"), app);
andList.add(predicateApp);
}
@@ -575,17 +605,17 @@ public class MonitorServiceImpl implements MonitorService {
Predicate andPredicate = criteriaBuilder.and(andList.toArray(andPredicates));
List<Predicate> orList = new ArrayList<>();
if (StringUtils.isNotBlank(search)) {
if (StringUtils.hasText(search)) {
Predicate predicateHost = criteriaBuilder.like(root.get("host"), "%" + search + "%");
Predicate predicateName = criteriaBuilder.like(criteriaBuilder.lower(root.get("name")), "%" + search.toLowerCase() + "%");
if (StringUtils.isNumeric(search)){
if (CommonUtil.isNumeric(search)){
Predicate predicateId = criteriaBuilder.equal(root.get("id"), Long.parseLong(search));
orList.add(predicateId);
}
orList.add(predicateHost);
orList.add(predicateName);
}
if (StringUtils.isNotBlank(labels)) {
if (StringUtils.hasText(labels)) {
String[] labelAres = labels.split(",");
for (String label : labelAres) {
String[] labelArr = label.split(":");
@@ -620,20 +650,14 @@ public class MonitorServiceImpl implements MonitorService {
}
@Override
public void cancelManageMonitors(HashSet<Long> ids) {
public void cancelManageMonitors(Set<Long> ids) {
if (CollectionUtils.isEmpty(ids)) {
return;
}
final Set<Long> subMonitorIds = monitorBindDao.findAllByBizIdInAndType(ids, CommonConstants.MONITOR_BIND_TYPE_SD_SUB_MONITOR).stream()
.map(MonitorBind::getMonitorId)
.collect(Collectors.toSet());
if (!CollectionUtils.isEmpty(subMonitorIds)) {
ids.addAll(subMonitorIds);
}
// Update monitoring status Delete corresponding monitoring periodic task
// The jobId is not deleted, and the jobId is reused again after the management is started.
Set<Long> subMonitorIds = monitorBindDao.findMonitorBindsByBizIdIn(ids).stream().map(MonitorBind::getMonitorId).collect(Collectors.toSet());
ids.addAll(subMonitorIds);
List<Monitor> managedMonitors = monitorDao.findMonitorsByIdIn(ids)
.stream().filter(monitor ->
monitor.getStatus() != CommonConstants.MONITOR_PAUSED_CODE)
@@ -648,8 +672,10 @@ public class MonitorServiceImpl implements MonitorService {
}
@Override
public void enableManageMonitors(HashSet<Long> ids) {
public void enableManageMonitors(Set<Long> ids) {
// Update monitoring status Add corresponding monitoring periodic task
Set<Long> subMonitorIds = monitorBindDao.findMonitorBindsByBizIdIn(ids).stream().map(MonitorBind::getMonitorId).collect(Collectors.toSet());
ids.addAll(subMonitorIds);
List<Monitor> unManagedMonitors = monitorDao.findMonitorsByIdIn(ids)
.stream().filter(monitor ->
monitor.getStatus() == CommonConstants.MONITOR_PAUSED_CODE)
@@ -662,20 +688,12 @@ public class MonitorServiceImpl implements MonitorService {
for (Monitor monitor : unManagedMonitors) {
// Construct the collection task Job entity
List<Param> params = paramDao.findParamsByMonitorId(monitor.getId());
final Optional<Param> sdParam = SdMonitorOperator.getSdParam(params);
Job appDefine = appService.getAppDefine(monitor.getApp());
if (sdParam.isPresent()) {
final List<MonitorBind> mainMonitorBind = monitorBindDao.findMonitorBindByBizIdAndType(monitor.getId(), CommonConstants.MONITOR_BIND_TYPE_SD_MAIN_MONITOR);
if (CollectionUtils.isEmpty(mainMonitorBind)) {
monitorBindDao.save(Objects.requireNonNull(
SdMonitorOperator.buildSdMainMonitorBind(
SdMonitorParam.builder().sdParam(sdParam.get()).build(), monitor.getId()
)));
}
appDefine = SdMonitorOperator.constructSdJob(appDefine, sdParam.get());
boolean isStatic = CommonConstants.SCRAPE_STATIC.equals(monitor.getScrape()) || !StringUtils.hasText(monitor.getScrape());
String app = isStatic ? monitor.getApp() : monitor.getScrape();
Job appDefine = appService.getAppDefine(app);
if (!isStatic) {
appDefine.setSd(true);
}
if (CommonConstants.PROMETHEUS.equals(monitor.getApp())) {
appDefine.setApp(CommonConstants.PROMETHEUS_APP_PREFIX + monitor.getName());
}
@@ -691,7 +709,7 @@ public class MonitorServiceImpl implements MonitorService {
List<Configmap> configmaps = params.stream().map(param ->
new Configmap(param.getField(), param.getParamValue(), param.getType())).collect(Collectors.toList());
List<ParamDefine> paramDefaultValue = appDefine.getParams().stream()
.filter(item -> StringUtils.isNotBlank(item.getDefaultValue()))
.filter(item -> StringUtils.hasText(item.getDefaultValue()))
.toList();
paramDefaultValue.forEach(defaultVar -> {
if (configmaps.stream().noneMatch(item -> item.getKey().equals(defaultVar.getField()))) {
@@ -752,25 +770,6 @@ public class MonitorServiceImpl implements MonitorService {
}).filter(Objects::nonNull).collect(Collectors.toList());
}
@Override
@Transactional(rollbackFor = Exception.class)
public void copyMonitors(List<Long> ids) {
ids.stream().parallel().forEach(id -> {
// get monitor and Params according id
Optional<Monitor> monitorOpt = monitorDao.findById(id);
List<Param> params = paramDao.findParamsByMonitorId(id);
monitorOpt.ifPresentOrElse(monitor -> {
// deep copy original monitor to achieve persist in JPA
Monitor newMonitor = JsonUtil.fromJson(JsonUtil.toJson(monitor), Monitor.class);
if (newMonitor != null) {
copyMonitor(newMonitor, params);
}
}, () -> log.warn("can not find the monitor for id {}", id));
});
}
@Override
public void updateAppCollectJob(Job job) {
List<Monitor> monitors = monitorDao.findMonitorsByAppEquals(job.getApp())
@@ -807,7 +806,7 @@ public class MonitorServiceImpl implements MonitorService {
List<Configmap> configmaps = params.stream().map(param -> new Configmap(param.getField(),
param.getParamValue(), param.getType())).collect(Collectors.toList());
List<ParamDefine> paramDefaultValue = appDefine.getParams().stream()
.filter(item -> StringUtils.isNotBlank(item.getDefaultValue()))
.filter(item -> StringUtils.hasText(item.getDefaultValue()))
.toList();
paramDefaultValue.forEach(defaultVar -> {
if (configmaps.stream().noneMatch(item -> item.getKey().equals(defaultVar.getField()))) {
@@ -827,80 +826,7 @@ public class MonitorServiceImpl implements MonitorService {
}
}
}
@Override
public void addAndSaveMonitorJob(Monitor monitor, List<Param> params, String collector, SdMonitorParam sdMonitorParam, GrafanaDashboard grafanaDashboard) {
// Apply for monitor id
long monitorId = SnowFlakeIdGenerator.generateId();
Map<String, String> labels = monitor.getLabels();
if (labels == null) {
labels = new HashMap<>(8);
monitor.setLabels(labels);
}
MonitorBind monitorBind = null;
// Construct the collection task Job entity
Job appDefine = appService.getAppDefine(monitor.getApp());
if (Objects.nonNull(sdMonitorParam)) {
monitorBind = Objects.isNull(sdMonitorParam.getSdParam())
? SdMonitorOperator.buildSdSubMonitorBind(sdMonitorParam, monitorId, labels)
: SdMonitorOperator.buildSdMainMonitorBind(sdMonitorParam, monitorId);
appDefine = SdMonitorOperator.constructSdJobAndTag(sdMonitorParam, labels, appDefine);
}
if (CommonConstants.PROMETHEUS.equals(monitor.getApp())) {
appDefine.setApp(CommonConstants.PROMETHEUS_APP_PREFIX + monitor.getName());
}
appDefine.setMonitorId(monitorId);
appDefine.setDefaultInterval(monitor.getIntervals());
appDefine.setCyclic(true);
appDefine.setTimestamp(System.currentTimeMillis());
Map<String, String> metadata = Map.of(CommonConstants.LABEL_INSTANCE_NAME, monitor.getName(),
CommonConstants.LABEL_INSTANCE_HOST, monitor.getHost());
appDefine.setMetadata(metadata);
appDefine.setLabels(monitor.getLabels());
appDefine.setAnnotations(monitor.getAnnotations());
resetMetricsCommonField(monitor, appDefine, sdMonitorParam);
List<Configmap> configmaps = params.stream().map(param -> {
param.setMonitorId(monitorId);
return new Configmap(param.getField(), param.getParamValue(), param.getType());
}).collect(Collectors.toList());
appDefine.setConfigmap(configmaps);
long jobId = collector == null ? collectJobScheduling.addAsyncCollectJob(appDefine, null) :
collectJobScheduling.addAsyncCollectJob(appDefine, collector);
try {
detectMonitor(monitor, params, collector);
} catch (Exception ignored) {
}
try {
if (collector != null) {
CollectorMonitorBind collectorMonitorBind = CollectorMonitorBind.builder()
.collector(collector)
.monitorId(monitorId)
.build();
collectorMonitorBindDao.save(collectorMonitorBind);
}
monitor.setId(monitorId);
monitor.setJobId(jobId);
// create grafana dashboard
if (monitor.getApp().equals(CommonConstants.PROMETHEUS) && grafanaDashboard != null && grafanaDashboard.isEnabled()) {
dashboardService.createOrUpdateDashboard(grafanaDashboard.getTemplate(), monitorId);
}
monitorDao.save(monitor);
if (Objects.nonNull(monitorBind)) {
monitorBindDao.save(monitorBind);
}
paramDao.saveAll(params);
} catch (Exception e) {
log.error("Error while adding monitor: {}", e.getMessage(), e);
collectJobScheduling.cancelAsyncCollectJob(jobId);
throw new MonitorDatabaseException(e.getMessage());
}
}
@Override
public Monitor getMonitor(Long monitorId) {
return monitorDao.findById(monitorId).orElse(null);
@@ -916,73 +842,69 @@ public class MonitorServiceImpl implements MonitorService {
return monitorDao.findMonitorsByAppEquals(app);
}
private void resetMetricsCommonField(Monitor monitor, Job appDefine, SdMonitorParam sdMonitorParam) {
if (Objects.isNull(sdMonitorParam)
|| org.apache.commons.lang3.StringUtils.isAnyBlank(sdMonitorParam.getDetectedHost(), sdMonitorParam.getDetectedPort())) {
return;
@Override
@Transactional(rollbackFor = Exception.class)
public void copyMonitor(Long id) {
// Get the source monitor information
Optional<Monitor> monitorOptional = monitorDao.findById(id);
if (monitorOptional.isEmpty()) {
throw new IllegalArgumentException("Monitor not found: " + id);
}
for (Metrics metric : appDefine.getMetrics()) {
Arrays.stream(metric.getClass().getDeclaredFields())
.filter(field -> metric.getProtocol().equalsIgnoreCase(field.getName()))
.findFirst()
.ifPresent(field -> {
field.setAccessible(true);
final Object obj;
try {
obj = field.get(metric);
if (obj instanceof CommonRequestProtocol protocol) {
protocol.setHost(sdMonitorParam.getDetectedHost());
protocol.setPort(sdMonitorParam.getDetectedPort());
}
} catch (IllegalAccessException exception) {
log.warn("Not such field {}", field.getName());
}
});
Monitor sourceMonitor = monitorOptional.get();
// Get the parameters of source monitor
List<Param> sourceParams = paramDao.findParamsByMonitorId(id);
// Create new monitor object
Monitor newMonitor = new Monitor();
// Copy basic properties, exclude ID, jobId and status
BeanUtils.copyProperties(sourceMonitor, newMonitor, "id", "jobId", "status");
// Set new name
newMonitor.setName(sourceMonitor.getName() + "_copy");
// Set initial status
newMonitor.setStatus(CommonConstants.MONITOR_UP_CODE);
// Set create and update time
newMonitor.setGmtCreate(LocalDateTime.now());
newMonitor.setGmtUpdate(LocalDateTime.now());
// Copy parameters
List<Param> newParams = new ArrayList<>();
if (!sourceParams.isEmpty()) {
for (Param sourceParam : sourceParams) {
Param newParam = new Param();
BeanUtils.copyProperties(sourceParam, newParam, "id");
newParam.setMonitorId(newMonitor.getId());
newParams.add(newParam);
}
}
monitor.setHost(sdMonitorParam.getDetectedHost());
Map<String, String> labels = monitor.getLabels();
if (labels == null) {
labels = new HashMap<>(8);
monitor.setLabels(labels);
}
labels.put(CommonConstants.TAG_MONITOR_HOST, sdMonitorParam.getDetectedHost());
monitor.setName(monitor.getApp().toUpperCase() + CommonConstants.LOCALE_SEPARATOR + sdMonitorParam.getDetectedHost()
+ CommonConstants.LOCALE_SEPARATOR + SnowFlakeIdGenerator.generateId());
addMonitor(newMonitor, newParams, null, null);
}
private void copyMonitor(Monitor monitor, List<Param> params) {
monitor.setName(String.format("%s - copy", monitor.getName()));
addMonitor(monitor, params, null, null);
}
private void collectOneTimeSdData(Monitor monitor, String collector, Param sdParam) {
private void detectSdMonitor(Monitor monitor, List<Param> params, String collector) {
Long monitorId = monitor.getId();
if (monitorId == null || monitorId == 0) {
monitorId = MONITOR_ID_TMP;
}
Job appDefine = appService.getAppDefine(monitor.getApp());
if (CommonConstants.PROMETHEUS.equals(monitor.getApp())) {
appDefine.setApp(CommonConstants.PROMETHEUS_APP_PREFIX + monitor.getName());
if (monitor.getScrape() == null || params == null || params.isEmpty()) {
throw new IllegalArgumentException("scrape params is null or empty!");
}
Job appDefine = appService.getAppDefine(monitor.getScrape());
appDefine.setMonitorId(monitorId);
appDefine.setCyclic(false);
appDefine.setTimestamp(System.currentTimeMillis());
Map<String, String> metadata = Map.of(CommonConstants.LABEL_INSTANCE_NAME, monitor.getName(),
CommonConstants.LABEL_INSTANCE_HOST, monitor.getHost());
Map<String, String> metadata = Map.of(CommonConstants.LABEL_INSTANCE_NAME, monitor.getName());
appDefine.setMetadata(metadata);
appDefine.setLabels(monitor.getLabels());
appDefine.setAnnotations(monitor.getAnnotations());
final Job sdJob = SdMonitorOperator.constructSdJob(appDefine, sdParam);
List<Configmap> configmaps = params.stream().map(param ->
new Configmap(param.getField(), param.getParamValue(), param.getType())).collect(Collectors.toList());
appDefine.setConfigmap(configmaps);
appDefine.setSd(true);
List<CollectRep.MetricsData> collectRep;
if (collector != null) {
collectRep = collectJobScheduling.collectSyncJobData(sdJob, collector);
collectRep = collectJobScheduling.collectSyncJobData(appDefine, collector);
} else {
collectRep = collectJobScheduling.collectSyncJobData(sdJob);
collectRep = collectJobScheduling.collectSyncJobData(appDefine);
}
monitor.setStatus(CommonConstants.MONITOR_UP_CODE);
// If the detection result fails, a detection exception is thrown
if (collectRep == null || collectRep.isEmpty()) {
monitor.setStatus(CommonConstants.MONITOR_DOWN_CODE);
@@ -1038,95 +960,4 @@ public class MonitorServiceImpl implements MonitorService {
}
collectRep.forEach(CollectRep.MetricsData::close);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void copyMonitor(Long id) {
// Get the source monitor information
Optional<Monitor> monitorOptional = monitorDao.findById(id);
if (monitorOptional.isEmpty()) {
throw new IllegalArgumentException("Monitor not found: " + id);
}
Monitor sourceMonitor = monitorOptional.get();
// Get the parameters of source monitor
List<Param> sourceParams = paramDao.findParamsByMonitorId(id);
// Create new monitor object
Monitor newMonitor = new Monitor();
// Copy basic properties, exclude ID, jobId and status
BeanUtils.copyProperties(sourceMonitor, newMonitor, "id", "jobId", "status");
// Set new name
newMonitor.setName(sourceMonitor.getName() + "_copy");
// Set initial status
newMonitor.setStatus(CommonConstants.MONITOR_UP_CODE);
// Set create and update time
newMonitor.setGmtCreate(LocalDateTime.now());
newMonitor.setGmtUpdate(LocalDateTime.now());
// Generate new ID using snowflake algorithm
newMonitor.setId(SnowFlakeIdGenerator.generateId());
// Save new monitor
newMonitor = monitorDao.save(newMonitor);
// Ensure ID is set
if (newMonitor.getId() == null) {
throw new RuntimeException("Failed to generate monitor ID");
}
// Copy parameters
if (!sourceParams.isEmpty()) {
List<Param> newParams = new ArrayList<>();
for (Param sourceParam : sourceParams) {
Param newParam = new Param();
BeanUtils.copyProperties(sourceParam, newParam, "id");
newParam.setMonitorId(newMonitor.getId());
newParams.add(newParam);
}
paramDao.saveAll(newParams);
}
try {
// Build collect job
Job appDefine = appService.getAppDefine(newMonitor.getApp());
if (CommonConstants.PROMETHEUS.equals(newMonitor.getApp())) {
appDefine.setApp(CommonConstants.PROMETHEUS_APP_PREFIX + newMonitor.getName());
}
// Ensure using correct monitor ID
appDefine.setMonitorId(newMonitor.getId());
appDefine.setDefaultInterval(newMonitor.getIntervals());
appDefine.setCyclic(true);
appDefine.setTimestamp(System.currentTimeMillis());
Map<String, String> metadata = Map.of(CommonConstants.LABEL_INSTANCE_NAME, newMonitor.getName(),
CommonConstants.LABEL_INSTANCE_HOST, newMonitor.getHost());
appDefine.setMetadata(metadata);
appDefine.setLabels(newMonitor.getLabels());
appDefine.setAnnotations(newMonitor.getAnnotations());
List<Configmap> configmaps = sourceParams.stream()
.map(param -> new Configmap(param.getField(), param.getParamValue(), param.getType()))
.collect(Collectors.toList());
appDefine.setConfigmap(configmaps);
// Get collector configuration from source monitor
Optional<CollectorMonitorBind> bindOptional =
collectorMonitorBindDao.findCollectorMonitorBindByMonitorId(sourceMonitor.getId());
String collector = bindOptional.map(CollectorMonitorBind::getCollector).orElse(null);
// Dispatch collect job
long jobId = collectJobScheduling.addAsyncCollectJob(appDefine, collector);
newMonitor.setJobId(jobId);
monitorDao.save(newMonitor);
// Copy collector binding if exists
if (collector != null) {
CollectorMonitorBind newBind = CollectorMonitorBind.builder()
.collector(collector)
.monitorId(newMonitor.getId())
.build();
collectorMonitorBindDao.save(newBind);
}
} catch (Exception e) {
log.error("Create collect job error: {}", e.getMessage(), e);
throw new RuntimeException("Create collect job failed: " + e.getMessage());
}
}
}
@@ -224,7 +224,9 @@ public class PluginServiceImpl implements PluginService {
List<PluginItem> pluginItems = new ArrayList<>();
AtomicInteger pluginImplementationCount = new AtomicInteger(0);
try {
validateFilePath(jarFile);
URL jarUrl = new URL("file:" + jarFile.getAbsolutePath());
validateJarUrl(jarUrl);
try (URLClassLoader classLoader = new URLClassLoader(new URL[]{jarUrl}, this.getClass().getClassLoader());
JarFile jar = new JarFile(jarFile)) {
Enumeration<JarEntry> entries = jar.entries();
@@ -272,6 +274,35 @@ public class PluginServiceImpl implements PluginService {
return metadata;
}
/**
* Validate that the file resides within the expected directory.
*
* @param file the file to validate
*/
private void validateFilePath(File file) {
try {
String canonicalPath = file.getCanonicalPath();
String expectedDir = new File("plugin-lib").getCanonicalPath();
if (!canonicalPath.startsWith(expectedDir)) {
throw new CommonException("File is outside the allowed directory: " + canonicalPath);
}
} catch (IOException e) {
log.error("Error validating file path: {}", file.getAbsolutePath(), e);
throw new CommonException("Error validating file path: " + file.getAbsolutePath());
}
}
/**
* Validate that the URL uses the 'file:' protocol and does not point to an external resource.
*
* @param url the URL to validate
*/
private void validateJarUrl(URL url) {
if (!"file".equals(url.getProtocol())) {
throw new CommonException("Invalid URL protocol: " + url.getProtocol());
}
}
private void validateMetadata(PluginMetadata metadata) {
if (metadataDao.countPluginMetadataByName(metadata.getName()) != 0) {
throw new CommonException("A plugin named " + metadata.getName() + " already exists");
@@ -60,6 +60,9 @@ warehouse:
url: http://localhost:8428
username: root
password: root
insert:
buffer-size: 1000
flush-interval: 3
td-engine:
enabled: false
driver-class-name: com.taosdata.jdbc.rs.RestfulDriver
@@ -132,10 +132,26 @@ warehouse:
# (please set this configuration reasonably as history records can affect performance when it is large)
max-history-record-num: 6000
victoria-metrics:
# Standalone mode toggle — must be set to false when using cluster mode
enabled: false
url: http://localhost:8428
username: root
password: root
insert:
buffer-size: 1000
flush-interval: 3
cluster:
enabled: false
select:
url: http://localhost:8481
username: root
password: root
insert:
url: http://localhost:8480
username: root
password: root
buffer-size: 1000
flush-interval: 3
td-engine:
enabled: false
driver-class-name: com.taosdata.jdbc.rs.RestfulDriver

Some files were not shown because too many files have changed in this diff Show More