Compare commits

..
Author SHA1 Message Date
Logic 047d59381c Merge branch 'master' into theme-dark-update
Signed-off-by: Logic <zqr10159@dromara.org>
2025-02-07 19:20:37 +08:00
Logic cdb4219065 style(alert-center): enhance 3D transformation and z-index layers
- Add transform-style and perspective to alert-center container
- Implement relative positioning and z-index layering for alert cards
- Adjust z-index values for expanded and hovered cards
- Update alert-details and collapse components with relative positioning
- Remove redundant perspective property from alert-cards
2025-02-07 19:18:17 +08:00
tomsun28 cabe8d8ff9 Merge branch 'master' into theme-dark-update 2025-02-07 10:43:58 +08:00
tomsun28 4ac8170d21 [webapp] bugfix edit monitor http query params error
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-02-07 00:44:07 +08:00
tomsun28 5641a5ca89 [webapp] bugfix edit monitor http query params error
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-02-07 00:38:18 +08:00
tomsun28 de8bd79ef9 [alert] update theme ui color
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-02-07 00:19:36 +08:00
tomsun28 30332969ee [alert] add multi query expr threshold unit test
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-02-06 23:41:15 +08:00
1855 changed files with 41995 additions and 152018 deletions
+36 -405
View File
@@ -6,7 +6,7 @@
"files": [
"README.md",
"README_CN.md",
"README_JP.md"
"home/src/pages/team/index.jsx"
],
"imageSize": 100,
"commit": true,
@@ -267,9 +267,7 @@
"contributions": [
"code",
"bug",
"design",
"test",
"blog"
"design"
]
},
{
@@ -363,9 +361,7 @@
"contributions": [
"bug",
"code",
"doc",
"test",
"design"
"doc"
]
},
{
@@ -687,8 +683,7 @@
"contributions": [
"code",
"test",
"doc",
"design"
"doc"
]
},
{
@@ -802,11 +797,7 @@
"profile": "https://github.com/zqr10159",
"contributions": [
"doc",
"code",
"blog",
"bug",
"test",
"design"
"code"
]
},
{
@@ -898,8 +889,7 @@
"avatar_url": "https://avatars.githubusercontent.com/u/50475131?v=4",
"profile": "https://github.com/l646505418",
"contributions": [
"code",
"bug"
"code"
]
},
{
@@ -908,8 +898,7 @@
"avatar_url": "https://avatars.githubusercontent.com/u/78642589?v=4",
"profile": "http://www.carpewang.com",
"contributions": [
"code",
"bug"
"code"
]
},
{
@@ -1269,12 +1258,12 @@
]
},
{
"login": "lynx009",
"name": "lynx009",
"login": "a-little-fool",
"name": "小笨蛋",
"avatar_url": "https://avatars.githubusercontent.com/u/105542329?v=4",
"profile": "https://github.com/lynx009",
"profile": "https://github.com/a-little-fool",
"contributions": [
"doc"
"code"
]
},
{
@@ -1337,8 +1326,7 @@
"avatar_url": "https://avatars.githubusercontent.com/u/67301054?v=4",
"profile": "http://www.jimmyqiao.top",
"contributions": [
"code",
"bug"
"code"
]
},
{
@@ -1431,21 +1419,18 @@
"avatar_url": "https://avatars.githubusercontent.com/u/28394742?v=4",
"profile": "https://github.com/Clownsw",
"contributions": [
"code",
"bug"
"code"
]
},
{
"login": "Calvin979",
"name": "Calvin",
"name": "Naruse",
"avatar_url": "https://avatars.githubusercontent.com/u/131688897?v=4",
"profile": "https://github.com/Calvin979",
"contributions": [
"doc",
"code",
"design",
"bug",
"test"
"design"
]
},
{
@@ -1484,8 +1469,7 @@
"contributions": [
"code",
"design",
"doc",
"bug"
"doc"
]
},
{
@@ -1557,9 +1541,7 @@
"avatar_url": "https://avatars.githubusercontent.com/u/42080876?v=4",
"profile": "https://github.com/yqxxgh",
"contributions": [
"doc",
"code",
"bug"
"doc"
]
},
{
@@ -1604,8 +1586,7 @@
"avatar_url": "https://avatars.githubusercontent.com/u/55646681?v=4",
"profile": "https://github.com/starmilkxin",
"contributions": [
"code",
"bug"
"code"
]
},
{
@@ -1624,11 +1605,7 @@
"profile": "https://github.com/LiuTianyou",
"contributions": [
"code",
"doc",
"bug",
"test",
"blog",
"design"
"doc"
]
},
{
@@ -1674,11 +1651,9 @@
"avatar_url": "https://avatars.githubusercontent.com/u/71874373?v=4",
"profile": "https://github.com/gjjjj0101",
"contributions": [
"test",
"code",
"doc",
"design",
"bug"
"test"
]
},
{
@@ -1687,8 +1662,7 @@
"avatar_url": "https://avatars.githubusercontent.com/u/37864182?v=4",
"profile": "https://jangto.tistory.com/",
"contributions": [
"code",
"doc"
"code"
]
},
{
@@ -1698,10 +1672,7 @@
"profile": "https://crossoverjie.top/",
"contributions": [
"code",
"doc",
"blog",
"test",
"design"
"doc"
]
},
{
@@ -1737,9 +1708,7 @@
"avatar_url": "https://avatars.githubusercontent.com/u/8465969?v=4",
"profile": "https://github.com/xuziyang",
"contributions": [
"code",
"doc",
"bug"
"code"
]
},
{
@@ -1793,13 +1762,7 @@
"avatar_url": "https://avatars.githubusercontent.com/u/29418975?v=4",
"profile": "http://www.hadoop.wiki/",
"contributions": [
"code",
"ideas",
"doc",
"blog",
"bug",
"test",
"design"
"code"
]
},
{
@@ -1808,11 +1771,7 @@
"avatar_url": "https://avatars.githubusercontent.com/u/61108539?v=4",
"profile": "https://github.com/zuobiao-zhou",
"contributions": [
"code",
"doc",
"bug",
"blog",
"test"
"code"
]
},
{
@@ -1848,11 +1807,7 @@
"avatar_url": "https://avatars.githubusercontent.com/u/25810623?v=4",
"profile": "https://github.com/Aias00",
"contributions": [
"code",
"doc",
"bug",
"ideas",
"test"
"code"
]
},
{
@@ -1870,10 +1825,7 @@
"avatar_url": "https://avatars.githubusercontent.com/u/77964041?v=4",
"profile": "https://blog.csdn.net/qq_52397471",
"contributions": [
"code",
"bug",
"test",
"blog"
"code"
]
},
{
@@ -1882,9 +1834,7 @@
"avatar_url": "https://avatars.githubusercontent.com/u/118667222?v=4",
"profile": "https://github.com/Yanshuming1",
"contributions": [
"code",
"doc",
"bug"
"code"
]
},
{
@@ -1984,10 +1934,8 @@
"avatar_url": "https://avatars.githubusercontent.com/u/3371163?v=4",
"profile": "https://github.com/kerwin612",
"contributions": [
"design",
"code",
"doc",
"bug"
"design"
]
},
{
@@ -2014,9 +1962,7 @@
"avatar_url": "https://avatars.githubusercontent.com/u/69385076?v=4",
"profile": "https://github.com/pwallk",
"contributions": [
"code",
"doc",
"bug"
"code"
]
},
{
@@ -2061,8 +2007,7 @@
"avatar_url": "https://avatars.githubusercontent.com/u/83876361?v=4",
"profile": "https://github.com/keaifafafa",
"contributions": [
"code",
"bug"
"code"
]
},
{
@@ -2197,9 +2142,7 @@
"avatar_url": "https://avatars.githubusercontent.com/u/59016860?v=4",
"profile": "https://github.com/Rancho-7",
"contributions": [
"doc",
"code",
"bug"
"doc"
]
},
{
@@ -2217,11 +2160,7 @@
"avatar_url": "https://avatars.githubusercontent.com/u/91836599?v=4",
"profile": "https://zzrl.cc/",
"contributions": [
"code",
"doc",
"bug",
"test",
"blog"
"code"
]
},
{
@@ -2239,8 +2178,7 @@
"avatar_url": "https://avatars.githubusercontent.com/u/14932913?v=4",
"profile": "https://simonsigre.com/",
"contributions": [
"doc",
"code"
"doc"
]
},
{
@@ -2303,9 +2241,7 @@
"avatar_url": "https://avatars.githubusercontent.com/u/11783444?v=4",
"profile": "https://medium.com/@pjfanning",
"contributions": [
"code",
"bug",
"test"
"code"
]
},
{
@@ -2323,9 +2259,7 @@
"avatar_url": "https://avatars.githubusercontent.com/u/22274133?v=4",
"profile": "https://github.com/MasamiYui",
"contributions": [
"code",
"doc",
"bug"
"code"
]
},
{
@@ -2364,309 +2298,6 @@
"contributions": [
"code"
]
},
{
"login": "yasminvo",
"name": "yasminvo",
"avatar_url": "https://avatars.githubusercontent.com/u/107528848?v=4",
"profile": "https://github.com/yasminvo",
"contributions": [
"code"
]
},
{
"login": "notbugggg",
"name": "不关银渐层的事哦",
"avatar_url": "https://avatars.githubusercontent.com/u/147966331?v=4",
"profile": "https://github.com/notbugggg",
"contributions": [
"code",
"bug"
]
},
{
"login": "yyahang",
"name": "yyahang",
"avatar_url": "https://avatars.githubusercontent.com/u/90464876?v=4",
"profile": "https://github.com/yyahang",
"contributions": [
"code"
]
},
{
"login": "JuJinPark",
"name": "jujin",
"avatar_url": "https://avatars.githubusercontent.com/u/44892459?v=4",
"profile": "https://github.com/JuJinPark",
"contributions": [
"code",
"doc",
"ideas",
"blog"
]
},
{
"login": "LL-LIN",
"name": "LL-LIN",
"avatar_url": "https://avatars.githubusercontent.com/u/43002118?v=4",
"profile": "https://github.com/LL-LIN",
"contributions": [
"code",
"bug"
]
},
{
"login": "bigcyy",
"name": "Yang Chen",
"avatar_url": "https://avatars.githubusercontent.com/u/73413979?v=4",
"profile": "https://bigcyy.github.io/",
"contributions": [
"code",
"doc",
"bug"
]
},
{
"login": "sarthakeash",
"name": "Sarthak Arora",
"avatar_url": "https://avatars.githubusercontent.com/u/74091160?v=4",
"profile": "https://github.com/sarthakeash",
"contributions": [
"code",
"doc"
]
},
{
"login": "PengJingzhao",
"name": "彭镜肇",
"avatar_url": "https://avatars.githubusercontent.com/u/97368949?v=4",
"profile": "https://github.com/PengJingzhao",
"contributions": [
"code"
]
},
{
"login": "gagaradio",
"name": "Walter Jia",
"avatar_url": "https://avatars.githubusercontent.com/u/18532370?v=4",
"profile": "https://github.com/gagaradio",
"contributions": [
"code"
]
},
{
"login": "boyucjz",
"name": "boyucjz",
"avatar_url": "https://avatars.githubusercontent.com/u/18730041?v=4",
"profile": "https://github.com/boyucjz",
"contributions": [
"code"
]
},
{
"login": "Cyanty",
"name": "Cyanty",
"avatar_url": "https://avatars.githubusercontent.com/u/153884653?v=4",
"profile": "https://github.com/Cyanty",
"contributions": [
"code",
"doc"
]
},
{
"login": "KevinLLF",
"name": "Jay丿167",
"avatar_url": "https://avatars.githubusercontent.com/u/85452733?v=4",
"profile": "https://github.com/KevinLLF",
"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"
]
},
{
"login": "RainBondsongyg",
"name": "songyg",
"avatar_url": "https://avatars.githubusercontent.com/u/94501396?v=4",
"profile": "https://github.com/RainBondsongyg",
"contributions": [
"doc"
]
},
{
"login": "lx1229",
"name": "Liuxin",
"avatar_url": "https://avatars.githubusercontent.com/u/44620005?v=4",
"profile": "https://github.com/lx1229",
"contributions": [
"code"
]
},
{
"login": "yy549159265",
"name": "yy549159265",
"avatar_url": "https://avatars.githubusercontent.com/u/40821310?v=4",
"profile": "https://github.com/yy549159265",
"contributions": [
"code",
"test",
"design"
]
},
{
"login": "cto-huhang",
"name": "cto-huhang",
"avatar_url": "https://avatars.githubusercontent.com/u/53338629?v=4",
"profile": "https://github.com/cto-huhang",
"contributions": [
"doc"
]
},
{
"login": "Saramanda9988",
"name": "LunaRain_079",
"avatar_url": "https://avatars.githubusercontent.com/u/176664901?v=4",
"profile": "https://github.com/Saramanda9988",
"contributions": [
"doc"
]
},
{
"login": "delei",
"name": "DeleiGuo",
"avatar_url": "https://avatars.githubusercontent.com/u/17263766?v=4",
"profile": "https://github.com/delei",
"contributions": [
"doc",
"code",
"test",
"bug"
]
},
{
"login": "chingjustwe",
"name": "Rocky, Chi",
"avatar_url": "https://avatars.githubusercontent.com/u/13643747?v=4",
"profile": "https://github.com/chingjustwe",
"contributions": [
"code"
]
},
{
"login": "rowankid",
"name": "Wenqi Luo",
"avatar_url": "https://avatars.githubusercontent.com/u/18652781?v=4",
"profile": "https://github.com/rowankid",
"contributions": [
"bug"
]
},
{
"login": "tuzuy",
"name": "tuzuy",
"avatar_url": "https://avatars.githubusercontent.com/u/95274591?v=4",
"profile": "https://github.com/tuzuy",
"contributions": [
"code"
]
},
{
"login": "carlpinto25",
"name": "carl pinto",
"avatar_url": "https://avatars.githubusercontent.com/u/117299909?v=4",
"profile": "https://github.com/carlpinto25",
"contributions": [
"code"
]
},
{
"login": "cxhello",
"name": "cxhello",
"avatar_url": "https://avatars.githubusercontent.com/u/49056040?v=4",
"profile": "https://cxhello.top/",
"contributions": [
"doc"
]
},
{
"login": "jl15988",
"name": "会功夫的李白",
"avatar_url": "https://avatars.githubusercontent.com/u/70638770?v=4",
"profile": "https://github.com/jl15988",
"contributions": [
"code"
]
},
{
"login": "AlbertYang0801",
"name": "Albert.Yang",
"avatar_url": "https://avatars.githubusercontent.com/u/37127008?v=4",
"profile": "https://blog.aytop.cloud/",
"contributions": [
"code"
]
},
{
"login": "mengnankkkk",
"name": "zhou yong kang",
"avatar_url": "https://avatars.githubusercontent.com/u/150590575?v=4",
"profile": "https://blog.tokenlen.top/",
"contributions": [
"code"
]
},
{
"login": "warrobe",
"name": "warrobe",
"avatar_url": "https://avatars.githubusercontent.com/u/89446159?v=4",
"profile": "https://github.com/warrobe",
"contributions": [
"code"
]
},
{
"login": "Jetiaime",
"name": "TeAmo",
"avatar_url": "https://avatars.githubusercontent.com/u/93769000?v=4",
"profile": "https://github.com/Jetiaime",
"contributions": [
"code"
]
},
{
"login": "pentium100",
"name": "pentium100",
"avatar_url": "https://avatars.githubusercontent.com/u/27917?v=4",
"profile": "https://github.com/pentium100",
"contributions": [
"code"
]
},
{
"login": "dedyks",
"name": "Dedy Kurniawan Santoso",
"avatar_url": "https://avatars.githubusercontent.com/u/23741665?v=4",
"profile": "https://github.com/dedyks",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,
+1 -4
View File
@@ -16,7 +16,7 @@
#
github:
description: Real-time observability system with agentless, performance cluster, prometheus-compatible, custom monitoring and status page building capabilities.
description: Apache HertzBeat(incubating) is a real-time monitoring system with agentless, performance cluster, prometheus-compatible, custom monitoring and status page building capabilities.
homepage: https://hertzbeat.apache.org/
labels:
- monitoring
@@ -52,9 +52,6 @@ github:
required_pull_request_reviews:
dismiss_stale_reviews: true
required_approving_review_count: 1
# enable GitHub Dependabot to create PRs for security alerts but not for every dependency update
dependabot_alerts: true
dependabot_updates: false
notifications:
commits: notifications@hertzbeat.apache.org
issues: notifications@hertzbeat.apache.org
+1
View File
@@ -15,6 +15,7 @@
"customizations": {
"vscode": {
"settings": {},
// same extensions as Gitpod, should match /.gitpod.yml
"extensions": [
"vscjava.vscode-java-pack",
"editorconfig.editorconfig",
-2
View File
@@ -17,9 +17,7 @@
/home export-ignore
/hip export-ignore
/template-marketplace export-ignore
/.github export-ignore
/.idea export-ignore
/.devcontainer export-ignore
.gitattributes export-ignore
.gitignore export-ignore
+1 -2
View File
@@ -53,5 +53,4 @@ body:
validations:
required: false
- type: markdown
attributes:
value: "Please read the [Contribution Guideline](https://hertzbeat.apache.org/docs/community/contribution) before submitting the PR"
value: "Please read the [Contribution Guideline](https://hertzbeat.apache.org/docs/community/contribution) before submitting the PR"
+1 -1
View File
@@ -46,7 +46,7 @@ jobs:
- uses: ./script/ci/github-actions/setup-deps
- name: Build with Maven
run: mvnd clean -B package -Prelease -Dmaven.test.skip=false --file pom.xml
run: mvn clean -B package -Prelease -Dmaven.test.skip=false --file pom.xml
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v4.0.1
-2
View File
@@ -31,8 +31,6 @@ name: "CodeQL"
on:
schedule:
- cron: '21 13 * * 4'
push:
branches: [ action* ]
jobs:
analyze:
+7 -5
View File
@@ -19,7 +19,7 @@ name: DOC CI
on:
push:
branches: [ master, dev, action* ]
branches: [ master, dev ]
paths:
- 'home/**'
pull_request:
@@ -39,7 +39,7 @@ jobs:
python-version: '3.8'
- name: Check Markdown
uses: DavidAnson/markdownlint-cli2-action@992badcdf24e3b8eb7e87ff9287fe931bcb00c6e
uses: DavidAnson/markdownlint-cli2-action@v16
with:
globs: './home/**/*.md'
@@ -49,9 +49,11 @@ jobs:
- name: Dead Link Check
run: |
sudo npm install -g markdown-link-check@3.8.7
find ./home -name "*.md" > all_md_files.txt
grep -vFf ./script/ci/exclude_files.txt all_md_files.txt > to_check.txt
xargs -P 8 -a to_check.txt -I{} markdown-link-check -c ./script/ci/link_check.json -q "{}"
for file in $(find ./home -name "*.md"); do
if ! grep -Fxq "$file" ./script/ci/exclude_files.txt; then
markdown-link-check -c ./script/ci/link_check.json -q "$file"
fi
done
- name: NPM INSTALL
working-directory: home
+1 -2
View File
@@ -17,8 +17,7 @@
name: 'HertzBeat Doc Pdf Builder'
on:
push:
branches: [ page_build* ]
page_build
jobs:
build:
+4 -7
View File
@@ -22,7 +22,7 @@ name: Frontend CI
on:
push:
branches: [ master, dev, action* ]
branches: [ master, dev ]
paths:
- 'web-app/**'
pull_request:
@@ -35,12 +35,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- name: Install
- name: Yarn Install
working-directory: web-app
run: pnpm install
run: yarn
- name: EsLint Test
working-directory: web-app
run: pnpm lint:ts
run: yarn lint:ts
+1 -1
View File
@@ -22,7 +22,7 @@ name: License Checker
on:
push:
branches: [ master, dev, action* ]
branches: [ master, dev ]
pull_request:
branches: [ master, dev ]
-97
View File
@@ -1,97 +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.
name: MCP Bash Server CI
on:
push:
paths:
- 'mcp-servers/mcp-bash-server/**'
- '.github/workflows/mcp-bashserver-test.yml'
pull_request:
paths:
- 'mcp-servers/mcp-bash-server/**'
- '.github/workflows/mcp-bashserver-test.yml'
env:
CARGO_TERM_COLOR: always
RUST_VERSION: 1.88.0
jobs:
test:
name: Test MCP Bash Server
runs-on: ubuntu-latest
strategy:
matrix:
profile: [dev, release]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_VERSION }}
components: rustfmt, clippy
- name: Cache cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-registry-
- name: Cache cargo build
uses: actions/cache@v4
with:
path: target/
key: ${{ runner.os }}-cargo-build-${{ matrix.profile }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-build-${{ matrix.profile }}-
${{ runner.os }}-cargo-build-
- name: Check code format
run: cargo fmt --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml --check
- name: Run cargo check
run: |
if [ "${{ matrix.profile }}" = "release" ]; then
cargo check --release --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml
else
cargo check --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml
fi
- name: Run clippy
run: |
if [ "${{ matrix.profile }}" = "release" ]; then
cargo clippy --release --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml -- -D warnings
else
cargo clippy --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml -- -D warnings
fi
- name: Run tests
run: |
if [ "${{ matrix.profile }}" = "release" ]; then
cargo test --release --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml
else
cargo test --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml
fi
-53
View File
@@ -1,53 +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.
# 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
+3 -6
View File
@@ -31,21 +31,18 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: ./script/ci/github-actions/setup-deps
- uses: pnpm/action-setup@v4
with:
version: 10
- name: Build the Frontend
run: |
cd web-app
pnpm install
pnpm package
yarn install
yarn package
- name: Build the Backend
run: |
mvn clean install
mvn clean package -Prelease -DskipTests
cd hertzbeat-collector
cd collector
mvn clean package -Pcluster -DskipTests
- uses: docker/setup-qemu-action@v3
+3 -5
View File
@@ -6,13 +6,13 @@ Thumbs.db
build/
out/
micronaut-cli.yml
.mvn/
mvnw
mvnw.bat
*.log
package-lock.json
*.zip
jdk/
data/
!/e2e/**/data/
### STS ###
.apt_generated
@@ -54,5 +54,3 @@ node_modules
application-dev.yml
application-mysql.yml
application-pg.yml
/package.json
/yarn.lock
-7
View File
@@ -1,7 +0,0 @@
FROM gitpod/workspace-full
USER gitpod
RUN bash -c ". /home/gitpod/.sdkman/bin/sdkman-init.sh && \
sdk install java 17.0.3-ms && \
sdk default java 17.0.3-ms"
+2 -3
View File
@@ -1,6 +1,3 @@
image:
file: .gitpod.Dockerfile
tasks:
- name: Run backend
@@ -18,6 +15,8 @@ tasks:
- name: Setup backend
init: |
sdk install java 17.0.11.fx-zulu < /dev/null
sdk default java 17.0.11.fx-zulu < /dev/null
mvn clean install -DskipTests
command: |
gp sync-done setup-backend
-2
View File
@@ -66,9 +66,7 @@ header:
- '.gitpod.yml'
- 'karma.conf.js'
- 'proxy.conf.js'
- 'pnpm-lock.yaml'
- '.helmignore'
- 'script/helm/**'
- 'web-app/src/app/core/**'
- 'web-app/src/app/layout/**'
- 'web-app/src/app/routes/exception/**'
+4 -5
View File
@@ -15,7 +15,6 @@
// limitations under the License.
//
{
"globs": ["home/**/*.md"],
"config": {
"default": true,
"MD001": true,
@@ -40,9 +39,9 @@
"MD047": true,
"MD052": true
},
"ignores": [
"**/node_modules/**",
"**/target/**",
"**/dist/**"
"ignore": [
"node_modules/",
"target/",
"dist/"
]
}
-17
View File
@@ -1,17 +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.
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
+14 -10
View File
@@ -44,17 +44,19 @@ Even small corrections to typos are very welcome :)
#### Frontend start
1. Need `Node pnpm` Environment, Make sure `Node.js >= 18`
1. Need `Node Yarn` Environment, Make sure `Node.js >= 18`
2. Cd to the `web-app` directory: `cd web-app`
3. Install pnpm if not existed `npm install -g pnpm`
3. Install yarn if not existed `npm install -g yarn`
4. Install Dependencies: `pnpm install` or `pnpm install --registry=https://registry.npmmirror.com` in `web-app`
4. Install Dependencies: `yarn install` or `yarn install --registry=https://registry.npmmirror.com` in `web-app`
5. After the local backend is started, start the local frontend in the web-app directory: `pnpm start`
5. Install angular-cli globally: `yarn global add @angular/cli@15` or `yarn global add @angular/cli@15 --registry=https://registry.npmmirror.com`
6. Browser access to localhost:4200 to start, default account/password is *admin/hertzbeat*
6. After the local backend is started, start the local frontend in the web-app directory: `ng serve --open`
7. Browser access to localhost:4200 to start, default account/password is *admin/hertzbeat*
### Find tasks
@@ -196,17 +198,19 @@ Add WeChat account `ahertzbeat` to pull you into the WeChat group.
#### 前端启动
1. 需要 `nodejs pnpm` 环境, Make sure `Node.js >= 18`
1. 需要 `nodejs yarn` 环境, Make sure `Node.js >= 18`
2. 进入 `web-app` 目录: `cd web-app`
3. 安装pnpm: `npm install -g pnpm`
3. 安装yarn: `npm install -g yarn`
4. 在前端工程目录 `web-app` 下执行: `pnpm install` or `pnpm install --registry=https://registry.npmmirror.com` in `web-app`
4. 在前端工程目录 `web-app` 下执行: `yarn install` or `yarn install --registry=https://registry.npmmirror.com` in `web-app`
5. 待本地后端启动后,在web-app目录下启动本地前端 `pnpm start`
5. 全局安装 `angular-cli`: `yarn global add @angular/cli@15` or `yarn global add @angular/cli@15 --registry=https://registry.npmmirror.com`
6. 浏览器访问 localhost:4200 即可开始,默认账号密码 *admin/hertzbeat*
6. 待本地后端启动后,在web-app目录下启动本地前端 `ng serve --open`
7. 浏览器访问 localhost:4200 即可开始,默认账号密码 *admin/hertzbeat*
### 寻找任务
+10
View File
@@ -0,0 +1,10 @@
Apache HertzBeat (incubating) is an effort undergoing incubation at the Apache
Software Foundation (ASF), sponsored by the Apache Incubator PMC.
Incubation is required of all newly accepted projects until a further review
indicates that the infrastructure, communications, and decision making process
have stabilized in a manner consistent with other successful ASF projects.
While incubation status is not necessarily a reflection of the completeness
or stability of the code, it does indicate that the project has yet to be
fully endorsed by the ASF.
+2 -2
View File
@@ -1,5 +1,5 @@
Apache HertzBeat
Copyright 2024-2025 The Apache Software Foundation
Apache HertzBeat (incubating)
Copyright 2024 The Apache Software Foundation
This product includes software developed at
The Apache Software Foundation (http://www.apache.org/).
+41 -83
View File
@@ -4,23 +4,21 @@
</a>
</p>
<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>
<h4 align="center">
<a href="README.md">English Document</a> | <a href="README_CN.md">中文文档</a>
</h4>
> A real-time observability system with agentless, performance cluster, prometheus-compatible, custom monitoring and status page building capabilities.
> A real-time monitoring system with agentless, performance cluster, prometheus-compatible, custom monitoring and status page building capabilities.
[![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)
[![Twitter](https://img.shields.io/twitter/follow/hertzbeat1024?logo=twitter)](https://twitter.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)
[![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**
@@ -28,7 +26,7 @@
## 🎡 <font color="green">Introduction</font>
[Apache HertzBeat](https://github.com/apache/hertzbeat) is an easy-to-use, open source, real-time observability system with agentless, high performance cluster, prometheus-compatible, offers powerful custom monitoring and status page building capabilities.
[Apache HertzBeat](https://github.com/apache/hertzbeat) (incubating) is an easy-to-use, open source, real-time monitoring system with agentless, high performance cluster, prometheus-compatible, offers powerful custom monitoring and status page building capabilities.
### Features
@@ -224,7 +222,7 @@ Thanks to these wonderful people, welcome to join us:
<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/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></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>
@@ -238,7 +236,7 @@ Thanks to these wonderful people, welcome to join us:
<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/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></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>
@@ -280,7 +278,7 @@ Thanks to these wonderful people, welcome to join us:
<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>
<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></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>
@@ -296,7 +294,7 @@ Thanks to these wonderful people, welcome to join us:
<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/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></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>
@@ -310,8 +308,8 @@ Thanks to these wonderful people, welcome to join us:
<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/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></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></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>
@@ -361,7 +359,7 @@ Thanks to these wonderful people, welcome to join us:
<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/lynx009"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="lynx009"/><br /><sub><b>lynx009</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lynx009" title="Documentation">📖</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></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>
@@ -370,7 +368,7 @@ Thanks to these wonderful people, welcome to join us:
<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>
<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></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>
@@ -384,14 +382,14 @@ Thanks to these wonderful people, welcome to join us:
<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="Calvin"/><br /><sub><b>Calvin</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/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></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></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/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></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>
@@ -401,29 +399,29 @@ Thanks to these wonderful people, welcome to join us:
</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/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></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>
<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></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/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></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>
<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="Code">💻</a> <a href="#design-gjjjj0101" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/commits?author=gjjjj0101" title="Tests">⚠️</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://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></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></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/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></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>
@@ -431,17 +429,17 @@ Thanks to these wonderful people, welcome to join us:
<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="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></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></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/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></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://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></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></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>
@@ -456,17 +454,17 @@ Thanks to these wonderful people, welcome to join us:
<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/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="https://github.com/apache/hertzbeat/commits?author=kerwin612" title="Code">💻</a> <a href="#design-kerwin612" title="Design">🎨</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/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></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/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></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>
@@ -485,13 +483,13 @@ Thanks to these wonderful people, welcome to join us:
<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/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></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>
<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></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="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></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>
@@ -500,55 +498,15 @@ Thanks to these wonderful people, welcome to join us:
</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://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></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/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></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>
<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>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RainBondsongyg"><img src="https://avatars.githubusercontent.com/u/94501396?v=4?s=100" width="100px;" alt="songyg"/><br /><sub><b>songyg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=RainBondsongyg" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lx1229"><img src="https://avatars.githubusercontent.com/u/44620005?v=4?s=100" width="100px;" alt="Liuxin"/><br /><sub><b>Liuxin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lx1229" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yy549159265"><img src="https://avatars.githubusercontent.com/u/40821310?v=4?s=100" width="100px;" alt="yy549159265"/><br /><sub><b>yy549159265</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Tests">⚠️</a> <a href="#design-yy549159265" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cto-huhang"><img src="https://avatars.githubusercontent.com/u/53338629?v=4?s=100" width="100px;" alt="cto-huhang"/><br /><sub><b>cto-huhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cto-huhang" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Saramanda9988"><img src="https://avatars.githubusercontent.com/u/176664901?v=4?s=100" width="100px;" alt="LunaRain_079"/><br /><sub><b>LunaRain_079</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/delei"><img src="https://avatars.githubusercontent.com/u/17263766?v=4?s=100" width="100px;" alt="DeleiGuo"/><br /><sub><b>DeleiGuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Adelei" title="Bug reports">🐛</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chingjustwe"><img src="https://avatars.githubusercontent.com/u/13643747?v=4?s=100" width="100px;" alt="Rocky, Chi"/><br /><sub><b>Rocky, Chi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=chingjustwe" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rowankid"><img src="https://avatars.githubusercontent.com/u/18652781?v=4?s=100" width="100px;" alt="Wenqi Luo"/><br /><sub><b>Wenqi Luo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3Arowankid" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tuzuy"><img src="https://avatars.githubusercontent.com/u/95274591?v=4?s=100" width="100px;" alt="tuzuy"/><br /><sub><b>tuzuy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tuzuy" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/carlpinto25"><img src="https://avatars.githubusercontent.com/u/117299909?v=4?s=100" width="100px;" alt="carl pinto"/><br /><sub><b>carl pinto</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=carlpinto25" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://cxhello.top/"><img src="https://avatars.githubusercontent.com/u/49056040?v=4?s=100" width="100px;" alt="cxhello"/><br /><sub><b>cxhello</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cxhello" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jl15988"><img src="https://avatars.githubusercontent.com/u/70638770?v=4?s=100" width="100px;" alt="会功夫的李白"/><br /><sub><b>会功夫的李白</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jl15988" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://blog.aytop.cloud/"><img src="https://avatars.githubusercontent.com/u/37127008?v=4?s=100" width="100px;" alt="Albert.Yang"/><br /><sub><b>Albert.Yang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=AlbertYang0801" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://blog.tokenlen.top/"><img src="https://avatars.githubusercontent.com/u/150590575?v=4?s=100" width="100px;" alt="zhou yong kang"/><br /><sub><b>zhou yong kang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mengnankkkk" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/warrobe"><img src="https://avatars.githubusercontent.com/u/89446159?v=4?s=100" width="100px;" alt="warrobe"/><br /><sub><b>warrobe</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=warrobe" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Jetiaime"><img src="https://avatars.githubusercontent.com/u/93769000?v=4?s=100" width="100px;" alt="TeAmo"/><br /><sub><b>TeAmo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Jetiaime" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pentium100"><img src="https://avatars.githubusercontent.com/u/27917?v=4?s=100" width="100px;" alt="pentium100"/><br /><sub><b>pentium100</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pentium100" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dedyks"><img src="https://avatars.githubusercontent.com/u/23741665?v=4?s=100" width="100px;" alt="Dedy Kurniawan Santoso"/><br /><sub><b>Dedy Kurniawan Santoso</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dedyks" title="Code">💻</a></td>
</tr>
</tbody>
</table>
@@ -570,13 +528,13 @@ WeChat Group : Add friend `ahertzbeat` and invite to the group.
WeChat Public : Search ID `usthecom`.
[QQ Group](https://qm.qq.com/q/xxqecSC2cw) : Group num `1035688434`
[QQ Group](https://jq.qq.com/?_wv=1027&k=Bud9OzdI) : Group num `630061200`
[Github Discussion](https://github.com/apache/hertzbeat/discussions)
[Reddit Community](https://www.reddit.com/r/hertzbeat/)
[Follow Us Twitter](https://x.com/hertzbeat1024)
[Follow Us Twitter](https://twitter.com/hertzbeat1024)
[Subscribe YouTube](https://www.youtube.com/channel/UCri75zfWX0GHqJFPENEbLow)
@@ -592,8 +550,8 @@ HertzBeat is built on so many great open source projects, thanks to them!
<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/?item=observability-and-analysis--observability--hertzbeat">
CNCF Observability And Analysis - Observability Landscape.</a>
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
+48 -89
View File
@@ -4,16 +4,15 @@
</a>
</p>
<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>
<h4 align="center">
<a href="README_CN.md">中文文档</a> | <a href="README.md">English Document</a>
</h4>
> 实时观测系统,无需 Agent,性能集群,兼容 Prometheus,自定义监控和状态页构建能力。
> 实时监控系统,无需 Agent,性能集群,兼容 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)
[![Twitter](https://img.shields.io/twitter/follow/hertzbeat1024?logo=twitter)](https://twitter.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)
@@ -28,16 +27,16 @@
## 🎡 <font color="green">介绍</font>
[Apache HertzBeat](https://github.com/apache/hertzbeat) 是一个易用友好的开源实时监控告警系统,无需 Agent,高性能集群,兼容 Prometheus,提供强大的自定义监控和状态页构建能力。
[Apache HertzBeat](https://github.com/apache/hertzbeat) incubating是一个易用友好的开源实时监控告警系统,无需 Agent,高性能集群,兼容 Prometheus,提供强大的自定义监控和状态页构建能力。
### 特点
-**监控+告警+通知** 为一体,支持对应用服务,应用程序,数据库,缓存,操作系统,大数据,中间件,Web 服务器,云原生,网络,自定义等监控阈值告警通知一步到位。
- 易用友好,无需 `Agent`,全 `WEB` 页面操作,鼠标点一点就能监控告警,无需学习成本。
-`Http, Jmx, Ssh, Snmp, Jdbc, Prometheus` 等协议规范可配置化,只需在浏览器配置监控模 `YML` 就能使用这些协议去自定义采集想要的指标。您相信只需简单配置即可快速适配一款 `K8s``Docker` 等新的监控类型吗?
- 易用友好,无需 `Agent`,全 `WEB` 页面操作,鼠标点一点就能监控告警,零上手学习成本。
-`Http, Jmx, Ssh, Snmp, Jdbc, Prometheus` 等协议规范可配置化,只需在浏览器配置监控模 `YML` 就能使用这些协议去自定义采集想要的指标。您相信只需配置下就能立刻适配一款 `K8s``Docker` 等新的监控类型吗?
- 兼容 `Prometheus` 的系统生态并且更多,只需页面操作就可以监控 `Prometheus` 所能监控的。
- 高性能,支持多采集器集群横向扩展,支持多隔离网络监控,云边协同。
- 灵活的告警阈值规则,`邮件` `Discord` `Slack` `Telegram` `钉钉` `微信` `飞书` `短信` `Webhook` `Server酱` 等方式消息及时送达。
- 自由的告警阈值规则,`邮件` `Discord` `Slack` `Telegram` `钉钉` `微信` `飞书` `短信` `Webhook` `Server酱` 等方式消息及时送达。
- 提供强大的状态页构建能力,轻松向用户传达您产品服务的实时状态。
@@ -59,8 +58,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),
@@ -98,7 +97,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酱`
## 🐕 快速开始
@@ -126,7 +125,7 @@
- `-e IDENTITY=custom-collector-name` : 配置此采集器的唯一性标识符名称,多个采集器名称不能相同,建议自定义英文名称。
- `-e MODE=public` : 配置运行模式(public or private), 公共集群模式或私有云边模式。
- `-e MANAGER_HOST=127.0.0.1` : 配置连接主 HertzBeat 服务的对外 IP。
- `-e MANAGER_HOST=127.0.0.1` : 配置连接主 HertaBeat 服务的对外 IP。
- `-e MANAGER_PORT=1158` : 配置连接主 HertzBeat 服务的对外端口,默认1158。
@@ -222,7 +221,7 @@ Thanks these wonderful people, welcome to join us:
<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/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></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>
@@ -236,7 +235,7 @@ Thanks these wonderful people, welcome to join us:
<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/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></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>
@@ -278,7 +277,7 @@ Thanks these wonderful people, welcome to join us:
<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>
<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></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>
@@ -294,7 +293,7 @@ Thanks these wonderful people, welcome to join us:
<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/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></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>
@@ -308,8 +307,8 @@ Thanks these wonderful people, welcome to join us:
<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/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></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></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>
@@ -359,7 +358,7 @@ Thanks these wonderful people, welcome to join us:
<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/lynx009"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="lynx009"/><br /><sub><b>lynx009</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lynx009" title="Documentation">📖</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></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>
@@ -368,7 +367,7 @@ Thanks these wonderful people, welcome to join us:
<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>
<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></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>
@@ -382,14 +381,14 @@ Thanks these wonderful people, welcome to join us:
<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="Calvin"/><br /><sub><b>Calvin</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/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></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></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/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></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>
@@ -399,29 +398,29 @@ Thanks these wonderful people, welcome to join us:
</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/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></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>
<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></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/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></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>
<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="Code">💻</a> <a href="#design-gjjjj0101" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/commits?author=gjjjj0101" title="Tests">⚠️</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://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></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></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/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></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>
@@ -429,17 +428,17 @@ Thanks these wonderful people, welcome to join us:
<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="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></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></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/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></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://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></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></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>
@@ -454,17 +453,17 @@ Thanks these wonderful people, welcome to join us:
<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/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="https://github.com/apache/hertzbeat/commits?author=kerwin612" title="Code">💻</a> <a href="#design-kerwin612" title="Design">🎨</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/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></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/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></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>
@@ -483,13 +482,13 @@ Thanks these wonderful people, welcome to join us:
<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/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></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>
<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></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="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></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>
@@ -498,55 +497,15 @@ Thanks these wonderful people, welcome to join us:
</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://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></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/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></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>
<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>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RainBondsongyg"><img src="https://avatars.githubusercontent.com/u/94501396?v=4?s=100" width="100px;" alt="songyg"/><br /><sub><b>songyg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=RainBondsongyg" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lx1229"><img src="https://avatars.githubusercontent.com/u/44620005?v=4?s=100" width="100px;" alt="Liuxin"/><br /><sub><b>Liuxin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lx1229" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yy549159265"><img src="https://avatars.githubusercontent.com/u/40821310?v=4?s=100" width="100px;" alt="yy549159265"/><br /><sub><b>yy549159265</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Tests">⚠️</a> <a href="#design-yy549159265" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cto-huhang"><img src="https://avatars.githubusercontent.com/u/53338629?v=4?s=100" width="100px;" alt="cto-huhang"/><br /><sub><b>cto-huhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cto-huhang" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Saramanda9988"><img src="https://avatars.githubusercontent.com/u/176664901?v=4?s=100" width="100px;" alt="LunaRain_079"/><br /><sub><b>LunaRain_079</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/delei"><img src="https://avatars.githubusercontent.com/u/17263766?v=4?s=100" width="100px;" alt="DeleiGuo"/><br /><sub><b>DeleiGuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Adelei" title="Bug reports">🐛</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chingjustwe"><img src="https://avatars.githubusercontent.com/u/13643747?v=4?s=100" width="100px;" alt="Rocky, Chi"/><br /><sub><b>Rocky, Chi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=chingjustwe" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rowankid"><img src="https://avatars.githubusercontent.com/u/18652781?v=4?s=100" width="100px;" alt="Wenqi Luo"/><br /><sub><b>Wenqi Luo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3Arowankid" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tuzuy"><img src="https://avatars.githubusercontent.com/u/95274591?v=4?s=100" width="100px;" alt="tuzuy"/><br /><sub><b>tuzuy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tuzuy" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/carlpinto25"><img src="https://avatars.githubusercontent.com/u/117299909?v=4?s=100" width="100px;" alt="carl pinto"/><br /><sub><b>carl pinto</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=carlpinto25" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://cxhello.top/"><img src="https://avatars.githubusercontent.com/u/49056040?v=4?s=100" width="100px;" alt="cxhello"/><br /><sub><b>cxhello</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cxhello" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jl15988"><img src="https://avatars.githubusercontent.com/u/70638770?v=4?s=100" width="100px;" alt="会功夫的李白"/><br /><sub><b>会功夫的李白</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jl15988" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://blog.aytop.cloud/"><img src="https://avatars.githubusercontent.com/u/37127008?v=4?s=100" width="100px;" alt="Albert.Yang"/><br /><sub><b>Albert.Yang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=AlbertYang0801" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://blog.tokenlen.top/"><img src="https://avatars.githubusercontent.com/u/150590575?v=4?s=100" width="100px;" alt="zhou yong kang"/><br /><sub><b>zhou yong kang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mengnankkkk" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/warrobe"><img src="https://avatars.githubusercontent.com/u/89446159?v=4?s=100" width="100px;" alt="warrobe"/><br /><sub><b>warrobe</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=warrobe" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Jetiaime"><img src="https://avatars.githubusercontent.com/u/93769000?v=4?s=100" width="100px;" alt="TeAmo"/><br /><sub><b>TeAmo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Jetiaime" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pentium100"><img src="https://avatars.githubusercontent.com/u/27917?v=4?s=100" width="100px;" alt="pentium100"/><br /><sub><b>pentium100</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pentium100" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dedyks"><img src="https://avatars.githubusercontent.com/u/23741665?v=4?s=100" width="100px;" alt="Dedy Kurniawan Santoso"/><br /><sub><b>Dedy Kurniawan Santoso</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dedyks" title="Code">💻</a></td>
</tr>
</tbody>
</table>
@@ -568,13 +527,13 @@ Thanks these wonderful people, welcome to join us:
微信公众号 : 搜索 ID `usthecom`.
[QQ交流群](https://qm.qq.com/q/xxqecSC2cw) : 群号 `1035688434`
[QQ交流群](https://jq.qq.com/?_wv=1027&k=Bud9OzdI) : 群号 `630061200`
[Github Discussion](https://github.com/apache/hertzbeat/discussions)
[Reddit Community](https://www.reddit.com/r/hertzbeat/)
[Follow Us Twitter](https://x.com/hertzbeat1024)
[Follow Us Twitter](https://twitter.com/hertzbeat1024)
[Subscribe YouTube](https://www.youtube.com/channel/UCri75zfWX0GHqJFPENEbLow)
@@ -592,8 +551,8 @@ HertzBeat is built on so many great open source projects, thanks to them!
<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/?item=observability-and-analysis--observability--hertzbeat">
CNCF Observability And Analysis - Observability Landscape.</a>
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
-604
View File
@@ -1,604 +0,0 @@
<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/lynx009"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="lynx009"/><br /><sub><b>lynx009</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lynx009" title="Documentation">📖</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="Calvin"/><br /><sub><b>Calvin</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>
<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>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RainBondsongyg"><img src="https://avatars.githubusercontent.com/u/94501396?v=4?s=100" width="100px;" alt="songyg"/><br /><sub><b>songyg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=RainBondsongyg" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lx1229"><img src="https://avatars.githubusercontent.com/u/44620005?v=4?s=100" width="100px;" alt="Liuxin"/><br /><sub><b>Liuxin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lx1229" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yy549159265"><img src="https://avatars.githubusercontent.com/u/40821310?v=4?s=100" width="100px;" alt="yy549159265"/><br /><sub><b>yy549159265</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Tests">⚠️</a> <a href="#design-yy549159265" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cto-huhang"><img src="https://avatars.githubusercontent.com/u/53338629?v=4?s=100" width="100px;" alt="cto-huhang"/><br /><sub><b>cto-huhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cto-huhang" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Saramanda9988"><img src="https://avatars.githubusercontent.com/u/176664901?v=4?s=100" width="100px;" alt="LunaRain_079"/><br /><sub><b>LunaRain_079</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/delei"><img src="https://avatars.githubusercontent.com/u/17263766?v=4?s=100" width="100px;" alt="DeleiGuo"/><br /><sub><b>DeleiGuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Adelei" title="Bug reports">🐛</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chingjustwe"><img src="https://avatars.githubusercontent.com/u/13643747?v=4?s=100" width="100px;" alt="Rocky, Chi"/><br /><sub><b>Rocky, Chi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=chingjustwe" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rowankid"><img src="https://avatars.githubusercontent.com/u/18652781?v=4?s=100" width="100px;" alt="Wenqi Luo"/><br /><sub><b>Wenqi Luo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3Arowankid" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tuzuy"><img src="https://avatars.githubusercontent.com/u/95274591?v=4?s=100" width="100px;" alt="tuzuy"/><br /><sub><b>tuzuy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tuzuy" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/carlpinto25"><img src="https://avatars.githubusercontent.com/u/117299909?v=4?s=100" width="100px;" alt="carl pinto"/><br /><sub><b>carl pinto</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=carlpinto25" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://cxhello.top/"><img src="https://avatars.githubusercontent.com/u/49056040?v=4?s=100" width="100px;" alt="cxhello"/><br /><sub><b>cxhello</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cxhello" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jl15988"><img src="https://avatars.githubusercontent.com/u/70638770?v=4?s=100" width="100px;" alt="会功夫的李白"/><br /><sub><b>会功夫的李白</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jl15988" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://blog.aytop.cloud/"><img src="https://avatars.githubusercontent.com/u/37127008?v=4?s=100" width="100px;" alt="Albert.Yang"/><br /><sub><b>Albert.Yang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=AlbertYang0801" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://blog.tokenlen.top/"><img src="https://avatars.githubusercontent.com/u/150590575?v=4?s=100" width="100px;" alt="zhou yong kang"/><br /><sub><b>zhou yong kang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mengnankkkk" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/warrobe"><img src="https://avatars.githubusercontent.com/u/89446159?v=4?s=100" width="100px;" alt="warrobe"/><br /><sub><b>warrobe</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=warrobe" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Jetiaime"><img src="https://avatars.githubusercontent.com/u/93769000?v=4?s=100" width="100px;" alt="TeAmo"/><br /><sub><b>TeAmo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Jetiaime" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pentium100"><img src="https://avatars.githubusercontent.com/u/27917?v=4?s=100" width="100px;" alt="pentium100"/><br /><sub><b>pentium100</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pentium100" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dedyks"><img src="https://avatars.githubusercontent.com/u/23741665?v=4?s=100" width="100px;" alt="Dedy Kurniawan Santoso"/><br /><sub><b>Dedy Kurniawan Santoso</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dedyks" 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/?item=observability-and-analysis--observability--hertzbeat">
CNCF Observability And Analysis - Observability Landscape.</a>
</p>
## 🛡️ License
[`Apache License, Version 2.0`](https://www.apache.org/licenses/LICENSE-2.0.html)
+3 -3
View File
@@ -1,6 +1,6 @@
# Security Policy
Apache HertzBeat follows the [ASF vulnerability handling process](https://apache.org/security/#vulnerability-handling)
Apache HertzBeat follows the [ASF vulnerability handling process](https://apache.org/security/#vulnerability-handling)
## Supported Versions
@@ -17,6 +17,6 @@ To report a new vulnerability you have discovered please follow the [ASF vulnera
## Extra Information
- [Apache HertzBeat Security Model](https://hertzbeat.apache.org/docs/help/security_model)
- [Apache HertzBeat Security Model](https://hertzbeat.apache.org/docs/help/security_model)
- [ASF Security](https://www.apache.org/security/)
- [Apache HertzBeat Document](https://hertzbeat.apache.org/docs/)
- [Apache HertzBeat Document](https://hertzbeat.apache.org/docs/)
-5
View File
@@ -29,11 +29,6 @@
"type": 0,
"paramValue": 1000
},
{
"field": "ssl",
"type": 1,
"paramValue": false
},
{
"field": "username",
"type": 1
-5
View File
@@ -73,11 +73,6 @@
"field": "successCode",
"type": 4,
"paramValue": "200, 201"
},
{
"field": "enableUrlEncoding",
"type": 1,
"paramValue": true
}
]
}
-39
View File
@@ -1,39 +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.
# 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
@@ -1 +0,0 @@
K10800b7e6b1f6390067e433bd30dff5f925893b60074f8c36f7725bfdce6a45917::server:ca7449eb0dddd8a4bb6e5ca86e2d0cdd
-37
View File
@@ -1,37 +0,0 @@
{
"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
@@ -1,69 +0,0 @@
#!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
@@ -1,123 +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.
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
+19 -15
View File
@@ -29,7 +29,7 @@ param:
monitorUDP: "{{randAlpha 6}}"
monitorWebsite: "{{randAlpha 6}}"
monitorFTP: "{{randAlpha 6}}"
labelName: "{{randAlpha 3}}"
tagName: "{{randAlpha 3}}"
items:
- name: login
request:
@@ -57,7 +57,7 @@ items:
"credential": "hertzbeat"
}
expect:
statusCode: 500
statusCode: 409
- name: monitorList
request:
api: /api/monitors?pageIndex=0&pageSize=8
@@ -342,35 +342,39 @@ items:
header:
Authorization: Bearer {{.login.data.token}}
## Label
- name: createLabel
## Tag
- name: createTag
request:
api: /api/label
api: /api/tag
method: POST
header:
Authorization: Bearer {{.login.data.token}}
Content-type: application/json
body: |
{
"name": "{{.param.labelName}}"
}
- name: listLabels
[
{
"color": "#ff4081",
"name": "{{.param.tagName}}"
}
]
- name: listTags
request:
api: /api/label?pageIndex=0&pageSize=8&type=1
api: /api/tag?pageIndex=0&pageSize=8&type=1
header:
Authorization: Bearer {{.login.data.token}}
- name: updateLabel
- name: updateTag
request:
api: /api/label
api: /api/tag
method: PUT
header:
Authorization: Bearer {{.login.data.token}}
Content-type: application/json
body: |
{
"id": {{(index .listLabels.data.content 0).id | int64}},
"id": {{(index .listTags.data.content 0).id | int64}},
"name": "{{randAlpha 3}}",
"tagValue": "{{randAlpha 3}}",
"color": "#ff4081",
"type": 1,
"creator": "admin",
"modifier": "admin"
@@ -378,9 +382,9 @@ items:
expect:
bodyFieldsExpect:
code: "0"
- name: deleteLabel
- name: deleteTag
request:
api: /api/label?ids={{(index .listLabels.data.content 0).id | int64}}
api: /api/tag?ids={{(index .listTags.data.content 0).id | int64}}
method: DELETE
header:
Authorization: Bearer {{.login.data.token}}
-106
View File
@@ -1,106 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat</artifactId>
<version>2.0-SNAPSHOT</version>
</parent>
<artifactId>hertzbeat-ai</artifactId>
<version>${hertzbeat.version}</version>
<properties>
<spring-ai.version>1.0.3</spring-ai.version>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-client-chat</artifactId>
</dependency>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-common</artifactId>
</dependency>
<!-- common -->
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-base</artifactId>
</dependency>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-alerter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-manager</artifactId>
</dependency>
<dependency>
<groupId>com.usthe.sureness</groupId>
<artifactId>spring-boot3-starter-sureness</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -1,246 +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.ai.config;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.usthe.sureness.mgt.SurenessSecurityManager;
import com.usthe.sureness.subject.SubjectSum;
import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpServerSession;
import io.modelcontextprotocol.spec.McpServerTransport;
import io.modelcontextprotocol.spec.McpServerTransportProvider;
import io.modelcontextprotocol.util.Assert;
import java.io.IOException;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import jakarta.servlet.http.HttpServletRequest;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.RouterFunctions;
import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.function.ServerResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* Custom Server-Sent Events transport provider for Model Context Protocol.
*/
@Slf4j
public class CustomSseServerTransport implements McpServerTransportProvider {
private final ObjectMapper objectMapper;
private final String messageEndpoint;
private final String sseEndpoint;
private final String baseUrl;
@Getter
private final RouterFunction<ServerResponse> routerFunction;
@Setter
private McpServerSession.Factory sessionFactory;
private final Map<String, Object> sessionRequest = new HashMap<>();
private final ConcurrentHashMap<String, McpServerSession> sessions;
private volatile boolean isClosing;
public CustomSseServerTransport(ObjectMapper objectMapper, String messageEndpoint) {
this(objectMapper, messageEndpoint, "/sse");
}
public CustomSseServerTransport(ObjectMapper objectMapper, String messageEndpoint, String sseEndpoint) {
this(objectMapper, "", messageEndpoint, sseEndpoint);
}
public CustomSseServerTransport(ObjectMapper objectMapper, String baseUrl, String messageEndpoint, String sseEndpoint) {
this.sessions = new ConcurrentHashMap();
this.isClosing = false;
Assert.notNull(objectMapper, "ObjectMapper must not be null");
Assert.notNull(baseUrl, "Message base URL must not be null");
Assert.notNull(messageEndpoint, "Message endpoint must not be null");
Assert.notNull(sseEndpoint, "SSE endpoint must not be null");
this.objectMapper = objectMapper;
this.baseUrl = baseUrl;
this.messageEndpoint = messageEndpoint;
this.sseEndpoint = sseEndpoint;
this.routerFunction = RouterFunctions.route().GET(this.sseEndpoint, this::handleSseConnection).POST(this.messageEndpoint, this::handleMessage).build();
}
public Mono<Void> notifyClients(String method, Object params) {
if (this.sessions.isEmpty()) {
log.debug("No active sessions to broadcast message to");
return Mono.empty();
} else {
log.debug("Attempting to broadcast message to {} active sessions", this.sessions.size());
return Flux.fromIterable(this.sessions.values())
.flatMap((session) -> session.sendNotification(method, params)
.doOnError((e) -> log.error("Failed to send message to session {}: {}", session.getId(), e.getMessage()))
.onErrorComplete())
.then();
}
}
public Mono<Void> closeGracefully() {
return Flux.fromIterable(this.sessions.values()).doFirst(() -> {
this.isClosing = true;
log.debug("Initiating graceful shutdown with {} active sessions", this.sessions.size());
}).flatMap(McpServerSession::closeGracefully).then().doOnSuccess((v) -> log.debug("Graceful shutdown completed"));
}
private ServerResponse handleSseConnection(ServerRequest request) {
log.debug("Handling SSE connection for request: {}", request);
HttpServletRequest servletRequest = request.servletRequest();
try {
log.debug("Processing SSE connection for servlet request: {}", servletRequest);
log.debug("Authorization header: {}", servletRequest.getHeader("Authorization"));
} catch (Exception e) {
log.error("Authentication failed for SSE connection: {}", e.getMessage());
return ServerResponse.status(HttpStatus.UNAUTHORIZED).body("Unauthorized: " + e.getMessage());
}
if (this.isClosing) {
return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).body("Server is shutting down");
} else {
String sessionId = UUID.randomUUID().toString();
log.debug("Generated session ID for SSE connection: {}", sessionId);
log.debug("Creating new SSE connection for session: {}", sessionId);
return ServerResponse.sse((sseBuilder) -> {
sseBuilder.onComplete(() -> {
log.debug("SSE connection completed for session: {}", sessionId);
this.sessions.remove(sessionId);
});
sseBuilder.onTimeout(() -> {
log.debug("SSE connection timed out for session: {}", sessionId);
this.sessions.remove(sessionId);
});
CustomSseServerTransport.WebMvcMcpSessionTransport sessionTransport = new CustomSseServerTransport.WebMvcMcpSessionTransport(sessionId, sseBuilder);
McpServerSession session = this.sessionFactory.create(sessionTransport);
this.sessionRequest.put(sessionId, request.servletRequest());
this.sessions.put(sessionId, session);
try {
sseBuilder.id(sessionId).event("endpoint").data(this.baseUrl + this.messageEndpoint + "?sessionId=" + sessionId);
} catch (Exception e) {
log.error("Failed to send initial endpoint event: {}", e.getMessage());
sseBuilder.error(e);
}
}, Duration.ZERO);
}
}
private ServerResponse handleMessage(ServerRequest request) {
if (this.isClosing) {
return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).body("Server is shutting down");
} else if (request.param("sessionId").isEmpty()) {
return ServerResponse.badRequest().body(new McpError("Session ID missing in message endpoint"));
} else {
String sessionId = (String) request.param("sessionId").get();
McpServerSession session = (McpServerSession) this.sessions.get(sessionId);
log.debug("Authorization header for message request: {}", request.servletRequest().getHeader("Authorization"));
SubjectSum subject = SurenessSecurityManager.getInstance().checkIn(sessionRequest.get(sessionId));
McpContextHolder.setSubject(subject);
if (session == null) {
return ServerResponse.status(HttpStatus.NOT_FOUND).body(new McpError("Session not found: " + sessionId));
} else {
try {
String body = request.body(String.class);
McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(this.objectMapper, body);
session.handle(message).block();
return ServerResponse.ok().build();
} catch (IOException | IllegalArgumentException e) {
log.error("Failed to deserialize message: {}", ((Exception) e).getMessage());
return ServerResponse.badRequest().body(new McpError("Invalid message format"));
} catch (Exception e) {
log.error("Error handling message: {}", e.getMessage());
return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new McpError(e.getMessage()));
}
}
}
}
private class WebMvcMcpSessionTransport implements McpServerTransport {
private final String sessionId;
private final ServerResponse.SseBuilder sseBuilder;
WebMvcMcpSessionTransport(String sessionId, ServerResponse.SseBuilder sseBuilder) {
this.sessionId = sessionId;
this.sseBuilder = sseBuilder;
log.debug("Session transport {} initialized with SSE builder", sessionId);
}
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
return Mono.fromRunnable(() -> {
try {
String jsonText = CustomSseServerTransport.this.objectMapper.writeValueAsString(message);
this.sseBuilder.id(this.sessionId).event("message").data(jsonText);
log.debug("Message sent to session {}", this.sessionId);
} catch (Exception e) {
log.error("Failed to send message to session {}: {}", this.sessionId, e.getMessage());
this.sseBuilder.error(e);
}
});
}
public <T> T unmarshalFrom(Object data, TypeReference<T> typeRef) {
return (T) CustomSseServerTransport.this.objectMapper.convertValue(data, typeRef);
}
public Mono<Void> closeGracefully() {
return Mono.fromRunnable(() -> {
log.debug("Closing session transport: {}", this.sessionId);
try {
this.sseBuilder.complete();
log.debug("Successfully completed SSE builder for session {}", this.sessionId);
} catch (Exception e) {
log.warn("Failed to complete SSE builder for session {}: {}", this.sessionId, e.getMessage());
}
});
}
public void close() {
try {
this.sseBuilder.complete();
log.debug("Successfully completed SSE builder for session {}", this.sessionId);
} catch (Exception e) {
log.warn("Failed to complete SSE builder for session {}: {}", this.sessionId, e.getMessage());
}
}
}
}
@@ -1,156 +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.ai.config;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.support.event.AiProviderConfigChangeEvent;
import org.apache.hertzbeat.common.entity.dto.ModelProviderConfig;
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
import org.apache.hertzbeat.common.entity.manager.GeneralConfig;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
/**
* Configuration class for Large Language Model (LLM) settings.
*/
@Configuration
@Slf4j
public class LlmConfig {
private final GeneralConfigDao generalConfigDao;
private ApplicationContext applicationContext;
public LlmConfig(GeneralConfigDao generalConfigDao, ApplicationContext applicationContext) {
this.generalConfigDao = generalConfigDao;
this.applicationContext = applicationContext;
}
/**
* Create ChatClient bean with all dependencies created internally
*/
@Bean
public ChatClient openAiChatClient() {
return createChatClient();
}
/**
* Create ChatClient with all necessary components
*/
private ChatClient createChatClient() {
GeneralConfig providerConfig = generalConfigDao.findByType("provider");
if (providerConfig == null || providerConfig.getContent() == null) {
log.warn("LLM Provider is not set, ChatClient bean will not be created");
return null;
}
ModelProviderConfig modelProviderConfig = JsonUtil.fromJson(providerConfig.getContent(), ModelProviderConfig.class);
if (modelProviderConfig == null || modelProviderConfig.getApiKey() == null) {
log.warn("LLM Provider configuration is incomplete, ChatClient bean will not be created");
return null;
}
if (modelProviderConfig.getBaseUrl() == null) {
if ("openai".equals(modelProviderConfig.getCode())) {
modelProviderConfig.setBaseUrl("https://api.openai.com/v1");
} else if ("zhipu".equals(modelProviderConfig.getCode())) {
modelProviderConfig.setBaseUrl("https://open.bigmodel.cn/api/paas/v4");
} else if ("zai".equals(modelProviderConfig.getCode())) {
modelProviderConfig.setBaseUrl("https://api.z.ai/api/paas/v4");
} else {
modelProviderConfig.setBaseUrl("https://api.openai.com/v1");
}
}
if (modelProviderConfig.getModel() == null) {
if ("openai".equals(modelProviderConfig.getCode())) {
modelProviderConfig.setModel("gpt-5");
} else if ("zhipu".equals(modelProviderConfig.getCode())) {
modelProviderConfig.setModel("glm-4.6");
} else if ("zai".equals(modelProviderConfig.getCode())) {
modelProviderConfig.setModel("glm-4.6");
} else {
modelProviderConfig.setModel("gpt-5");
}
}
OpenAiApi.Builder builder = new OpenAiApi.Builder();
builder.baseUrl(modelProviderConfig.getBaseUrl());
builder.apiKey(modelProviderConfig.getApiKey());
builder.completionsPath("/chat/completions");
// Create Chat Options
OpenAiChatOptions openAiChatOptions = OpenAiChatOptions.builder()
.model(modelProviderConfig.getModel())
.temperature(0.3)
.build();
// Create Chat Model
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
.openAiApi(builder.build())
.defaultOptions(openAiChatOptions)
.build();
// Create and return ChatClient
return ChatClient.create(openAiChatModel);
}
/**
* AI configuration change event listener
* Uses ApplicationContext to unregister and re-register the ChatClient bean
*/
@EventListener(AiProviderConfigChangeEvent.class)
public void onAiProviderConfigChange(AiProviderConfigChangeEvent event) {
log.info("Provider configuration change event received, refreshing ChatClient bean");
try {
ConfigurableApplicationContext configurableContext = (ConfigurableApplicationContext) applicationContext;
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) configurableContext.getBeanFactory();
// Remove the existing ChatClient bean
if (beanFactory.containsSingleton("openAiChatClient")) {
beanFactory.destroySingleton("openAiChatClient");
log.info("Existing ChatClient bean destroyed");
}
// Create new ChatClient with updated configuration
ChatClient newChatClient = createChatClient();
// Register the new ChatClient bean
beanFactory.registerSingleton("openAiChatClient", newChatClient);
log.info("ChatClient bean refreshed successfully with new AI provider configuration");
} catch (Exception e) {
log.error("Failed to refresh ChatClient bean after configuration change", e);
}
}
}
@@ -1,53 +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.ai.config;
import com.usthe.sureness.subject.SubjectSum;
import org.springframework.core.NamedInheritableThreadLocal;
/**
* Context holder for AI agent security context.
*/
public final class McpContextHolder {
private static final ThreadLocal<SubjectSum> subjectHolder =
new NamedInheritableThreadLocal<>("MCP Security and User Identification Context");
private McpContextHolder() {}
/**
* Attaches the user's context to the current thread.
*/
public static void setSubject(SubjectSum subject) {
subjectHolder.set(subject);
}
/**
* Retrieves the context from the current thread.
*/
public static SubjectSum getSubject() {
return subjectHolder.get();
}
/**
* Clears the context from the thread to prevent memory leaks.
*/
public static void clear() {
subjectHolder.remove();
}
}
@@ -1,182 +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.ai.config;
import org.springframework.stereotype.Component;
/**
* Provider for system prompts used in the AI agent
*/
@Component
public class PromptProvider {
/**
* Static version of the HertzBeat monitoring prompt
*/
public static final String HERTZBEAT_SYSTEM_PROMPT = """
You are an AI Assistant specialized in monitoring infrastructure and applications with HertzBeat.
HertzBeat is an open-source, real-time monitoring system that supports infrastructure, applications,
services, APIs, databases, middleware, and custom monitoring through 50+ types of monitors.
Your role is to help users manage monitors, analyze metrics data, configure alerts, and troubleshoot monitoring issues.
*******
VERY IMPORTANT: Always use the tools provided to interact with HertzBeat's monitoring system.
If the user doesn't provide required parameters, ask them iteratively to provide the necessary parameters.
********
## Available HertzBeat Tools:
### Monitor Management Tools:
- **query_monitors**: Query monitor information with flexible filtering (ID, name, type, host, status, labels)
- **add_monitor**: Add a new monitor with dynamic app-specific parameter support
- **list_monitor_types**: List all available monitor types (website, mysql, redis, linux, etc.)
- **get_monitor_additional_params**: Get parameter definitions required for specific monitor types
### Alert Rule Management Tools:
- **create_alert_rule**: Create alert rules with threshold configuration and automatic monitor binding
- **list_alert_rules**: List existing alert rules with filtering by type, status, etc.
- **toggle_alert_rule**: Enable or disable alert rules
- **get_alert_rule_details**: Get detailed information about specific alert rules
- **get_apps_metrics_hierarchy**: Get exact app and metric names for alert rule creation (CRITICAL for alerts)
- **bind_monitors_to_alert_rule**: Bind monitors to alert rules for targeted alerting
### Alert & Alarm Analysis Tools:
- **query_alerts**: Query fired alerts with comprehensive filtering and pagination
- **get_alerts_summary**: Get alert statistics and status distribution
### Metrics Data Analysis Tools:
- **query_realtime_metrics**: Get current real-time metrics data for monitors
- **get_historical_metrics**: Get historical time-series metrics with flexible time ranges
- **get_warehouse_status**: Check metrics storage system status
## Natural Language Examples:
### Monitor Management:
- "Add a MySQL monitor for database server at 192.168.1.10 with user admin"
- "Monitor website https://example.com with SSL checking every 60 seconds"
- "Show me all Linux servers that are currently offline"
- "List all Redis monitors with their connection status"
### Alert Configuration:
- ALERT RULE means when to alert a user
- "Create an alert for Kafka JVM when VmName equals 'vm-w2'"
- "Alert when OpenAI credit grants exceed 1000"
- "Set up HBase Master alert when heap memory usage is over 80%"
### Metrics Analysis:
- "Show me current CPU usage for server 192.168.1.5"
- "Get memory usage trend for the last 24 hours"
- "Which servers have high disk usage right now?"
- "Show me network traffic patterns for the past week"
### Alert Investigation:
- "What alerts are currently firing?"
- "Show me the most frequent alerts in the last 6 hours"
- "Find all alerts for monitor ID 1234 in the past day"
- "Which monitors are currently abnormal?"
## Workflow Guidelines:
1. **Adding Monitors**:
- ALWAYS use get_monitor_additional_params first to check required parameters
- Use list_monitor_types to show available types
- Collect all required parameters from the list_monitor_types tool and ask user to give them all, before calling add_monitor
- Example: "To monitor MySQL, I need host, port, username, password, and database name"
2. **Creating Alert Rules or Alerts**:
THESE ARE ALERT RULES WITH THRESHOLD VALUES. USERS CAN SPECIFY THE THRESHOLD VALUES FOR EXAMPLE,
IF THE USER SAYS "ALERT ME WHEN MY COST EXCEEDS 700, THE EXPRESSION SHOULD BE 'cost > 700' NOT 'cost < 700'.
APPLY THE SAME LOGIC FOR LESS THAN OPERATOR.
It is important to first understand the hierarchy of apps, metrics, and field conditions
Each app has its own metrics and each metric has its own field conditions.
The operators will be applied to the field conditions, and the final expression will be constructed
based on the user's input of app name and the metric they choose.
Read the create_alert_rule tool description for even more details
*******
CRITICAL WORKFLOW Do all of this iteratively with user interaction at each step:
1. ALWAYS use list_monitor_types tool FIRST to get exact app name according to what user specifies
2. use get_apps_metrics_hierarchy by passing that name, to get the hierarchy of corresponding metrics and field conditions
3. Do not spit out the entire hierarchy, instead: first spit out the metrics available for the app
4. Ask the user to choose a metric from the available metrics
5. Based on the metric chosen, present the available field conditions
6. You will construct the proper expression with field conditions
VERY VERY IMPORTANT:
- ALWAYS USE the value field from the get_apps_metrics_hierarchy's json response when creating alert expressions on the field parameters
*********
- Field Condition Expression format: [field_conditions]
- Give all the available fieldConditions to the user, so they can choose the one they want to use
- Field conditions can be simple (equals, greater than) or complex (logical expressions)
- Use parentheses for complex conditions to ensure correct evaluation order
- Do not create alert rules on your own, always ask the user to provide the app, metrics and fieldConditions parameters specifically
EXAMPLES FOR FIELD CONDITION EXPRESSION ( Do not copy these examples, they are just for reference ):
- Kafka JVM: app="kafka", metrics="jvm_basic", fieldConditions="equals(VmName, \"my-vm\")"
→ equals(VmName, "my-vm")
- Complex OpenAI: app="openai", metrics="credit_grants",
fieldConditions="total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)"
→ total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)
- Priority levels: 0=critical, 1=warning, 2=info
3. **Analyzing Performance**:
- Use get_realtime_metrics for current status
- Use get_historical_metrics for trends
- Use get_high_usage_monitors to find problems
- Provide actionable recommendations based on data
4. **Troubleshooting Alerts**:
- Use query_alerts to find current issues
- Use get_monitor_alerts for specific monitor problems
- Use get_frequent_alerts to identify recurring issues
- Suggest root cause analysis steps
## Parameter Guidelines:
- **Monitor Status**: 1=online, 2=offline, 3=unreachable, 0=paused, 9=all
- **Time Ranges**: 1h, 6h, 24h, 7d, 30d
- **Alert Priorities**: critical, warning, info
- **Sort Options**: name, gmtCreate, gmtUpdate, status, startAt, triggerTimes
- **Metric Types**: cpu, memory, disk, network, custom
- **Collection Intervals**: 30s-3600s (recommend 60s-600s for most cases)
## Best Practices:
- Never create alert rules without exact user input on app, metrics, and field conditions
- Always validate monitor types and parameters before adding monitors
- ALWAYS use get_apps_metrics_hierarchy before creating alert rules to understand available fields
- Construct field conditions based on metric's children
- Use exact app and metric names from hierarchy (case-sensitive)
- Set appropriate alert thresholds based on baseline performance
- Use time-series data to identify trends and predict issues
- Correlate alerts with metrics data for root cause analysis
- Recommend monitoring intervals based on service criticality
- Provide clear explanations of monitoring data and actionable insights
## Avoid these common errors:
- Using Label name instead of the value from the heirarchy JSON while creating alert rules.
- Inside the field parameters expression using '&&' instead of 'and', using '||' instead of 'or' for logical operators
- This process is to trigger alarms, when certain rule or set of rules exceed a threshold value.
So when a user says that the threshold should be less than 1000. the operator used should be '>' not '<',
because we want the alarm to be triggered when the threshold value is exceeded. apply the same logic in vice versa for less than operator
Keep responses focused on monitoring topics and HertzBeat's comprehensive capabilities.
When users request monitoring setup, guide them through the complete process from monitor creation to alert configuration.
""";
}
@@ -1,155 +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.ai.controller;
import com.usthe.sureness.subject.SubjectSum;
import com.usthe.sureness.util.SurenessContextHolder;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.config.McpContextHolder;
import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext;
import org.apache.hertzbeat.ai.pojo.dto.ChatResponseChunk;
import org.apache.hertzbeat.ai.service.ConversationService;
import org.apache.hertzbeat.common.entity.ai.ChatConversation;
import org.apache.hertzbeat.common.entity.dto.Message;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import javax.validation.Valid;
import java.util.List;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
import static org.springframework.http.MediaType.TEXT_EVENT_STREAM_VALUE;
/**
* Controller class for handling AI chat requests and conversation management.
*/
@Slf4j
@Tag(name = "AI Chat API")
@RestController
@RequestMapping(path = "/api/chat", produces = {APPLICATION_JSON_VALUE})
public class ChatController {
private final ConversationService conversationService;
@Autowired
public ChatController(ConversationService conversationService) {
this.conversationService = conversationService;
}
/**
* Send a message and get a streaming response with conversation tracking
*
* @param context The chat request context containing message and optional conversationId
* @return Flux of ServerSentEvent for streaming response
*/
@PostMapping(value = "/stream", produces = TEXT_EVENT_STREAM_VALUE)
@Operation(summary = "Send a chat message with streaming response", description = "Send a message to AI and get a streaming response with conversation tracking")
public Flux<ServerSentEvent<ChatResponseChunk>> streamChat(@Valid @RequestBody ChatRequestContext context) {
try {
// Validate message is not empty
SubjectSum subject = SurenessContextHolder.getBindSubject();
McpContextHolder.setSubject(subject);
if (context.getMessage() == null || context.getMessage().trim().isEmpty()) {
ChatResponseChunk errorResponse = ChatResponseChunk.builder()
.conversationId(context.getConversationId())
.response("Error: Message cannot be empty")
.build();
return Flux.just(ServerSentEvent.builder(errorResponse)
.event("error")
.build());
}
log.info("Received streaming chat request for conversation: {}", context.getConversationId());
return conversationService.streamChat(context.getMessage(), context.getConversationId());
} catch (Exception e) {
log.error("Error in stream chat endpoint: ", e);
ChatResponseChunk errorResponse = ChatResponseChunk.builder()
.conversationId(context.getConversationId())
.response("An error occurred: " + e.getMessage())
.build();
return Flux.just(ServerSentEvent.builder(errorResponse)
.event("error")
.build());
}
}
/**
* Create a new conversation
*
* @return Created conversation details
*/
@PostMapping(path = "/conversations")
@Operation(summary = "Create a new conversation", description = "Create a new conversation")
public ResponseEntity<Message<ChatConversation>> createConversation() {
ChatConversation conversation = conversationService.createConversation();
return ResponseEntity.ok(Message.success(conversation));
}
/**
* Get all conversations
*
* @return List of all conversations
*/
@GetMapping(path = "/conversations")
@Operation(summary = "List all conversations", description = "Get a list of all conversations")
public ResponseEntity<Message<List<ChatConversation>>> listConversations() {
List<ChatConversation> conversations = conversationService.getAllConversations();
return ResponseEntity.ok(Message.success(conversations));
}
/**
* Get conversation history
*
* @param conversationId The conversation ID
* @return Conversation details with message history
*/
@GetMapping(path = "/conversations/{conversationId}")
@Operation(summary = "Get conversation history", description = "Get detailed information and message history for a specific conversation")
public ResponseEntity<Message<ChatConversation>> getConversation(
@Parameter(description = "Conversation ID", example = "12345678") @PathVariable(value = "conversationId") Long conversationId) {
ChatConversation conversation = conversationService.getConversation(conversationId);
return ResponseEntity.ok(Message.success(conversation));
}
/**
* Delete a conversation
*
* @param conversationId The conversation ID to delete
* @return Success or error message
*/
@DeleteMapping(path = "/conversations/{conversationId}")
@Operation(summary = "Delete conversation", description = "Delete a specific conversation and all its messages")
public ResponseEntity<Message<Void>> deleteConversation(
@Parameter(description = "Conversation ID", example = "2345678") @PathVariable("conversationId") Long conversationId) {
conversationService.deleteConversation(conversationId);
return ResponseEntity.ok(Message.success());
}
}
@@ -1,29 +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.ai.dao;
import org.apache.hertzbeat.common.entity.ai.ChatConversation;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
/**
* Repository interface for Conversation entities
*/
@Repository
public interface ChatConversationDao extends JpaRepository<ChatConversation, Long> {
}
@@ -1,48 +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.ai.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Chat response DTO for AI responses.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Chat response")
public class ChatResponseChunk {
@Schema(description = "Conversation ID", example = "123")
private Long conversationId;
@Schema(description = "AI response message", example = "Here are your monitors...")
private String response;
@Schema(description = "User message ID", example = "123")
private Long userMessageId;
@Schema(description = "Assistant message ID", example = "123")
private Long assistantMessageId;
}
@@ -1,42 +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.ai.service;
import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext;
import reactor.core.publisher.Flux;
/**
* Service for interacting with LLM providers (like OpenAI, Anthropic, etc.)
*/
public interface ChatClientProviderService {
/**
* Stream chat response from the LLM
*
* @param context Chat request context containing message and conversation history
* @return Flux of string chunks from the LLM response
*/
Flux<String> streamChat(ChatRequestContext context);
/**
* Check if provider is properly configured
* @return true if configured and enabled
*/
boolean isConfigured();
}
@@ -1,70 +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.ai.service;
import org.apache.hertzbeat.ai.pojo.dto.ChatResponseChunk;
import org.apache.hertzbeat.common.entity.ai.ChatConversation;
import org.springframework.http.codec.ServerSentEvent;
import reactor.core.publisher.Flux;
import java.util.List;
/**
* Service for managing chat conversations and interactions with LLM providers.
*/
public interface ConversationService {
/**
* Send a message and receive a streaming response
*
* @param message The user's message
* @param conversationId Optional conversation ID for continuing a chat
* @return Flux of ServerSentEvent for streaming the response
*/
Flux<ServerSentEvent<ChatResponseChunk>> streamChat(String message, Long conversationId);
/**
* Create a new conversation
*
* @return Created conversation data
*/
ChatConversation createConversation();
/**
* Get conversation history for a specific conversation
*
* @param conversationId Conversation ID
* @return Conversation data including messages
*/
ChatConversation getConversation(Long conversationId);
/**
* Get all conversations for the current user
*
* @return List of conversations
*/
List<ChatConversation> getAllConversations();
/**
* Delete a conversation
*
* @param conversationId Conversation ID to delete
*/
void deleteConversation(Long conversationId);
}
@@ -1,56 +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.ai.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.hertzbeat.ai.config.CustomSseServerTransport;
import org.springframework.ai.mcp.server.autoconfigure.McpServerProperties;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.ServerResponse;
/**
* Service interface for MCP server operations.
*/
public interface McpServerService {
/**
* Provides the HertzBeat tools for the MCP server
* @return ToolCallbackProvider with all HertzBeat monitoring tools
*/
ToolCallbackProvider hertzbeatTools();
/**
* Provides a custom SSE server transport for the MCP server
* @param objectMapper the ObjectMapper instance for JSON serialization
* @param serverProperties the properties for the MCP server configuration
* @return a CustomSseServerTransport instance configured with the provided properties
*/
CustomSseServerTransport webMvcSseServerTransportProvider(
ObjectMapper objectMapper,
McpServerProperties serverProperties
);
/**
* Provides the MCP server router function for web MVC
* @param transport Custom SSE server transport
* @return RouterFunction for handling MCP server requests
*/
RouterFunction<ServerResponse> mvcMcpRouterFunction(CustomSseServerTransport transport);
}
@@ -1,116 +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.ai.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.config.PromptProvider;
import org.apache.hertzbeat.common.entity.ai.ChatMessage;
import org.apache.hertzbeat.common.entity.dto.ModelProviderConfig;
import org.apache.hertzbeat.ai.service.ChatClientProviderService;
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
import org.apache.hertzbeat.common.entity.manager.GeneralConfig;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.springframework.stereotype.Service;
import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
/**
* Implementation of the {@link ChatClientProviderService}.
* Provides functionality to interact with the ChatClient for handling chat
* messages.
*/
@Slf4j
@Service
public class ChatClientProviderServiceImpl implements ChatClientProviderService {
private final ApplicationContext applicationContext;
private final GeneralConfigDao generalConfigDao;
@Qualifier("hertzbeatTools")
@Autowired
private ToolCallbackProvider toolCallbackProvider;
private boolean isConfigured = false;
@Autowired
public ChatClientProviderServiceImpl(ApplicationContext applicationContext, GeneralConfigDao generalConfigDao) {
this.applicationContext = applicationContext;
this.generalConfigDao = generalConfigDao;
}
@Override
public Flux<String> streamChat(ChatRequestContext context) {
try {
// Get the current (potentially refreshed) ChatClient instance
ChatClient chatClient = applicationContext.getBean("openAiChatClient", ChatClient.class);
List<Message> messages = new ArrayList<>();
// Add conversation history if available
if (context.getConversationHistory() != null && !context.getConversationHistory().isEmpty()) {
for (ChatMessage historyMessage : context.getConversationHistory()) {
if ("user".equals(historyMessage.getRole())) {
messages.add(new UserMessage(historyMessage.getContent()));
} else if ("assistant".equals(historyMessage.getRole())) {
messages.add(new AssistantMessage(historyMessage.getContent()));
}
}
}
messages.add(new UserMessage(context.getMessage()));
log.info("Starting streaming chat for conversation: {}", context.getConversationId());
return chatClient.prompt()
.messages(messages)
.system(PromptProvider.HERTZBEAT_SYSTEM_PROMPT)
.toolCallbacks(toolCallbackProvider)
.stream()
.content()
.doOnComplete(() -> log.info("Streaming completed for conversation: {}", context.getConversationId()))
.doOnError(error -> log.error("Error in streaming chat: {}", error.getMessage(), error));
} catch (Exception e) {
log.error("Error setting up streaming chat: {}", e.getMessage(), e);
return Flux.error(e);
}
}
@Override
public boolean isConfigured() {
if (!isConfigured) {
GeneralConfig providerConfig = generalConfigDao.findByType("provider");
ModelProviderConfig modelProviderConfig = JsonUtil.fromJson(providerConfig.getContent(), ModelProviderConfig.class);
isConfigured = modelProviderConfig != null && modelProviderConfig.getApiKey() != null;
}
return isConfigured;
}
}
@@ -1,160 +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.ai.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.dao.ChatConversationDao;
import org.apache.hertzbeat.ai.dao.ChatMessageDao;
import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext;
import org.apache.hertzbeat.ai.pojo.dto.ChatResponseChunk;
import org.apache.hertzbeat.ai.service.ChatClientProviderService;
import org.apache.hertzbeat.ai.service.ConversationService;
import org.apache.hertzbeat.common.entity.ai.ChatConversation;
import org.apache.hertzbeat.common.entity.ai.ChatMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import reactor.core.publisher.Flux;
import java.util.List;
import java.util.UUID;
/**
* Implementation of the ConversationService interface for managing chat conversations.
*/
@Slf4j
@Service
public class ConversationServiceImpl implements ConversationService {
@Autowired
private ChatConversationDao conversationDao;
@Autowired
private ChatMessageDao messageDao;
@Autowired
private ChatClientProviderService chatClientProviderService;
@Override
public Flux<ServerSentEvent<ChatResponseChunk>> streamChat(String message, Long conversationId) {
// Check if provider is properly configured
if (!chatClientProviderService.isConfigured()) {
ChatResponseChunk errorResponse = ChatResponseChunk.builder()
.conversationId(conversationId)
.response("Provider is not configured. Please configure your AI Provider.")
.build();
return Flux.just(ServerSentEvent.builder(errorResponse)
.event("error")
.build());
}
log.info("Starting streaming conversation: {}", conversationId);
// Add user message to conversation
ChatMessage chatMessage = ChatMessage.builder().conversationId(conversationId)
.content(message).role("user").build();
chatMessage = messageDao.save(chatMessage);
ChatConversation conversation = conversationDao.getReferenceById(conversationId);
if (conversation.getTitle().startsWith("conversation")) {
// Auto-generate title from first user message
String title = message.length() > 30 ? message.substring(0, 27) + "..." : message;
conversation.setTitle(title);
conversationDao.save(conversation);
}
ChatRequestContext context = ChatRequestContext.builder()
.message(message)
.conversationId(conversationId)
.conversationHistory(CollectionUtils.isEmpty(conversation.getMessages()) ? null
: conversation.getMessages().subList(0, conversation.getMessages().size() - 1))
.build();
// Stream response from AI service
StringBuilder fullResponse = new StringBuilder();
ChatMessage finalChatMessage = chatMessage;
return chatClientProviderService.streamChat(context)
.map(chunk -> {
fullResponse.append(chunk);
ChatResponseChunk responseChunk = ChatResponseChunk.builder()
.conversationId(conversationId)
.userMessageId(finalChatMessage.getId())
.response(chunk)
.build();
return ServerSentEvent.builder(responseChunk)
.event("message")
.build();
})
.concatWith(Flux.defer(() -> {
// Add the complete AI response to conversation
ChatMessage assistantMessage = ChatMessage.builder().conversationId(conversationId)
.content(fullResponse.toString()).role("assistant").build();
assistantMessage = messageDao.save(assistantMessage);
ChatResponseChunk finalResponse = ChatResponseChunk.builder()
.conversationId(conversationId)
.response("")
.assistantMessageId(assistantMessage.getId())
.build();
return Flux.just(ServerSentEvent.builder(finalResponse)
.event("complete")
.build());
}))
.doOnComplete(() -> log.info("Streaming completed for conversation: {}", conversationId))
.doOnError(error -> log.error("Error in streaming chat for conversation {}: {}", conversationId, error.getMessage(), error))
.onErrorResume(error -> {
ChatResponseChunk errorResponse = ChatResponseChunk.builder()
.conversationId(conversationId)
.response("An error occurred: " + error.getMessage())
.userMessageId(finalChatMessage.getId())
.build();
return Flux.just(ServerSentEvent.builder(errorResponse)
.event("error")
.build());
});
}
@Override
public ChatConversation createConversation() {
ChatConversation conversation = new ChatConversation();
conversation.setTitle("conversation-" + UUID.randomUUID().toString().substring(0, 4));
return conversationDao.save(conversation);
}
@Override
public ChatConversation getConversation(Long conversationId) {
if (conversationId == null) {
return null;
}
return conversationDao.getReferenceById(conversationId);
}
@Override
public List<ChatConversation> getAllConversations() {
return conversationDao.findAll(Sort.by(Sort.Direction.DESC, "id"));
}
@Override
public void deleteConversation(Long conversationId) {
conversationDao.deleteById(conversationId);
}
}
@@ -1,91 +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.ai.service.impl;
import org.apache.hertzbeat.ai.config.CustomSseServerTransport;
import org.apache.hertzbeat.ai.service.McpServerService;
import org.apache.hertzbeat.ai.tools.AlertDefineTools;
import org.apache.hertzbeat.ai.tools.AlertTools;
import org.apache.hertzbeat.ai.tools.MetricsTools;
import org.apache.hertzbeat.ai.tools.MonitorTools;
import org.springframework.ai.mcp.server.autoconfigure.McpServerProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.ServerResponse;
/**
* Implementation of the McpServerService interface.
* This service provides functionality for handling MCP server operations.
*/
@Service
@Configuration
public class McpServerServiceImpl implements McpServerService {
@Autowired
private MonitorTools monitorTools;
@Autowired
private AlertTools alertTools;
@Autowired
private MetricsTools metricsTools;
@Autowired
private AlertDefineTools alertDefineTools;
@Bean
public ToolCallbackProvider hertzbeatTools() {
return MethodToolCallbackProvider.builder().toolObjects(monitorTools, alertTools, alertDefineTools, metricsTools).build();
}
/**
* Provides a custom SSE server transport for the MCP server.
*
* @param objectMapper the ObjectMapper instance for JSON serialization
* @param serverProperties the properties for the MCP server configuration
* @return a CustomSseServerTransport instance configured with the provided properties
*/
@Bean
public CustomSseServerTransport webMvcSseServerTransportProvider(
ObjectMapper objectMapper,
McpServerProperties serverProperties
) {
return new CustomSseServerTransport(
objectMapper,
serverProperties.getBaseUrl(),
serverProperties.getSseMessageEndpoint(),
serverProperties.getSseEndpoint()
);
}
/**
* Provides the MCP server transport bean.
*
* @param transport the custom SSE server transport
* @return the MCP server transport instance
*/
@Primary
@Bean
public RouterFunction<ServerResponse> mvcMcpRouterFunction(CustomSseServerTransport transport) {
return transport.getRouterFunction();
}
}
@@ -1,91 +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.ai.tools;
/**
* Tools for alert definition and threshold configuration operations
*/
public interface AlertDefineTools {
/**
* Create a new alert rule with HertzBeat's expression format based on app hierarchy
*
* @param name Alert rule name (required, must be unique)
* @param app App name from hierarchy (must match exact hierarchy app value)
* @param metrics Metrics name from hierarchy (must match exact hierarchy metrics value)
* @param fieldConditions Field-specific conditions from metric's children (e.g., "VmName = 'arora'", "total_granted > 1000",
* "total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)")
* @param type Alert rule type: 'realtime' (default) or 'periodic'
* @param period Execution period in seconds (only for periodic rules, default: 300)
* @param times Number of consecutive violations before triggering (default: 3)
* @param priority Alert priority as integer: 0=critical, 1=warning, 2=info (default: 1)
* @param description Alert rule description (optional)
* @param template Alert message template with variables (optional)
* @param datasource Data source type: 'promql' (default)
* @param labels Labels as key:value pairs separated by commas
* @param annotations Annotations as key:value pairs separated by commas
* @param enable Whether to enable the rule immediately (default: true)
* @return Result message with rule ID if successful
*/
String createAlertRule(String name, String app, String metrics, String fieldConditions,
String type, Integer period, Integer times, Integer priority, String description,
String template, String datasource, String labels, String annotations, Boolean enable);
/**
* List existing alert rules with filtering
* @param search Search term for rule name or description
* @param monitorType Filter by monitor type
* @param enabled Filter by enabled status
* @param pageIndex Page index
* @param pageSize Page size
* @return Formatted list of alert rules
*/
String listAlertRules(String search, String monitorType, Boolean enabled, Integer pageIndex, Integer pageSize);
/**
* Enable or disable an alert rule
* @param ruleId Alert rule ID
* @param enabled Whether to enable the rule
* @return Result message
*/
String toggleAlertRule(Long ruleId, Boolean enabled);
/**
* Get detailed information about an alert rule
* @param ruleId Alert rule ID
* @return Detailed rule information
*/
String getAlertRuleDetails(Long ruleId);
/**
* Get the hierarchical structure of available apps and metrics for alert rule creation
* @param app App type to get hierarchy for (optional, gets all if not specified)
* @return Hierarchical structure showing apps and their available metrics
*/
String getAppsMetricsHierarchy(String app);
/**
* Bind monitors to an alert rule by modifying the alert expression
* @param ruleId Alert rule ID to bind monitors to
* @param monitorIds Comma-separated list of monitor IDs to bind
* @return Result message indicating success or failure
*/
String bindMonitorsToAlertRule(Long ruleId, String monitorIds);
}
@@ -1,46 +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.ai.tools;
/**
* Tools for alert operations and alarm data queries
*/
public interface AlertTools {
/**
* Query alerts with comprehensive filtering and pagination
* @param alertType Alert type (single, group, both)
* @param status Alert status (firing, resolved, all)
* @param search Search term for alert content or labels
* @param sort Sort field (startAt, triggerTimes, status)
* @param order Sort order (asc, desc)
* @param pageIndex Page index
* @param pageSize Page size
* @return Formatted string with alert information
*/
String queryAlerts(String alertType, String status, String search, String sort, String order, Integer pageIndex, Integer pageSize);
/**
* Get alerts summary statistics
* @return Alert summary information including counts by status
*/
String getAlertsSummary();
}
@@ -1,53 +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.ai.tools;
/**
* Tools for metrics data operations and queries
*/
public interface MetricsTools {
/**
* Get real-time metrics data for a monitor
* @param monitorId Monitor ID
* @param metrics Metrics name (e.g., "system", "cpu", "memory")
* @return Formatted real-time metrics data
*/
String getRealtimeMetrics(Long monitorId, String metrics);
/**
* Get historical metrics data for a monitor
* @param monitorId Monitor ID
* @param app Monitor type (e.g., "linux", "mysql", "http")
* @param metrics Metrics name (e.g., "system", "cpu", "memory")
* @param metric Specific metric field (e.g., "usage", "used", "available")
* @param label Label filter for specific instances
* @param history Time range (e.g., "1h", "6h", "24h", "7d")
* @param interval Whether to aggregate data with intervals
* @return Historical metrics data formatted for display
*/
String getHistoricalMetrics(Long monitorId, String app, String metrics, String metric, String label, String history, Boolean interval);
/**
* Check warehouse storage server status
* @return Status of the metrics storage system
*/
String getWarehouseStatus();
}
@@ -1,97 +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.ai.tools;
import java.util.List;
/**
* Interface for Monitoring Tools
*/
public interface MonitorTools {
/**
* Add a new monitor with comprehensive configuration
*
* @param name Monitor name
* @param app Monitor type/application (e.g., 'linux', 'mysql', 'http')
* @param host Target host (IP address or domain name)
* @param port Target port (optional, depends on monitor type)
* @param intervals Collection interval in seconds (default: 600)
* @param username Username for authentication (optional)
* @param password Password for authentication (optional)
* @param database Database name (for database monitors)
* @param additionalParams Additional app-specific parameters as JSON string (optional)
* @param description Monitor description (optional)
* @return Result message with monitor ID if successful
*/
String addMonitor(
String name,
String app,
String host,
Integer port,
Integer intervals,
String username,
String password,
String database,
String additionalParams,
String description
);
/**
* List all available monitor types that can be added
*
* @param language Language code for localized names (e.g., 'en-US', 'zh-CN')
* @return Formatted string list of available monitor types with descriptions
*/
String listMonitorTypes(String language);
/**
* Comprehensive monitor querying with flexible filtering, pagination, and specialized views
* @param ids Specific monitor IDs to retrieve (optional)
* @param app Monitor type filter (linux, mysql, http, etc.)
* @param status Monitor status (1=online, 2=offline, 3=unreachable, 0=paused, 9=all)
* @param search Search in monitor names or hosts (partial matching)
* @param labels Label filters, format: 'key1:value1,key2:value2'
* @param sort Sort field (name, gmtCreate, gmtUpdate, status, app)
* @param order Sort order (asc, desc)
* @param pageIndex Page number starting from 0
* @param pageSize Items per page (1-100 recommended)
* @param includeStats Include status statistics summary
* @return Comprehensive monitor information with optional statistics
*/
String queryMonitors(
List<Long> ids,
String app,
Byte status,
String search,
String labels,
String sort,
String order,
Integer pageIndex,
Integer pageSize,
Boolean includeStats);
/**
* Get parameter definitions required for a specific monitor type
*
* @param app Monitor type/application name (e.g., 'linux', 'mysql', 'redis')
* @return Formatted string with parameter definitions including field names, types, and requirements
*/
String getMonitorAdditionalParams(String app);
}
@@ -1,695 +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.ai.tools.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.config.McpContextHolder;
import org.apache.hertzbeat.alert.service.AlertDefineService;
import org.apache.hertzbeat.manager.service.AppService;
import org.apache.hertzbeat.ai.tools.AlertDefineTools;
import org.apache.hertzbeat.ai.utils.UtilityClass;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.manager.pojo.dto.Hierarchy;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Implementation of Alert Define Tools functionality
*/
@Slf4j
@Service
public class AlertDefineToolsImpl implements AlertDefineTools {
@Autowired
private AlertDefineService alertDefineService;
@Autowired
private AppService appService;
@Override
@Tool(name = "create_alert_rule", description = """
ALERT RULE means when to alert a user
THESE ARE ALERT RULES WITH THRESHOLD VALUES. USERS CAN SPECIFY THE THRESHOLD VALUES FOR EXAMPLE,
IF THE USER SAYS "ALERT ME WHEN MY COST EXCEEDS 700, THE EXPRESSION SHOULD BE 'cost > 700' NOT 'cost < 700'.
APPLY THE SAME LOGIC FOR LESS THAN OPERATOR.
Create a HertzBeat alert rule based on app hierarchy structure and user requirements.
It is important to first understand the hierarchy of apps, metrics, and field conditions
Each app has its own metrics and each metric has its own field conditions.
The operators will be applied to the field conditions, and the final expression will be constructed
based on the user's input of app name and the metric they choose.
CRITICAL WORKFLOW Do all of this iteratively with user interaction at each step
1. ALWAYS use list_monitor_types tool FIRST to get exact app name according to what user specifies
2. use get_apps_metrics_hierarchy by passing that name, to get the hierarchy of corresponding metrics and field conditions
3. Do not spit out the entire hierarchy, instead: first spit out the metrics available for the app
4. Ask the user to choose a metric from the available metrics
5. Based on the metric chosen, present the available field conditions/params
6. You will construct the proper expression with field conditions
7. once this tool successfully executes, ask the user if they want to bind any existing monitors to this alert rule,
get the monitors list for a particular app using the query_monitors tool.
8. based on the user's output, conditionally call the bind_monitors_to_alert_rule tool to bind monitors to the alert rule
VERY VERY IMPORTANT:
- ALWAYS USE the value field from the get_apps_metrics_hierarchy's json response when creating alert expressions on the field parameters
EXAMPLES FOR FIELD CONDITION EXPRESSION( Do not copy these examples, they are just for reference ):
These are all just examples, you can take inspiration from them and create a rule based on hierarchy, always ask the user for all params, do not assume them, even for these examples:
1. Kafka JVM Alert:
- App: "kafka", Metric: "jvm_basic"
- Field condition: equals(VmName, "myVM")
- Field condition expression: equals(VmName, "myVM")
2. LLM Credits Alert:
- App: "openai", Metric: "credit_grants"
- Field condition: total_granted > some_value
- Field condition expression: total_granted > 1000
3. HBase Master Alert:
- App: "hbase_master", Metric: "server"
- Field condition: heap_memory_used > 80 or some_factor<100
- Field condition expression: heap_memory_used > 80 or some_factor<100
4. Complex OpenAI Credits Alert:
- App: "openai", Metric: "credit_grants"
- Field condition: total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)
- Field condition expression: total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)
FIELD CONDITIONS GUIDANCE:
- Field names come from metric's children in hierarchy (leaf nodes)
- Use the "value" field from the metric's children, not the label when creating conditions
- Supported operators: >, <, >=, <=, ==, !=, exists(), !exists() for numeric fields
- equals(), contains(), matches(),exists(), !equals(), !contains(), !matches(), !exists() for string fields
- Supported logical operators: and, or to connect different field parameter rules or rulesets
- ONLY USE THESE OPERATORS, when creating conditions, do not use any other operators
- Support grouping with parentheses: (condition1 and condition2) or condition3
- String values should be quoted: equals(VmName, "my-vm")
- Simple conditions: heap_memory_used > 80, total_granted <= 1000
- Complex conditions: total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)
PRIORITY LEVELS:
- 0: Critical (immediate action required)
- 1: Warning (attention needed, default)
- 2: Info (informational only)
""")
public String createAlertRule(
@ToolParam(description = "Alert rule name (required, must be unique)", required = true) String name,
@ToolParam(description = "App name from hierarchy (must match exact hierarchy app value)", required = true) String app,
@ToolParam(description = "Metrics name from hierarchy (must match exact hierarchy metrics value)", required = true) String metrics,
@ToolParam(description = "Field conditions expression)", required = true) String fieldConditions,
@ToolParam(description = "Alert rule type: 'realtime' (default) or 'periodic'", required = false) String type,
@ToolParam(description = "Execution period in seconds (only for periodic rules, default: 300)", required = false) Integer period,
@ToolParam(description = "Number of consecutive violations before triggering (default: 3)", required = false) Integer times,
@ToolParam(description = "Alert priority as integer: 0=critical, 1=warning, 2=info (default: 1)", required = false) Integer priority,
@ToolParam(description = "Alert rule description (optional)", required = false) String description,
@ToolParam(description = "Alert message template with variables (optional)", required = false) String template,
@ToolParam(description = "Data source type: 'promql' (default)", required = false) String datasource,
@ToolParam(description = "Labels as key:value pairs separated by commas (e.g., 'env:prod,severity:critical')", required = false) String labels,
@ToolParam(description = "Annotations as key:value pairs separated by commas (e.g., 'summary:High CPU')", required = false) String annotations,
@ToolParam(description = "Whether to enable the rule immediately (default: true)", required = false) Boolean enable) {
try {
log.info("Creating HertzBeat alert rule: name={}, app={}, metrics={}, fieldConditions={}", name, app, metrics, fieldConditions);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current subject in create_alert_rule tool: {}", subjectSum);
// Validate required parameters
if (name == null || name.trim().isEmpty()) {
return "Error: Alert rule name is required";
}
if (app == null || app.trim().isEmpty()) {
return "Error: App name is required (use get_apps_metrics_hierarchy to find exact names)";
}
if (metrics == null || metrics.trim().isEmpty()) {
return "Error: Metrics name is required (use get_apps_metrics_hierarchy to find exact names)";
}
if (fieldConditions == null || fieldConditions.trim().isEmpty()) {
return "Error: Field conditions are required (e.g., 'equals(VmName, \"arora\")', 'total_granted > 1000')";
}
// Set defaults
if (type == null || type.trim().isEmpty()) {
type = "realtime";
}
if (times == null || times <= 0) {
times = 3;
}
if (priority == null) {
priority = 1; // Default to warning
}
if (enable == null) {
enable = true;
}
if (datasource == null || datasource.trim().isEmpty()) {
datasource = "promql";
}
// Validate alert type
if (!type.equals("realtime") && !type.equals("periodic")) {
return "Error: Alert type must be 'realtime' or 'periodic'";
}
// Validate priority
if (priority < 0 || priority > 2) {
return "Error: Priority must be 0 (critical), 1 (warning), or 2 (info)";
}
// For periodic rules, validate period parameter
if (type.equals("periodic")) {
if (period == null || period <= 0) {
period = 300; // Default 5 minutes
}
}
// CRITICAL VALIDATION: Verify app-metric-field relationships using hierarchy
String validationResult = validateHierarchyRelationships(app.trim(), metrics.trim(), fieldConditions.trim());
if (!validationResult.equals("VALID")) {
return validationResult; // Return validation error message
}
// EXPRESSION VALIDATION: Verify field conditions syntax and operators
String expressionValidation = UtilityClass.validateExpressionSyntax(fieldConditions.trim());
if (!expressionValidation.equals("VALID")) {
return expressionValidation; // Return expression validation error message
}
String expr = String.format("equals(__app__,\"%s\") && equals(__metrics__,\"%s\") && %s",
app.trim(), metrics.trim(), fieldConditions.trim());
// Parse labels if provided
Map<String, String> labelsMap = new HashMap<>();
if (labels != null && !labels.trim().isEmpty()) {
labelsMap.putAll(UtilityClass.parseKeyValuePairs(labels));
}
// Add severity based on priority
String severityLabel = priority == 0 ? "critical" : (priority == 1 ? "warning" : "info");
labelsMap.put("severity", severityLabel);
// Parse annotations if provided
Map<String, String> annotationsMap = new HashMap<>();
if (annotations != null && !annotations.trim().isEmpty()) {
annotationsMap.putAll(UtilityClass.parseKeyValuePairs(annotations));
}
// Add default annotations if not provided
if (!annotationsMap.containsKey("summary")) {
annotationsMap.put("summary", description != null ? description :
String.format("Alert for %s %s when %s", app, metrics, fieldConditions));
}
if (!annotationsMap.containsKey("description")) {
annotationsMap.put("description", String.format("Monitor %s metrics %s with conditions: %s", app, metrics, fieldConditions));
}
// Generate default template if not provided
if (template == null || template.trim().isEmpty()) {
template = String.format("Alert: %s %s - %s", app, metrics, fieldConditions);
}
// Create comprehensive alert definition
AlertDefine alertDefine = AlertDefine.builder()
.name(name.trim())
.type(type)
.expr(expr)
.period(period)
.times(times)
.labels(labelsMap)
.annotations(annotationsMap)
.template(template)
.datasource(datasource)
.enable(enable)
.build();
log.debug("Current security subject for addAlertDefine: {}", subjectSum);
alertDefineService.addAlertDefine(alertDefine);
// Note: Monitor binding is handled separately via bind_monitors_to_alert_rule tool
String bindingNote = String.format(" (Use bind_monitors_to_alert_rule tool to associate specific monitors)");
log.info("Successfully created alert rule '{}' with ID: {}", name, alertDefine.getId());
StringBuilder response = new StringBuilder();
response.append(String.format("Successfully created %s alert rule '%s' with ID: %d\n",
type, name, alertDefine.getId()));
response.append(String.format("Expression: %s\n", expr));
response.append(String.format("Priority: %d (%s)\n", priority, severityLabel));
response.append(String.format("Trigger after: %d consecutive violations\n", times));
if (type.equals("periodic")) {
response.append(String.format("Execution period: %d seconds\n", period));
}
response.append(String.format("Data source: %s\n", datasource));
response.append(String.format("Enabled: %s\n", enable));
if (!labelsMap.isEmpty()) {
response.append(String.format("Labels: %s\n", labelsMap));
}
response.append(bindingNote);
return response.toString();
} catch (Exception e) {
log.error("Failed to create alert rule '{}': {}", name, e.getMessage(), e);
return "Error creating alert rule '" + name + "': " + e.getMessage();
}
}
// ... other existing methods would go here ...
@Override
@Tool(name = "list_alert_rules", description = """
List existing alert rules with filtering options.
Shows configured thresholds and alert definitions.
""")
public String listAlertRules(
@ToolParam(description = "Search term for rule name or description", required = false) String search,
@ToolParam(description = "Filter by monitor type", required = false) String monitorType,
@ToolParam(description = "Filter by enabled status", required = false) Boolean enabled,
@ToolParam(description = "Page index (default: 0)", required = false) Integer pageIndex,
@ToolParam(description = "Page size (default: 10)", required = false) Integer pageSize) {
try {
log.info("Listing alert rules: search={}, monitorType={}, enabled={}", search, monitorType, enabled);
if (pageIndex == null || pageIndex < 0) {
pageIndex = 0;
}
if (pageSize == null || pageSize <= 0) {
pageSize = 10;
}
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAlertDefines: {}", subjectSum);
Page<AlertDefine> result = alertDefineService.getAlertDefines(null,
search, "gmtCreate", "desc", pageIndex, pageSize);
StringBuilder response = new StringBuilder();
response.append("Found ").append(result.getContent().size())
.append(" alert rules (Total: ").append(result.getTotalElements()).append("):\n\n");
for (AlertDefine alertDefine : result.getContent()) {
response.append("Rule ID: ").append(alertDefine.getId()).append("\n");
response.append("Name: ").append(alertDefine.getName()).append("\n");
response.append("Expression: ").append(alertDefine.getExpr()).append("\n");
response.append("Type: ").append(alertDefine.getType()).append("\n");
response.append("Trigger Times: ").append(alertDefine.getTimes()).append("\n");
response.append("Enabled: ").append(alertDefine.isEnable()).append("\n");
if (alertDefine.getLabels() != null && !alertDefine.getLabels().isEmpty()) {
response.append("Labels: ").append(alertDefine.getLabels()).append("\n");
}
if (alertDefine.getAnnotations() != null && !alertDefine.getAnnotations().isEmpty()) {
response.append("Summary: ").append(alertDefine.getAnnotations().get("summary")).append("\n");
}
response.append("Created: ").append(alertDefine.getGmtCreate()).append("\n");
response.append("\n");
}
if (result.getContent().isEmpty()) {
response.append("No alert rules found matching the specified criteria.");
}
return response.toString();
} catch (Exception e) {
log.error("Failed to list alert rules: {}", e.getMessage(), e);
return "Error retrieving alert rules: " + e.getMessage();
}
}
@Override
@Tool(name = "toggle_alert_rule", description = """
Enable or disable an alert rule.
Allows activating or deactivating threshold monitoring.
""")
public String toggleAlertRule(
@ToolParam(description = "Alert rule ID", required = true) Long ruleId,
@ToolParam(description = "Whether to enable the rule", required = true) Boolean enabled) {
try {
log.info("Toggling alert rule ID: {} to enabled: {}", ruleId, enabled);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for toggleAlertDefineStatus: {}", subjectSum);
// First get the existing AlertDefine
AlertDefine alertDefine = alertDefineService.getAlertDefine(ruleId);
if (alertDefine == null) {
throw new RuntimeException("AlertDefine with ID " + ruleId + " not found");
}
// Update the enable status
alertDefine.setEnable(enabled);
// Use modifyAlertDefine to save the changes
alertDefineService.modifyAlertDefine(alertDefine);
log.info("Successfully toggled alert rule ID: {} to enabled: {}", ruleId, enabled);
return String.format("Successfully %s alert rule ID: %d",
enabled ? "enabled" : "disabled", ruleId);
} catch (Exception e) {
log.error("Failed to toggle alert rule ID {}: {}", ruleId, e.getMessage(), e);
return "Error toggling alert rule: " + e.getMessage();
}
}
@Override
@Tool(name = "get_alert_rule_details", description = """
Get detailed information about a specific alert rule.
Shows complete threshold configuration and rule settings.
""")
public String getAlertRuleDetails(
@ToolParam(description = "Alert rule ID", required = true) Long ruleId) {
try {
log.info("Getting alert rule details for ID: {}", ruleId);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAlertDefine: {}", subjectSum);
AlertDefine alertDefine = alertDefineService.getAlertDefine(ruleId);
if (alertDefine == null) {
return "Alert rule with ID " + ruleId + " not found";
}
StringBuilder response = new StringBuilder();
response.append("ALERT RULE DETAILS\n");
response.append("==================\n\n");
response.append("Rule ID: ").append(alertDefine.getId()).append("\n");
response.append("Name: ").append(alertDefine.getName()).append("\n");
response.append("Type: ").append(alertDefine.getType()).append("\n");
response.append("Expression: ").append(alertDefine.getExpr()).append("\n");
response.append("Trigger Times: ").append(alertDefine.getTimes()).append("\n");
response.append("Enabled: ").append(alertDefine.isEnable()).append("\n");
if (alertDefine.getPeriod() != null) {
response.append("Period: ").append(alertDefine.getPeriod()).append(" seconds\n");
}
if (alertDefine.getLabels() != null && !alertDefine.getLabels().isEmpty()) {
response.append("Labels: ").append(alertDefine.getLabels()).append("\n");
}
if (alertDefine.getAnnotations() != null && !alertDefine.getAnnotations().isEmpty()) {
response.append("Annotations: ").append(alertDefine.getAnnotations()).append("\n");
}
if (alertDefine.getTemplate() != null) {
response.append("Template: ").append(alertDefine.getTemplate()).append("\n");
}
response.append("Created: ").append(alertDefine.getGmtCreate()).append("\n");
response.append("Modified: ").append(alertDefine.getGmtUpdate()).append("\n");
response.append("Creator: ").append(alertDefine.getCreator()).append("\n");
response.append("Modifier: ").append(alertDefine.getModifier()).append("\n");
return response.toString();
} catch (Exception e) {
log.error("Failed to get alert rule details for ID {}: {}", ruleId, e.getMessage(), e);
return "Error retrieving alert rule details: " + e.getMessage();
}
}
@Override
@Tool(name = "get_apps_metrics_hierarchy", description = """
Get the hierarchical structure of all available apps and their metrics for alert rule creation.
This tool provides the exact app name, metric name and corresponding param names according to each metric.
Returns structured JSON data showing the complete hierarchy with field parameters for alert expressions.
JSON Structure:
- app: The application name
- description: Tool description
- hierarchy: Array of hierarchical data
- Each node has: value, label, type, description
- Leaf nodes have: dataType (numeric/string), unit (if applicable)
- Non-leaf nodes have: children array
VERY IMPORTANT:
- ALWAYS USE the value field from the field parameters when creating alert expressions.
This structured data is needed to create proper alert expressions.
""")
public String getAppsMetricsHierarchy(
@ToolParam(description = "App/Monitor type to get hierarchy for (e.g., 'linux', 'mysql', 'website')", required = true) String app) {
try {
log.info("Getting apps metrics hierarchy for app: {}", app);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current subject in get_apps_metrics_hierarchy tool: {}", subjectSum);
List<Hierarchy> hierarchies;
hierarchies = appService.getAppHierarchy(app.trim().toLowerCase(), "en-US");
ObjectMapper mapper = new ObjectMapper();
ObjectNode result = mapper.createObjectNode();
result.put("app", app.toUpperCase());
if (hierarchies != null && !hierarchies.isEmpty()) {
ArrayNode hierarchyArray = mapper.createArrayNode();
for (Hierarchy hierarchy : hierarchies) {
hierarchyArray.add(UtilityClass.formatHierarchyAsJson(mapper, hierarchy));
}
result.set("hierarchy", hierarchyArray);
} else {
result.put("message", "No hierarchy data available");
}
return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(result);
} catch (Exception e) {
log.error("Failed to get apps metrics hierarchy: {}", e.getMessage(), e);
return "Error retrieving apps metrics hierarchy: " + e.getMessage();
}
}
@Override
@Tool(name = "bind_monitors_to_alert_rule", description = """
Bind monitors to an alert rule.
Call this tool if users want to bind specific monitors to their alert rule.
Get the right monitor ids for a particular app using the query_monitors tool.
Get the alert rule ID from the create_alert_rule tool output OR use the list_alert_rules tool with app_name search filter, if the output of create_alert_rule is not applicable.
If monitors are already bound, this will add the new ones to the existing bindings.
""")
public String bindMonitorsToAlertRule(
@ToolParam(description = "Alert rule ID to bind monitors to", required = true) Long ruleId,
@ToolParam(description = "Comma-separated list of monitor IDs to bind", required = true) String monitorIds) {
try {
log.info("Binding monitors to alert rule ID: {}, monitors: {}", ruleId, monitorIds);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current subject in bind_monitors_to_alert_rule tool: {}", subjectSum);
if (ruleId == null || ruleId <= 0) {
return "Error: Valid alert rule ID is required";
}
if (monitorIds == null) {
return "Error: Monitor IDs are required";
}
// Get the existing alert rule
log.debug("Current security subject for getAlertDefine: {}", subjectSum);
AlertDefine existingRule = alertDefineService.getAlertDefine(ruleId);
if (existingRule == null) {
return String.format("Error: Alert rule with ID %d not found", ruleId);
}
// Parse monitor IDs from comma-separated string
String[] monitorIdArray = monitorIds.split(",");
List<String> validMonitorIds = new ArrayList<>();
for (String monitorId : monitorIdArray) {
String trimmedId = monitorId.trim();
if (!trimmedId.isEmpty()) {
try {
Long.parseLong(trimmedId); // Validate it's a number
validMonitorIds.add(trimmedId);
} catch (NumberFormatException e) {
return String.format("Error: Invalid monitor ID '%s'. Monitor IDs must be numeric.", trimmedId);
}
}
}
if (validMonitorIds.isEmpty()) {
return "Error: No valid monitor IDs provided";
}
// Build the monitor instance condition
String monitorCondition;
if (validMonitorIds.size() == 1) {
monitorCondition = String.format("equals(__instance__, \"%s\")", validMonitorIds.get(0));
} else {
StringBuilder conditionBuilder = new StringBuilder("(");
for (int i = 0; i < validMonitorIds.size(); i++) {
if (i > 0) {
conditionBuilder.append(" or ");
}
conditionBuilder.append(String.format("equals(__instance__, \"%s\")", validMonitorIds.get(i)));
}
conditionBuilder.append(")");
monitorCondition = conditionBuilder.toString();
}
// Get the current expression and modify it
String currentExpr = existingRule.getExpr();
String newExpr;
// Check if the expression already has __instance__ conditions
if (currentExpr.contains("__instance__")) {
// Extract existing monitor IDs and merge with new ones
List<String> existingMonitorIds = UtilityClass.extractExistingMonitorIds(currentExpr);
// Add new monitor IDs that aren't already present
for (String newId : validMonitorIds) {
if (!existingMonitorIds.contains(newId)) {
existingMonitorIds.add(newId);
}
}
String updatedMonitorCondition;
if (existingMonitorIds.size() == 1) {
updatedMonitorCondition = String.format("equals(__instance__, \"%s\")", existingMonitorIds.get(0));
} else {
StringBuilder conditionBuilder = new StringBuilder("(");
for (int i = 0; i < existingMonitorIds.size(); i++) {
if (i > 0) {
conditionBuilder.append(" or ");
}
conditionBuilder.append(String.format("equals(__instance__, \"%s\")", existingMonitorIds.get(i)));
}
conditionBuilder.append(")");
updatedMonitorCondition = conditionBuilder.toString();
}
// Replace existing __instance__ conditions with updated ones
newExpr = UtilityClass.replaceInstanceConditions(currentExpr, updatedMonitorCondition);
// Update the alert rule
existingRule.setExpr(newExpr);
log.debug("Current security subject for modifyAlertDefine: {}", subjectSum);
alertDefineService.modifyAlertDefine(existingRule);
log.info("Successfully added monitors {} to existing bindings for alert rule ID: {}", validMonitorIds, ruleId);
return String.format("Successfully added %d new monitor(s) to alert rule ID %d.\nTotal bound monitors: %s\nUpdated expression: %s",
validMonitorIds.size(), ruleId, String.join(", ", existingMonitorIds), newExpr);
}
// Insert the monitor condition after the metrics condition
// Pattern: equals(__app__,"app") && equals(__metrics__,"metric") && [existing_conditions]
// Result: equals(__app__,"app") && equals(__metrics__,"metric") && [monitor_condition] && [existing_conditions]
if (currentExpr.matches(".*equals\\(__app__,\"[^\"]+\"\\)\\s*&&\\s*equals\\(__metrics__,\"[^\"]+\"\\)\\s*&&\\s*.*")) {
// Find the position after the metrics condition
String metricsPattern = "equals\\(__metrics__,\"[^\"]+\"\\)";
java.util.regex.Pattern regex = java.util.regex.Pattern.compile(metricsPattern);
java.util.regex.Matcher matcher = regex.matcher(currentExpr);
if (matcher.find()) {
int metricsEnd = matcher.end();
// Find the " && " after the metrics condition
int andPosition = currentExpr.indexOf(" && ", metricsEnd);
if (andPosition != -1) {
String beforeAndPosition = currentExpr.substring(0, andPosition + 4); // Include " && "
String afterAndPosition = currentExpr.substring(andPosition + 4); // Everything after " && "
newExpr = beforeAndPosition + monitorCondition + " && " + afterAndPosition;
} else {
return String.format("Error: Unable to find field conditions after metrics in expression: %s", currentExpr);
}
} else {
return String.format("Error: Unable to parse metrics condition in expression: %s", currentExpr);
}
} else {
return String.format("Error: Expression format not supported for monitor binding: %s", currentExpr);
}
// Update the alert rule
existingRule.setExpr(newExpr);
alertDefineService.modifyAlertDefine(existingRule);
log.info("Successfully bound monitors {} to alert rule ID: {}", validMonitorIds, ruleId);
return String.format("Successfully bound %d monitor(s) to alert rule ID %d.\nMonitor IDs: %s\nUpdated expression: %s",
validMonitorIds.size(), ruleId, String.join(", ", validMonitorIds), newExpr);
} catch (Exception e) {
log.error("Failed to bind monitors to alert rule ID {}: {}", ruleId, e.getMessage(), e);
return String.format("Error binding monitors to alert rule: %s", e.getMessage());
}
}
/**
* Validates that the app, metric, and field conditions are valid according to hierarchy
* @param app App name to validate
* @param metrics Metric name to validate for the app
* @param fieldConditions Field conditions to validate for the metric
* @return "VALID" if all relationships are correct, error message otherwise
*/
private String validateHierarchyRelationships(String app, String metrics, String fieldConditions) {
try {
log.debug("Validating hierarchy relationships: app={}, metrics={}, fieldConditions={}", app, metrics, fieldConditions);
// Get hierarchy for the specified app
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAppHierarchy: {}", subjectSum);
List<Hierarchy> hierarchies = appService.getAppHierarchy(app.toLowerCase(), "en-US");
if (hierarchies == null || hierarchies.isEmpty()) {
return String.format("Error: App '%s' not found in hierarchy. Please use list_monitor_types to get valid app names.", app);
}
// Find the metric in the app's hierarchy
Hierarchy metricHierarchy = UtilityClass.findMetricInHierarchy(hierarchies, metrics);
if (metricHierarchy == null) {
return String.format("Error: Metric '%s' not found for app '%s'. Please use get_apps_metrics_hierarchy to get valid metrics for this app.", metrics, app);
}
// Extract field names from field conditions and validate them
List<String> fieldNames = UtilityClass.extractFieldNamesFromConditions(fieldConditions);
for (String fieldName : fieldNames) {
if (!UtilityClass.isFieldValidForMetric(metricHierarchy, fieldName)) {
return String.format("Error: Field '%s' not found for metric '%s' in app '%s'. Please use get_apps_metrics_hierarchy to get valid field parameters.", fieldName, metrics, app);
}
}
log.debug("Hierarchy validation passed for app={}, metrics={}", app, metrics);
return "VALID";
} catch (Exception e) {
log.error("Error during hierarchy validation: {}", e.getMessage(), e);
return String.format("Error: Unable to validate hierarchy relationships: %s", e.getMessage());
}
}
}
@@ -1,245 +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.ai.tools.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.config.McpContextHolder;
import org.apache.hertzbeat.ai.tools.AlertTools;
import org.apache.hertzbeat.ai.utils.UtilityClass;
import org.apache.hertzbeat.alert.dto.AlertSummary;
import org.apache.hertzbeat.alert.service.AlertService;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Service;
/**
* Implementation of Alert Tools functionality for alarm data queries and management
*/
@Slf4j
@Service
public class AlertToolsImpl implements AlertTools {
@Autowired
private AlertService alertService;
@Override
@Tool(name = "query_alerts", description = """
Query alerts with comprehensive filtering and pagination options.
ALERT TYPES:
- Pass alertType='single' for individual alert instances
- Pass alertType='group' for grouped/aggregated alerts
- Pass alertType='both' to get both types (separate sections)
STATUS FILTERING:
- 'firing': Currently active alerts requiring attention
- 'resolved': Previously active alerts that have been cleared
- 'all': Both firing and resolved alerts (default)
SEARCH & FILTERING:
- search: Search in alert content, labels, or descriptions
- sort: Order by 'startAt' (trigger time), 'triggerTimes' (frequency), 'status'
- order: 'asc' (oldest first) or 'desc' (newest first, default)
PAGINATION:
- pageIndex: Page number starting from 0
- pageSize: Number of alerts per page (default: 10, max recommended: 50)
EXAMPLE AND COMMON USE CASES:
- Recent active alerts: alertType='single', status='firing', sort='startAt', order='desc'
- Historical analysis: alertType='single', status='resolved', pageSize=50
- Alert grouping overview: alertType='group', status='all'
- Search specific issues: search='cpu', alertType='single', status='firing'
- Find abnormal monitors: status='firing' to get active alerts indicating monitor issues
- Monitor-specific alerts: use search parameter with monitor ID or name to find related alerts
- Frequent alerts analysis: sort='triggerTimes', order='desc' to find most frequently triggered alerts
- Recent recurring issues: status='all', sort='triggerTimes', order='desc', pageSize=20
""")
public String queryAlerts(
@ToolParam(description = "Alert type: 'single' (individual alerts), 'group' (grouped alerts), 'both' (default: single)", required = false) String alertType,
@ToolParam(description = "Alert status: 'firing' (active), 'resolved' (cleared), 'all' (default: all)", required = false) String status,
@ToolParam(description = "Search term for alert content or labels", required = false) String search,
@ToolParam(description = "Sort field: 'startAt', 'triggerTimes', 'status' (default: startAt)", required = false) String sort,
@ToolParam(description = "Sort order: 'asc' or 'desc' (default: desc)", required = false) String order,
@ToolParam(description = "Page index starting from 0 (default: 0)", required = false) Integer pageIndex,
@ToolParam(description = "Page size, 1-50 recommended (default: 10)", required = false) Integer pageSize) {
try {
log.info("Querying alerts: alertType={}, status={}, search={}, sort={}, order={}", alertType, status, search, sort, order);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current subject in query_alerts tool: {}", subjectSum);
// Set defaults
if (alertType == null || alertType.trim().isEmpty()) {
alertType = "single";
}
if (status == null || status.trim().isEmpty()) {
status = "all";
}
if (sort == null || sort.trim().isEmpty()) {
sort = "startAt";
}
if (order == null || order.trim().isEmpty()) {
order = "desc";
}
if (pageIndex == null) {
pageIndex = 0;
}
if (pageSize == null) {
pageSize = 10;
}
StringBuilder response = new StringBuilder();
response.append("ALERT QUERY RESULTS\n");
response.append("===================\n\n");
// Handle different alert types
if ("single".equalsIgnoreCase(alertType) || "both".equalsIgnoreCase(alertType)) {
Page<SingleAlert> singleResult = alertService.getSingleAlerts(status, search, sort, order, pageIndex, pageSize);
response.append("SINGLE ALERTS:\n");
response.append("Found ").append(singleResult.getContent().size()).append(" single alerts (Total: ").append(singleResult.getTotalElements()).append("):\n\n");
for (SingleAlert alert : singleResult.getContent()) {
response.append("Alert ID: ").append(alert.getId()).append("\n");
response.append("Status: ").append(alert.getStatus()).append("\n");
response.append("Content: ").append(alert.getContent() != null ? alert.getContent() : "No content").append("\n");
response.append("Trigger Times: ").append(alert.getTriggerTimes()).append("\n");
if (alert.getStartAt() != null) {
response.append("Started At: ").append(UtilityClass.formatTimestamp(alert.getStartAt())).append("\n");
}
if (alert.getActiveAt() != null) {
response.append("Active At: ").append(UtilityClass.formatTimestamp(alert.getActiveAt())).append("\n");
}
if (alert.getEndAt() != null) {
response.append("Ended At: ").append(UtilityClass.formatTimestamp(alert.getEndAt())).append("\n");
}
if (alert.getLabels() != null && !alert.getLabels().isEmpty()) {
response.append("Labels: ").append(alert.getLabels()).append("\n");
}
response.append("\n");
}
if (singleResult.getContent().isEmpty()) {
response.append("No single alerts found matching the specified criteria.\n");
}
}
// Handle group alerts
if ("group".equalsIgnoreCase(alertType) || "both".equalsIgnoreCase(alertType)) {
if ("both".equalsIgnoreCase(alertType)) {
response.append("\n");
}
Page<GroupAlert> groupResult = alertService.getGroupAlerts(status, search, sort, order, pageIndex, pageSize);
response.append("GROUP ALERTS:\n");
response.append("Found ").append(groupResult.getContent().size()).append(" group alerts (Total: ").append(groupResult.getTotalElements()).append("):\n\n");
for (GroupAlert alert : groupResult.getContent()) {
response.append("Group Alert ID: ").append(alert.getId()).append("\n");
response.append("Status: ").append(alert.getStatus()).append("\n");
response.append("Group Key: ").append(alert.getGroupKey() != null ? alert.getGroupKey() : "No group key").append("\n");
if (alert.getGmtCreate() != null) {
response.append("Created At: ").append(alert.getGmtCreate()).append("\n");
}
if (alert.getGmtUpdate() != null) {
response.append("Updated At: ").append(alert.getGmtUpdate()).append("\n");
}
if (alert.getCommonLabels() != null && !alert.getCommonLabels().isEmpty()) {
response.append("Common Labels: ").append(alert.getCommonLabels()).append("\n");
}
if (alert.getCommonAnnotations() != null && !alert.getCommonAnnotations().isEmpty()) {
response.append("Annotations: ").append(alert.getCommonAnnotations()).append("\n");
}
response.append("\n");
}
if (groupResult.getContent().isEmpty()) {
response.append("No group alerts found matching the specified criteria.\n");
}
}
return response.toString();
} catch (Exception e) {
log.error("Failed to query alerts: {}", e.getMessage(), e);
return "Error retrieving alerts: " + e.getMessage();
}
}
@Override
@Tool(name = "get_alerts_summary", description = """
Get alerts summary statistics including total counts, status distribution, and recent trends.
Returns comprehensive overview of the current alerting status across all monitors.
""")
public String getAlertsSummary() {
try {
log.info("Getting alerts summary");
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current subject in get_alerts_summary tool: {}", subjectSum);
AlertSummary summary = alertService.getAlertsSummary();
StringBuilder response = new StringBuilder();
response.append("ALERTS SUMMARY\n");
response.append("==============\n\n");
if (summary != null) {
response.append("Total Alerts: ").append(summary.getTotal()).append("\n");
response.append("Handled Alerts: ").append(summary.getDealNum()).append("\n");
response.append("Handling Rate: ").append(String.format("%.1f", summary.getRate())).append("%\n\n");
response.append("Priority Breakdown (Unhandled):\n");
response.append("- Critical: ").append(summary.getPriorityCriticalNum()).append("\n");
response.append("- Emergency: ").append(summary.getPriorityEmergencyNum()).append("\n");
response.append("- Warning: ").append(summary.getPriorityWarningNum()).append("\n\n");
long totalUnhandled = summary.getPriorityCriticalNum() + summary.getPriorityEmergencyNum() + summary.getPriorityWarningNum();
response.append("Total Unhandled Alerts: ").append(totalUnhandled).append("\n");
if (totalUnhandled > 0) {
response.append("\nUnhandled Alert Distribution:\n");
response.append("- Critical: ").append(String.format("%.1f", (summary.getPriorityCriticalNum() * 100.0 / totalUnhandled))).append("%\n");
response.append("- Emergency: ").append(String.format("%.1f", (summary.getPriorityEmergencyNum() * 100.0 / totalUnhandled))).append("%\n");
response.append("- Warning: ").append(String.format("%.1f", (summary.getPriorityWarningNum() * 100.0 / totalUnhandled))).append("%\n");
}
} else {
response.append("No alert summary data available.");
}
return response.toString();
} catch (Exception e) {
log.error("Failed to get alerts summary: {}", e.getMessage(), e);
return "Error retrieving alerts summary: " + e.getMessage();
}
}
}
@@ -1,262 +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.ai.tools.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.config.McpContextHolder;
import org.apache.hertzbeat.warehouse.service.MetricsDataService;
import org.apache.hertzbeat.ai.tools.MetricsTools;
import org.apache.hertzbeat.common.entity.dto.Field;
import org.apache.hertzbeat.common.entity.dto.MetricsData;
import org.apache.hertzbeat.common.entity.dto.MetricsHistoryData;
import org.apache.hertzbeat.common.entity.dto.Value;
import org.apache.hertzbeat.common.entity.dto.ValueRow;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
/**
* Implementation of Metrics Tools functionality for metrics data queries and analysis
*/
@Slf4j
@Service
public class MetricsToolsImpl implements MetricsTools {
@Autowired
private MetricsDataService metricsDataService;
@Override
@Tool(name = "query_realtime_metrics", description = """
Get the supported monitor types/names from the list_monitor_types tool, make sure to use right name in the next call
Use the query_monitors tool to find monitor IDs in case the user does not tell the id explicitly. You might have to use this multiple times based on the user's query
Get real-time metrics data for a specific monitor.
Returns current metrics values including CPU, memory, disk usage, etc.
Based on the monitor type/name, use the get_apps_metrics_hierarchy tool to get the metrics hierarchy. i.e., metrics and the field parameter (sub-metric).
Each metric has its submetrics as well for example: cpu has field parameters or sub-metrics like 'usage', 'load', 'core'. These value might be numeric or string
User might ask about specific metrics like 'cpu usage', 'memory used', 'disk available', etc.
So use this tool to get the real-time metrics data for the monitor- user asked the specific metrics for, along with any logical or numeric conditions if the user mentions
In case of multiple monitors matching the user's description, you might have to call query_realtime_metrics tool mutliple times with different parameters.
EXAMPLE WORKFLOW
If the user asks for 'cpu usage' for a particular monitor or multiple matching monitors like 'web server', 'database server', etc.
Get the closest matching monitor name from list_monitor_types tool.
Call query_monitors tool to get the monitor ID/IDs with the obtained monitor type
Call get_apps_metrics_hierarchy tool to get all the metrics (type=metric) for the obtained monitor type
Call query_realtime_metrics tool with each of the IDs and the closest matching metrics name (e.g., 'cpu', 'memory', etc.)
From the result, do whatever operation user wants you to do with the data. In this example case display the cpu usage for each matching monitor.
""")
public String getRealtimeMetrics(
@ToolParam(description = "Monitor ID", required = true) Long monitorId,
@ToolParam(description = "Metrics name (e.g., 'system', 'cpu', 'memory') obtained from get_apps_metrics_hierarchy result", required = true) String metrics) {
try {
log.info("Getting real-time metrics for monitor {} and metrics {}", monitorId, metrics);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current subject in get_realtime_metrics tool: {}", subjectSum);
MetricsData metricsData = metricsDataService.getMetricsData(monitorId, metrics);
if (metricsData == null) {
return String.format("No real-time metrics data found for monitor ID %d and metrics '%s'", monitorId, metrics);
}
StringBuilder response = new StringBuilder();
response.append("REAL-TIME METRICS DATA\n");
response.append("=".repeat(50)).append("\n");
response.append("Monitor ID: ").append(monitorId).append("\n");
response.append("Metrics: ").append(metrics).append("\n");
response.append("=".repeat(50)).append("\n\n");
if (metricsData.getValueRows() != null && !metricsData.getValueRows().isEmpty()) {
List<Field> fields = metricsData.getFields();
response.append("Available Field Parameters (Sub-metrics):\n");
response.append("-".repeat(40)).append("\n");
for (ValueRow valueRow : metricsData.getValueRows()) {
// Show labels if available
if (valueRow.getLabels() != null && !valueRow.getLabels().isEmpty()) {
response.append("Instance Labels: ").append(valueRow.getLabels()).append("\n");
response.append("-".repeat(20)).append("\n");
}
List<Value> values = valueRow.getValues();
for (int i = 0; i < values.size() && i < fields.size(); i++) {
Field field = fields.get(i);
Value value = values.get(i);
// Enhanced field information display
response.append("• Field Parameter: ").append(field.getName());
if (field.getUnit() != null && !field.getUnit().isEmpty()) {
response.append(" (").append(field.getUnit()).append(")");
}
response.append("\n");
response.append(" Current Value: ").append(value.getOrigin());
// Add data type indication
try {
Double.parseDouble(value.getOrigin());
response.append(" [Numeric]");
} catch (NumberFormatException e) {
response.append(" [String]");
}
response.append("\n\n");
}
if (valueRow.getLabels() != null && !valueRow.getLabels().isEmpty()) {
response.append("-".repeat(20)).append("\n");
}
}
} else {
response.append("No field parameter data available for metrics '").append(metrics).append("'.\n");
response.append("Use get_apps_metrics_hierarchy tool to check available metrics for this monitor type.");
}
return response.toString();
} catch (Exception e) {
log.error("Failed to get real-time metrics: {}", e.getMessage(), e);
return "Error retrieving real-time metrics: " + e.getMessage();
}
}
@Override
@Tool(name = "get_historical_metrics", description = """
Get historical metrics data for analysis and trending.
Returns time-series data for specified metrics over a time range.
Use the query_monitors tool to find the correct monitor IDs/ name or type for the monitor(s) user asked the metrics for
Pass that name into the get_apps_metrics_hierarchy tool to get the metrics hierarchy i.e metrics and the field paramater
DO NOT USE THE LABEL FIELD ALWAYS USE THE VALUE FIELD FROM THE HIERARCHY JSON
Ask user to provide the filters for labels, history and interval aggregation
""")
public String getHistoricalMetrics(
@ToolParam(description = "Monitor ID", required = true) Long monitorId,
@ToolParam(description = "Monitor type (e.g., 'linux', 'mysql', 'http')", required = true) String app,
@ToolParam(description = "Metrics name (e.g., 'target', 'cpu', 'memory')", required = true) String metrics,
@ToolParam(description = "Field Parameter (e.g., 'usage', 'used', 'available')", required = false) String fieldParameter,
@ToolParam(description = "Label filter for specific instances", required = false) String label,
@ToolParam(description = "Time range (e.g., '1h', '6h', '24h', '7d')", required = false) String history,
@ToolParam(description = "Whether to aggregate data with intervals", required = false) Boolean interval) {
try {
log.info("Getting historical metrics for monitor {} and metrics {}", monitorId, metrics);
if (history == null || history.trim().isEmpty()) {
history = "24h";
}
if (interval == null) {
interval = true;
}
MetricsHistoryData historyData = metricsDataService.getMetricHistoryData(
monitorId, app, metrics, fieldParameter, label, history, interval);
if (historyData == null) {
return String.format("No historical metrics data found for monitor ID %d and metrics '%s'", monitorId, metrics);
}
StringBuilder response = new StringBuilder();
response.append("HISTORICAL METRICS: ").append(metrics).append(" (Monitor ID: ").append(monitorId).append(")\n");
response.append("Time Range: ").append(history).append(" | Interval Aggregation: ").append(interval).append("\n");
response.append("=".repeat(60)).append("\n\n");
if (historyData.getValues() != null && !historyData.getValues().isEmpty()) {
response.append("Field: ").append(historyData.getField() != null ? historyData.getField().getName() : "Unknown").append("\n");
// Calculate total data points
int totalPoints = historyData.getValues().values().stream()
.mapToInt(List::size)
.sum();
response.append("Data Points: ").append(totalPoints).append("\n");
// Show sample data points (first 10) from all value lists
int count = 0;
response.append("\nSample Data Points:\n");
for (Map.Entry<String, List<Value>> entry : historyData.getValues().entrySet()) {
String labelKey = entry.getKey();
List<Value> values = entry.getValue();
for (Value value : values) {
if (count >= 10) break;
response.append("Label: ").append(labelKey)
.append(" | Time: ").append(value.getTime())
.append(" | Value: ").append(value.getOrigin()).append("\n");
count++;
}
if (count >= 10) break;
}
if (totalPoints > 10) {
response.append("... and ").append(totalPoints - 10).append(" more data points\n");
}
} else {
response.append("No historical data points available.");
}
return response.toString();
} catch (Exception e) {
log.error("Failed to get historical metrics: {}", e.getMessage(), e);
return "Error retrieving historical metrics: " + e.getMessage();
}
}
@Override
@Tool(name = "get_warehouse_status", description = """
Check the status of the metrics storage warehouse system.
Returns whether the metrics storage is operational and accessible.
""")
public String getWarehouseStatus() {
try {
log.info("Checking warehouse storage status");
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getWarehouseStorageServerStatus: {}", subjectSum);
Boolean status = metricsDataService.getWarehouseStorageServerStatus();
StringBuilder response = new StringBuilder();
response.append("METRICS WAREHOUSE STATUS\n");
response.append("========================\n\n");
if (status != null && status) {
response.append("Status: ONLINE ✓\n");
response.append("The metrics storage warehouse is operational and accessible.\n");
response.append("Historical metrics data queries are available.");
} else {
response.append("Status: OFFLINE ✗\n");
response.append("The metrics storage warehouse is not accessible.\n");
response.append("Only real-time metrics may be available.");
}
return response.toString();
} catch (Exception e) {
log.error("Failed to get warehouse status: {}", e.getMessage(), e);
return "Error checking warehouse status: " + e.getMessage();
}
}
}
@@ -1,505 +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.ai.tools.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.config.McpContextHolder;
import org.apache.hertzbeat.manager.service.MonitorService;
import org.apache.hertzbeat.manager.service.AppService;
import org.apache.hertzbeat.ai.utils.UtilityClass;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.apache.hertzbeat.ai.tools.MonitorTools;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.apache.hertzbeat.common.entity.manager.Monitor;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Implementation of Monitoring Tools functionality
*/
@Slf4j
@Service
public class MonitorToolsImpl implements MonitorTools {
@Autowired
private MonitorService monitorService;
@Autowired
private AppService appService;
/**
* Tool to query monitor information with flexible filtering and pagination.
* Supports filtering by monitor IDs, type, status, host, labels, sorting, and
* pagination.
* Returns detailed monitor information including ID, name, type, host, and status.
*/
@Override
@Tool(name = "query_monitors", description = """
Query Existing/configured monitors in HertzBeat.
This tool retrieves monitors based on various filters and parameters.
Comprehensive monitor querying with flexible filtering, pagination, and specialized views.
MONITOR STATUSES:
- status=1: Online/Active monitors (healthy, responding normally)
- status=2: Offline monitors (not responding, connection failed)
- status=3: Unreachable monitors (network/connectivity issues)
- status=0: Paused monitors (manually disabled/suspended)
- status=9 or null: All monitors regardless of status (default)
COMMON USE CASES & PARAMETER COMBINATIONS:
1. BASIC MONITOR LISTING:
- Default: No parameters (shows all monitors, 8 per page)
- By type: app='linux' (show only Linux monitors)
- Search: search='web' (find monitors with 'web' in name/host)
2. STATUS-BASED QUERIES:
- Healthy monitors: status=1, pageSize=50
- Problem monitors: status=2 or status=3, pageSize=50
- Offline monitors only: status=2
- Unreachable monitors only: status=3
- Paused monitors: status=0
3. MONITORING HEALTH OVERVIEW:
- All statuses with statistics: status=9, includeStats=true, pageSize=100
- Unhealthy monitors: Pass both status=2 AND status=3 (make 2 separate calls)
4. ADVANCED FILTERING:
- Specific monitor types: app='mysql', status=1 (healthy MySQL monitors)
- Label-based: labels='env:prod,critical:true'
- Host search: search='192.168' (find by IP pattern)
- Monitor IDs: ids=[1,2,3] (specific monitors by ID)
5. SORTING & PAGINATION:
- Recently updated: sort='gmtUpdate', order='desc'
- Alphabetical: sort='name', order='asc'
- By creation: sort='gmtCreate', order='desc' (newest first)
- Large datasets: pageSize=50-100 for bulk operations
RESPONSE FORMAT:
- includeStats=true: Adds status distribution summary at top
- Default: Simple list with ID, name, type, host, status
- Shows total count and pagination info
""")
public String queryMonitors(
@ToolParam(description = "Specific monitor IDs to retrieve (optional)", required = false) List<Long> ids,
@ToolParam(description = "Monitor type filter: 'linux', 'mysql', 'http', 'redis', etc. (optional)", required = false) String app,
@ToolParam(description = "Monitor status: 1=online, 2=offline, 3=unreachable, 0=paused, 9=all (default: 9)", required = false) Byte status,
@ToolParam(description = "Search in monitor names or hosts (partial matching)", required = false) String search,
@ToolParam(description = "Label filters, format: 'key1:value1,key2:value2'", required = false) String labels,
@ToolParam(description = "Sort field: 'name', 'gmtCreate', 'gmtUpdate', 'status', 'app' (default: gmtCreate)", required = false) String sort,
@ToolParam(description = "Sort order: 'asc' (ascending) or 'desc' (descending, default)", required = false) String order,
@ToolParam(description = "Page number starting from 0 (default: 0)", required = false) Integer pageIndex,
@ToolParam(description = "Items per page: 1-100 recommended (default: 20)", required = false) Integer pageSize,
@ToolParam(description = "Include status statistics summary (default: false)", required = false) Boolean includeStats) {
try {
// Set defaults
if (pageSize == null || pageSize <= 0) {
pageSize = 20;
}
if (pageIndex == null) {
pageIndex = 0;
}
if (includeStats == null) {
includeStats = false;
}
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject: {}", subjectSum);
Page<Monitor> result = monitorService.getMonitors(
ids, app, search, status, sort, order, pageIndex, pageSize, labels);
log.debug("MonitorService.getMonitors result: {}", result);
StringBuilder response = new StringBuilder();
response.append("MONITOR QUERY RESULTS\n");
response.append("====================\n\n");
// Include statistics if requested
if (includeStats) {
// Get status distribution by calling with different status values
long onlineCount = monitorService.getMonitors(null, app, search, (byte) 1, null, null, 0, 1000, labels).getTotalElements();
long offlineCount = monitorService.getMonitors(null, app, search, (byte) 2, null, null, 0, 1000, labels).getTotalElements();
long unreachableCount = monitorService.getMonitors(null, app, search, (byte) 3, null, null, 0, 1000, labels).getTotalElements();
long pausedCount = monitorService.getMonitors(null, app, search, (byte) 0, null, null, 0, 1000, labels).getTotalElements();
response.append("STATUS OVERVIEW:\n");
response.append("- Online: ").append(onlineCount).append("\n");
response.append("- Offline: ").append(offlineCount).append("\n");
response.append("- Unreachable: ").append(unreachableCount).append("\n");
response.append("- Paused: ").append(pausedCount).append("\n");
long total = onlineCount + offlineCount + unreachableCount + pausedCount;
if (total > 0) {
double healthPercentage = (onlineCount * 100.0) / total;
response.append("- Health Rate: ").append(String.format("%.1f", healthPercentage)).append("%\n");
}
response.append("\n");
}
response.append("Query Results: ").append(result.getContent().size())
.append(" monitors (Total: ").append(result.getTotalElements()).append(")\n");
if (result.getTotalPages() > 1) {
response.append("Page ").append(pageIndex + 1).append(" of ").append(result.getTotalPages()).append("\n");
}
response.append("\n");
for (Monitor monitor : result.getContent()) {
response.append("ID: ").append(monitor.getId())
.append(" | Name: ").append(monitor.getName())
.append(" | Type: ").append(monitor.getApp())
.append(" | Host: ").append(monitor.getHost())
.append(" | Status: ").append(UtilityClass.getStatusText(monitor.getStatus()));
// Add creation date for better context
if (monitor.getGmtCreate() != null) {
response.append(" | Created: ").append(monitor.getGmtCreate().toString(), 0, 10);
}
response.append("\n");
}
if (result.getContent().isEmpty()) {
response.append("No monitors found matching the specified criteria.\n");
response.append("Try adjusting your filters or search terms.");
}
return response.toString();
} catch (Exception e) {
return "Error retrieving monitors: " + e.getMessage();
}
}
@Override
@Tool(name = "add_monitor", description = """
Add a new monitoring target to HertzBeat with comprehensive configuration.
This tool dynamically handles different parameter requirements for each monitor type.
This tool creates monitors with proper app-specific parameters.
*********
VERY IMPORTANT:
ALWAYS use get_monitor_additional_params to check the additional required parameters for the chosen type before adding a monitor or even mentioning it.
Use list_monitor_types tool to see available monitor type names to use here in the app parameter.
Use the information obtained from this to query user for parameters.
If the User has not given any parameters, ask them to provide the necessary parameters, until all the necessary parameters are provided.
**********
Examples of natural language requests this tool handles:
- "Monitor website example.com with HTTPS on port 443"
- "Add MySQL monitoring for database server at 192.168.1.10 with user admin"
- "Monitor Linux server health on host server.company.com via SSH"
- "Set up Redis monitoring on localhost port 6379 with password"
PARAMETER MAPPING: The tool intelligently maps common parameters:
- host: Target server/domain
- port: Service port (auto-detected if not specified)
- username: Authentication username
- password: Authentication password
- database: Database name (for DB monitors)
- additionalParams: JSON string for app-specific parameters (to be obtained from get_monitor_param_defines)
ADDITIONAL PARAMETERS EXAMPLES:
- Website: {"uri":"/api/health", "ssl":"true", "method":"POST"}
- Linux: {"privateKey":"ssh-key-content", "script":"custom-script"}
- Database: {"url":"jdbc:mysql://custom", "timeout":"10000"}
""")
public String addMonitor(
@ToolParam(description = "Monitor name (required)", required = true) String name,
@ToolParam(description = "Monitor type: website, mysql, postgresql, redis, linux, windows, etc.", required = true) String app,
@ToolParam(description = "Target host: IP address or domain name", required = true) String host,
@ToolParam(description = "Target port (optional, auto-detected if not specified)", required = false) Integer port,
@ToolParam(description = "Collection interval in seconds (default: 600)", required = false) Integer intervals,
@ToolParam(description = "Username for authentication (optional)", required = false) String username,
@ToolParam(description = "Password for authentication (optional)", required = false) String password,
@ToolParam(description = "Database name (for database monitors)", required = false) String database,
@ToolParam(description = "Additional app-specific parameters as JSON: {\"uri\":\"/api\", \"ssl\":\"true\", \"method\":\"POST\"}", required = false) String additionalParams,
@ToolParam(description = "Monitor description (optional)", required = false) String description) {
try {
log.info("Adding monitor: name={}, app={}, host={}", name, app, host);
// Validate required parameters
if (name == null || name.trim().isEmpty()) {
return "Error: Monitor name is required";
}
if (app == null || app.trim().isEmpty()) {
return "Error: Monitor type/application is required";
}
if (host == null || host.trim().isEmpty()) {
return "Error: Host is required";
}
// Set defaults
if (intervals == null || intervals < 10) {
intervals = 600;
}
// Create Monitor entity
Monitor monitor = Monitor.builder()
.name(name.trim())
.app(app.toLowerCase().trim())
.host(host.trim())
.intervals(intervals)
.status((byte) 1)
.type((byte) 0)
.description(description != null ? description.trim() : "")
.build();
List<Param> params = createBasicParams(host, port, username, password, database, additionalParams);
// Validate that all required parameters for this monitor type are provided
try {
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getMonitorParamDefines: {}", subjectSum);
List<ParamDefine> requiredParams = appService.getAppParamDefines(app.toLowerCase().trim());
log.info("Checking required parameters for monitor type '{}': {}", app, requiredParams);
List<String> missingParams = new ArrayList<>();
for (ParamDefine paramDefine : requiredParams) {
if (paramDefine.isRequired()) {
String fieldName = paramDefine.getField();
boolean hasParam = params.stream()
.anyMatch(param -> fieldName.equals(param.getField()));
if (!hasParam) {
missingParams.add(fieldName);
}
}
}
if (!missingParams.isEmpty()) {
return String.format("Error: Missing required parameters for monitor type '%s': %s. "
+ "Use get_monitor_additional_params tool to see all required parameters.",
app, String.join(", ", missingParams));
}
} catch (Exception e) {
log.warn("Could not validate required parameters for monitor type '{}': {}", app, e.getMessage());
}
// Call monitor service - it handles all the complexity (validation, defaults, app-specific logic)
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for addMonitor: {}", subjectSum);
monitorService.addMonitor(monitor, params, null, null);
Long monitorId = monitor.getId();
log.info("Successfully added monitor '{}' with ID: {}", name, monitorId);
return String.format("Successfully added %s monitor '%s' with ID: %d (Host: %s, Interval: %d seconds)",
app.toUpperCase(), name, monitorId, host, intervals);
} catch (Exception e) {
log.error("Failed to add monitor '{}': {}", name, e.getMessage(), e);
return "Error adding monitor '" + name + "': " + e.getMessage();
}
}
/**
* Create basic parameter list from user inputs
*/
private List<Param> createBasicParams(String host, Integer port, String username,
String password, String database, String additionalParams) {
List<Param> params = new ArrayList<>();
// Add host (always required)
params.add(Param.builder().field("host").paramValue(host.trim()).type((byte) 1).build());
// Add optional common parameters
if (port != null) {
params.add(Param.builder().field("port").paramValue(port.toString()).type((byte) 0).build());
}
if (username != null && !username.trim().isEmpty()) {
params.add(Param.builder().field("username").paramValue(username.trim()).type((byte) 1).build());
}
if (password != null && !password.trim().isEmpty()) {
params.add(Param.builder().field("password").paramValue(password.trim()).type((byte) 2).build());
}
if (database != null && !database.trim().isEmpty()) {
params.add(Param.builder().field("database").paramValue(database.trim()).type((byte) 1).build());
}
// Parse additional parameters if provided
if (additionalParams != null && !additionalParams.trim().isEmpty()) {
try {
String cleaned = additionalParams.trim().replaceAll("[{}]", "");
String[] pairs = cleaned.split(",");
for (String pair : pairs) {
String[] keyValue = pair.split(":");
if (keyValue.length == 2) {
String key = keyValue[0].trim().replaceAll("\"", "");
String value = keyValue[1].trim().replaceAll("\"", "");
params.add(Param.builder().field(key).paramValue(value).type((byte) 1).build());
}
}
} catch (Exception e) {
log.warn("Failed to parse additionalParams: {}", e.getMessage());
}
}
return params;
}
@Override
@Tool(name = "list_monitor_types", description = """
List all available monitor types that can be added to HertzBeat.
This tool shows all supported monitor types with their display names.
Use this to see what types of monitors you can create with the add_monitor tool.
""")
public String listMonitorTypes(
@ToolParam(description = "Language code for localized names (en-US, zh-CN, etc.). Default: en-US", required = false) String language) {
try {
log.info("Listing available monitor types for language: {}", language);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current subject in list_monitor_types tool: {}", subjectSum);
// Set default language if not provided
if (language == null || language.trim().isEmpty()) {
language = "en-US";
}
// Get available monitor types from app service
Map<String, String> monitorTypes = appService.getI18nApps(language);
if (monitorTypes == null || monitorTypes.isEmpty()) {
return "No monitor types are currently available.";
}
// Format the response as a nice list
StringBuilder response = new StringBuilder();
response.append("Available Monitor Types (Total: ").append(monitorTypes.size()).append("):\n\n");
// Sort monitor types alphabetically by key
List<Map.Entry<String, String>> sortedTypes = monitorTypes.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.toList();
for (Map.Entry<String, String> entry : sortedTypes) {
String typeKey = entry.getKey();
String displayName = entry.getValue();
response.append("").append(typeKey)
.append(" - ").append(displayName)
.append("\n");
}
response.append("\nTo add a monitor, use the add_monitor tool with one of these types as the 'app' parameter.");
log.info("Successfully listed {} monitor types", monitorTypes);
return response.toString();
} catch (Exception e) {
log.error("Failed to list monitor types: {}", e.getMessage(), e);
return "Error retrieving monitor types: " + e.getMessage();
}
}
@Override
@Tool(name = "get_monitor_additional_params", description = """
Get the parameter definitions required for a specific monitor type.
This tool shows what parameters are needed when adding a monitor of the specified type,
ALWAYS use this before adding a monitor to understand what parameters the user needs to provide.
Use the app parameter to specify the monitor type/application name (e.g., 'linux', 'mysql', 'redis') this can be obtained from the list_monitor_types tool.
""")
public String getMonitorAdditionalParams(
@ToolParam(description = "Monitor type/application name (e.g., 'linux', 'mysql', 'redis')", required = true) String app) {
try {
log.info("Getting parameter definitions for monitor type: {}", app);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current subject in get_monitor_param_defines tool: {}", subjectSum);
// Validate required parameter
if (app == null || app.trim().isEmpty()) {
return "Error: Monitor type/application parameter is required";
}
// Get parameter definitions from app service
List<ParamDefine> paramDefines = appService.getAppParamDefines(app.toLowerCase().trim());
if (paramDefines == null || paramDefines.isEmpty()) {
return String.format("No parameter definitions found for monitor type '%s'. "
+ "This monitor type may not exist or may not require additional parameters.", app);
}
// Format the response
StringBuilder response = new StringBuilder();
response.append(String.format("Parameter Definitions for Monitor Type '%s' (Total: %d):\n\n",
app, paramDefines.size()));
for (ParamDefine paramDefine : paramDefines) {
response.append("• Field: ").append(paramDefine.getField()).append("\n");
// Add display name if available
if (paramDefine.getName() != null && !paramDefine.getName().toString().trim().isEmpty()) {
response.append(" Name: ").append(paramDefine.getName()).append("\n");
}
// Add type
if (paramDefine.getType() != null && !paramDefine.getType().trim().isEmpty()) {
response.append(" Type: ").append(paramDefine.getType()).append("\n");
}
// Add required status
response.append(" Required: ").append(paramDefine.isRequired() ? "Yes" : "No").append("\n");
// Add default value if present
if (paramDefine.getDefaultValue() != null && !paramDefine.getDefaultValue().trim().isEmpty()) {
response.append(" Default: ").append(paramDefine.getDefaultValue()).append("\n");
}
// Add validation range if present
if (paramDefine.getRange() != null && !paramDefine.getRange().trim().isEmpty()) {
response.append(" Range: ").append(paramDefine.getRange()).append("\n");
}
// Add limit if present
if (paramDefine.getLimit() != null) {
response.append(" Limit: ").append(paramDefine.getLimit()).append("\n");
}
// Add placeholder text if present
if (paramDefine.getPlaceholder() != null && !paramDefine.getPlaceholder().trim().isEmpty()) {
response.append(" Placeholder: ").append(paramDefine.getPlaceholder()).append("\n");
}
response.append("\n");
}
response.append("To add a monitor of this type, use the add_monitor tool with these parameters.\n");
response.append(String.format("Example: add_monitor(name='my-monitor', app='%s', host='your-host', ...)", app));
log.info("Successfully retrieved {} parameter definitions for monitor type: {}", paramDefines.size(), app);
return response.toString();
} catch (Exception e) {
log.error("Failed to get parameter definitions for monitor type '{}': {}", app, e.getMessage(), e);
return "Error retrieving parameter definitions for monitor type '" + app + "': " + e.getMessage();
}
}
}
@@ -1,495 +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.ai.utils;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.hertzbeat.manager.pojo.dto.Hierarchy;
/**
* Utility class providing helper methods for field expression validation, parsing,
* hierarchy management, timestamp formatting and metric/status conversions.
* This class contains stateless utility methods used across the application for
* common operations and validations.
*/
@Slf4j
@lombok.experimental.UtilityClass
public class UtilityClass {
/**
* Validates the syntax of field conditions expression
* @param fieldConditions Field conditions string to validate
* @return "VALID" if syntax is correct, error message otherwise
*/
public String validateExpressionSyntax(String fieldConditions) {
try {
log.debug("Validating expression syntax: {}", fieldConditions);
// Check for basic syntax requirements
if (fieldConditions == null || fieldConditions.trim().isEmpty()) {
return "Error: Field conditions cannot be empty";
}
// Check for balanced parentheses
if (!hasBalancedParentheses(fieldConditions)) {
return "Error: Unbalanced parentheses in field conditions. Please check your expression syntax.";
}
// Validate operators used in the expression
String operatorValidation = validateOperators(fieldConditions);
if (!operatorValidation.equals("VALID")) {
return operatorValidation;
}
// Validate logical connectors
String logicalValidation = validateLogicalConnectors(fieldConditions);
if (!logicalValidation.equals("VALID")) {
return logicalValidation;
}
// Validate function syntax (equals, contains, etc.)
String functionValidation = validateFunctions(fieldConditions);
if (!functionValidation.equals("VALID")) {
return functionValidation;
}
log.debug("Expression syntax validation passed for: {}", fieldConditions);
return "VALID";
} catch (Exception e) {
log.error("Error during expression syntax validation: {}", e.getMessage(), e);
return String.format("Error: Unable to validate expression syntax: %s", e.getMessage());
}
}
/**
* Checks if parentheses are balanced in the expression
*/
public boolean hasBalancedParentheses(String expression) {
int count = 0;
for (char c : expression.toCharArray()) {
if (c == '(') {
count++;
} else if (c == ')') {
count--;
if (count < 0) {
return false; // More closing than opening
}
}
}
return count == 0; // Should be perfectly balanced
}
/**
* Validates that only supported operators are used
*/
public String validateOperators(String fieldConditions) {
// Define supported operators for different field types
String[] numericOperators = {">", "<", ">=", "<=", "==", "!=", "exists()", "!exists()"};
String[] stringOperators = {"equals(", "contains(", "matches(", "exists()", "!equals(", "!contains(", "!matches(", "!exists()"};
String[] logicalOperators = {" and ", " or "};
// Remove quotes and function calls temporarily for operator checking
String tempExpression = fieldConditions
.replaceAll("\"[^\"]*\"", "VALUE") // Remove quoted strings
.replaceAll("'[^']*'", "VALUE") // Remove single quoted strings
.replaceAll("\\w+\\([^)]*\\)", "FUNCTION"); // Remove function calls
// Check for invalid operators (common mistakes)
String[] invalidOperators = {"&&", "||", "AND", "OR", "=", "!="};
for (String invalidOp : invalidOperators) {
if (tempExpression.contains(invalidOp)) {
if (invalidOp.equals("&&") || invalidOp.equals("||")) {
return String.format("Error: Use 'and'/'or' instead of '%s' for logical operations", invalidOp);
}
if (invalidOp.equals("AND") || invalidOp.equals("OR")) {
return String.format("Error: Use lowercase '%s' for logical operations", invalidOp.toLowerCase());
}
if (invalidOp.equals("=")) {
return "Error: Use '==' for equality comparison, not '='";
}
}
}
// Check for unsupported special characters that might indicate syntax errors
if (tempExpression.matches(".*[#$%^&*+\\[\\]{}|\\\\;:'\"`~].*")) {
return "Error: Expression contains unsupported special characters. Use only supported operators and functions.";
}
return "VALID";
}
/**
* Validates logical connectors syntax
*/
public String validateLogicalConnectors(String fieldConditions) {
// Check for proper spacing around logical operators
if (fieldConditions.matches(".*(\\S(and|or)\\S).*")) {
return "Error: Logical operators 'and'/'or' must be surrounded by spaces";
}
// Check for consecutive logical operators
if (fieldConditions.matches(".*(and\\s+and|or\\s+or|and\\s+or\\s+and|or\\s+and\\s+or).*")) {
return "Error: Consecutive logical operators found. Use parentheses to group conditions properly.";
}
// Check for logical operators at the beginning or end
String trimmed = fieldConditions.trim();
if (trimmed.startsWith("and ") || trimmed.startsWith("or ")
|| trimmed.endsWith(" and") || trimmed.endsWith(" or")) {
return "Error: Expression cannot start or end with logical operators 'and'/'or'";
}
return "VALID";
}
/**
* Validates function syntax (equals, contains, matches, etc.)
*/
public String validateFunctions(String fieldConditions) {
// Check for properly formed function calls
String[] supportedFunctions = {"equals", "contains", "matches", "exists", "!equals", "!contains", "!matches", "!exists"};
// Find all function-like patterns
java.util.regex.Pattern functionPattern = java.util.regex.Pattern.compile("(!?\\w+)\\s*\\(([^)]*)\\)");
java.util.regex.Matcher matcher = functionPattern.matcher(fieldConditions);
while (matcher.find()) {
String functionName = matcher.group(1);
String functionArgs = matcher.group(2);
// Check if function is supported
boolean isSupported = false;
for (String supportedFunc : supportedFunctions) {
if (functionName.equals(supportedFunc)) {
isSupported = true;
break;
}
}
if (!isSupported) {
return String.format("Error: Unsupported function '%s'. Supported functions: %s",
functionName, String.join(", ", supportedFunctions));
}
// Validate function arguments
if (functionName.equals("exists") || functionName.equals("!exists")) {
// exists() should have one parameter or no parameters
String[] args = functionArgs.trim().isEmpty() ? new String[0] : functionArgs.split(",");
if (args.length > 1) {
return String.format("Error: Function '%s' should have at most one parameter", functionName);
}
} else {
// Other functions should have exactly 2 parameters
String[] args = functionArgs.split(",");
if (args.length != 2) {
return String.format("Error: Function '%s' requires exactly 2 parameters (field, value)", functionName);
}
// Check that parameters are not empty
for (String arg : args) {
if (arg.trim().isEmpty()) {
return String.format("Error: Function '%s' has empty parameter", functionName);
}
}
}
}
return "VALID";
}
/**
* Helper method to parse key-value pairs from a string
* Format: "key1:value1, key2:value2, ..."
*/
public Map<String, String> parseKeyValuePairs(String input) {
Map<String, String> result = new HashMap<>();
if (input == null || input.trim().isEmpty()) {
return result;
}
String[] pairs = input.split(",");
for (String pair : pairs) {
String[] keyValue = pair.split(":");
if (keyValue.length == 2) {
result.put(keyValue[0].trim(), keyValue[1].trim());
}
}
return result;
}
/**
* Recursively searches for a metric in the hierarchy
*/
public Hierarchy findMetricInHierarchy(List<Hierarchy> hierarchies, String metricName) {
for (Hierarchy hierarchy : hierarchies) {
// Check if this is the metric we're looking for
if (metricName.equals(hierarchy.getValue())) {
// Verify it has field children (leaf nodes)
if (hierarchy.getChildren() != null && !hierarchy.getChildren().isEmpty()) {
boolean hasLeafChildren = hierarchy.getChildren().stream()
.anyMatch(child -> child.getIsLeaf() != null && child.getIsLeaf());
if (hasLeafChildren) {
return hierarchy;
}
}
}
// Recursively search in children
if (hierarchy.getChildren() != null) {
Hierarchy found = findMetricInHierarchy(hierarchy.getChildren(), metricName);
if (found != null) {
return found;
}
}
}
return null;
}
/**
* Checks if a field is valid for the given metric
*/
public boolean isFieldValidForMetric(Hierarchy metricHierarchy, String fieldName) {
if (metricHierarchy.getChildren() == null) {
return false;
}
for (Hierarchy child : metricHierarchy.getChildren()) {
if (child.getIsLeaf() != null && child.getIsLeaf() && fieldName.equals(child.getValue())) {
return true;
}
// Also check nested children
if (child.getChildren() != null && isFieldValidForMetric(child, fieldName)) {
return true;
}
}
return false;
}
/**
* Extracts field names from field conditions string
* Handles simple cases like "field > 80", "equals(field, 'value')", complex expressions
*/
public List<String> extractFieldNamesFromConditions(String fieldConditions) {
List<String> fieldNames = new ArrayList<>();
// Split by logical operators (and, or) and parentheses, but preserve the field names
// This is a simple implementation - could be enhanced with a proper parser
String[] parts = fieldConditions.split("\\s+(and|or|&&|\\|\\|)\\s+|[()]+");
for (String part : parts) {
part = part.trim();
if (part.isEmpty()) {
continue;
}
// Handle equals() function: equals(fieldName, "value")
if (part.contains("equals(")) {
String fieldName = extractFieldFromEquals(part);
if (fieldName != null && !fieldNames.contains(fieldName)) {
fieldNames.add(fieldName);
}
} else {
// Handle simple comparisons: fieldName > value, fieldName <= value
String fieldName = extractFieldFromComparison(part);
if (fieldName != null && !fieldNames.contains(fieldName)) {
fieldNames.add(fieldName);
}
}
}
return fieldNames;
}
/**
* Extracts field name from equals() function
*/
public String extractFieldFromEquals(String condition) {
// Pattern: equals(fieldName, "value") or equals(fieldName, value)
int startParen = condition.indexOf('(');
int comma = condition.indexOf(',');
if (startParen != -1 && comma != -1 && comma > startParen) {
String fieldName = condition.substring(startParen + 1, comma).trim();
// Remove quotes if present
if (fieldName.startsWith("\"") && fieldName.endsWith("\"")) {
fieldName = fieldName.substring(1, fieldName.length() - 1);
}
return fieldName;
}
return null;
}
/**
* Extracts field name from comparison operation
*/
public String extractFieldFromComparison(String condition) {
// Pattern: fieldName operator value
// Updated to include all supported operators
String[] operators = {" >= ", " <= ", " > ", " < ", " == ", " != "};
for (String operator : operators) {
if (condition.contains(operator)) {
String fieldName = condition.split(operator)[0].trim();
// Basic validation - field names shouldn't contain quotes or special chars
if (fieldName.matches("[a-zA-Z_][a-zA-Z0-9_]*")) {
return fieldName;
}
}
}
return null;
}
/**
* Helper method to format hierarchy structure as JSON recursively
*/
public ObjectNode formatHierarchyAsJson(ObjectMapper mapper, Hierarchy hierarchy) {
ObjectNode node = mapper.createObjectNode();
node.put("value", hierarchy.getValue());
node.put("label", hierarchy.getLabel());
if (hierarchy.getIsLeaf() != null && hierarchy.getIsLeaf()) {
// Leaf node - actual metric field parameter
node.put("type", "field_parameter");
if (hierarchy.getType() != null) {
node.put("dataType", hierarchy.getType() == 0 ? "numeric" : "string");
}
if (hierarchy.getUnit() != null && !hierarchy.getUnit().trim().isEmpty()) {
node.put("unit", hierarchy.getUnit());
}
node.put("description", "Available field parameter for alert conditions");
} else {
// Category, app, or metric node
// Determine node type based on children
boolean hasLeafChildren = hierarchy.getChildren().stream()
.anyMatch(child -> child.getIsLeaf() != null && child.getIsLeaf());
if (hasLeafChildren) {
node.put("type", "metric");
node.put("description", "Metric with available field parameters");
} else {
node.put("type", "app");
node.put("description", "Application with available metrics");
}
if (hierarchy.getChildren() != null && !hierarchy.getChildren().isEmpty()) {
ArrayNode childrenArray = mapper.createArrayNode();
for (Hierarchy child : hierarchy.getChildren()) {
childrenArray.add(formatHierarchyAsJson(mapper, child));
}
node.set("children", childrenArray);
}
}
return node;
}
/**
* Format timestamp to readable format
*/
public String formatTimestamp(Long timestamp) {
if (timestamp == null) {
return "N/A";
}
LocalDateTime dateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault());
return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
/**
* Helper method to convert monitor status byte to readable text
* @param status The status byte from monitor
* @return Human-readable status text
*/
public String getStatusText(Byte status) {
if (status == null) {
return "Unknown";
}
return switch (status) {
case 0 -> "Paused";
case 1 -> "Online";
case 2 -> "Offline";
case 3 -> "Unreachable";
default -> "Unknown (" + status + ")";
};
}
/**
* Extract existing monitor IDs from the alert expression
* @param expression The alert expression containing __instance__ conditions
* @return List of existing monitor IDs
*/
public List<String> extractExistingMonitorIds(String expression) {
List<String> monitorIds = new ArrayList<>();
String pattern = "equals\\(__instance__,\\s*\"([^\"]+)\"\\)";
java.util.regex.Pattern regex = java.util.regex.Pattern.compile(pattern);
java.util.regex.Matcher matcher = regex.matcher(expression);
while (matcher.find()) {
monitorIds.add(matcher.group(1));
}
return monitorIds;
}
/**
* Replace existing __instance__ conditions in the expression with new ones
* @param expression The current expression
* @param newMonitorCondition The new monitor condition to replace with
* @return Updated expression
*/
public String replaceInstanceConditions(String expression, String newMonitorCondition) {
// More precise pattern to match complete __instance__ condition blocks without cutting field parameters
// Pattern matches either:
// 1. Single: equals(__instance__, "id")
// 2. Multiple: (equals(__instance__, "id1") or equals(__instance__, "id2") or ...)
// First try to match grouped conditions: (equals(__instance__, "id1") or equals(__instance__, "id2"))
String groupedPattern = "\\(\\s*equals\\(__instance__,\\s*\"[^\"]+\"\\)(?:\\s+or\\s+equals\\(__instance__,\\s*\"[^\"]+\"\\))*\\s*\\)";
if (expression.matches(".*" + groupedPattern + ".*")) {
return expression.replaceFirst(groupedPattern, newMonitorCondition);
}
// Then try single condition: equals(__instance__, "id")
String singlePattern = "equals\\(__instance__,\\s*\"[^\"]+\"\\)";
if (expression.matches(".*" + singlePattern + ".*")) {
return expression.replaceFirst(singlePattern, newMonitorCondition);
}
// If no match found, return original expression
return expression;
}
}
+19
View File
@@ -98,6 +98,25 @@
<version>${easy-poi.version}</version>
<scope>compile</scope>
</dependency>
<!-- sms -->
<dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java-sms</artifactId>
<exclusions>
<exclusion>
<groupId>com.squareup.okhttp</groupId>
<artifactId>logging-interceptor</artifactId>
</exclusion>
<exclusion>
<groupId>com.squareup.okhttp</groupId>
<artifactId>okhttp</artifactId>
</exclusion>
<exclusion>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.huaweicloud.sdk</groupId>
<artifactId>huaweicloud-sdk-smn</artifactId>
@@ -35,12 +35,10 @@ public class AlerterWorkerPool {
private ThreadPoolExecutor workerExecutor;
private ThreadPoolExecutor notifyExecutor;
private ThreadPoolExecutor logWorkerExecutor;
public AlerterWorkerPool() {
initWorkExecutor();
initNotifyExecutor();
initLogWorkerExecutor();
}
private void initWorkExecutor() {
@@ -79,21 +77,6 @@ public class AlerterWorkerPool {
new ThreadPoolExecutor.AbortPolicy());
}
private void initLogWorkerExecutor() {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("Alerter logWorkerExecutor has uncaughtException.");
log.error(throwable.getMessage(), throwable);
})
.setDaemon(true)
.setNameFormat("log-worker-%d")
.build();
logWorkerExecutor = new ThreadPoolExecutor(10, 10, 10, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
threadFactory,
new ThreadPoolExecutor.AbortPolicy());
}
/**
* Run the alerter task
* @param runnable task
@@ -113,13 +96,4 @@ public class AlerterWorkerPool {
notifyExecutor.execute(runnable);
}
/**
* Executes the given runnable task using the logWorkerExecutor.
*
* @param runnable the task to be executed
* @throws RejectedExecutionException if the task cannot be accepted for execution
*/
public void executeLogJob(Runnable runnable) throws RejectedExecutionException {
logWorkerExecutor.execute(runnable);
}
}
@@ -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.alert.calculate;
import com.google.common.collect.Table;
import com.google.common.collect.Tables;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.alert.dao.SingleAlertDao;
import org.apache.hertzbeat.alert.util.AlertUtil;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
/**
* alert cache manager
*/
@Component
public class AlarmCacheManager {
private static final String CUSTOM_FIRING_ROW_KEY = "CUSTOM_FIRING_";
/**
* The alarm in the process is triggered
* rowKey - define id
* columnKey - labels fingerprint
*/
private final Table<String, String, SingleAlert> pendingAlertMap;
/**
* The not recover alert
* rowKey - define id
* columnKey - labels fingerprint
*/
private final Table<String, String, SingleAlert> firingAlertMap;
public AlarmCacheManager(SingleAlertDao singleAlertDao) {
this.pendingAlertMap = Tables.newCustomTable(new ConcurrentHashMap<>(8), ConcurrentHashMap::new);
this.firingAlertMap = Tables.newCustomTable(new ConcurrentHashMap<>(8), ConcurrentHashMap::new);
List<SingleAlert> singleAlerts = singleAlertDao.querySingleAlertsByStatus(CommonConstants.ALERT_STATUS_FIRING);
for (SingleAlert singleAlert : singleAlerts) {
String fingerprint = AlertUtil.calculateFingerprint(singleAlert.getLabels());
String defineId = singleAlert.getLabels().get(CommonConstants.LABEL_DEFINE_ID);
if (StringUtils.isBlank(defineId)) {
defineId = getCustomKey(fingerprint);
}
singleAlert.setId(null);
this.firingAlertMap.put(defineId, fingerprint, singleAlert);
}
}
public void putPending(Long defineId, String fingerPrint, SingleAlert alert) {
this.pendingAlertMap.put(String.valueOf(defineId), fingerPrint, alert);
}
public SingleAlert getPending(Long defineId, String fingerPrint) {
return this.pendingAlertMap.get(String.valueOf(defineId), fingerPrint);
}
public void removePending(Long defineId, String fingerPrint) {
this.pendingAlertMap.remove(String.valueOf(defineId), fingerPrint);
}
public void putFiring(Long defineId, String fingerPrint, SingleAlert alert) {
this.firingAlertMap.put(String.valueOf(defineId), fingerPrint, alert);
}
public void putFiring(String fingerPrint, SingleAlert alert) {
this.firingAlertMap.put(getCustomKey(fingerPrint), fingerPrint, alert);
}
public SingleAlert getFiring(Long defineId, String fingerPrint) {
SingleAlert singleAlert = this.firingAlertMap.get(String.valueOf(defineId), fingerPrint);
if (null != singleAlert) {
return singleAlert;
}
return getFiring(fingerPrint);
}
public SingleAlert removeFiring(Long defineId, String fingerPrint) {
SingleAlert singleAlert = this.firingAlertMap.remove(String.valueOf(defineId), fingerPrint);
if (null == singleAlert) {
return this.firingAlertMap.remove(getCustomKey(fingerPrint), fingerPrint);
}
return singleAlert;
}
public SingleAlert getFiring(String fingerPrint) {
return this.firingAlertMap.get(getCustomKey(fingerPrint), fingerPrint);
}
private String getCustomKey(String fingerPrint) {
return CUSTOM_FIRING_ROW_KEY + fingerPrint;
}
public SingleAlert removeFiring(String fingerPrint) {
return this.firingAlertMap.remove(getCustomKey(fingerPrint), fingerPrint);
}
}
@@ -1,130 +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.alert.calculate;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.dao.AlertCollectorDao;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.alert.util.AlertUtil;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.apache.hertzbeat.common.entity.manager.Collector;
import org.apache.hertzbeat.common.support.event.SystemConfigChangeEvent;
import org.apache.hertzbeat.common.util.ResourceBundleUtil;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
import java.util.ResourceBundle;
/**
* handle collector alarm
*/
@Component
@Slf4j
public class CollectorAlertHandler {
private static final String KEY_COLLECTOR_NAME = "collectorName";
private static final String KEY_COLLECTOR_VERSION = "collectorVersion";
private static final String KEY_COLLECTOR_HOST = "collectorHost";
private final AlertCollectorDao alertCollectorDao;
private final AlarmCommonReduce alarmCommonReduce;
private final AlarmCacheManager alarmCacheManager;
private ResourceBundle bundle;
public CollectorAlertHandler(AlarmCommonReduce alarmCommonReduce, AlertCollectorDao alertCollectorDao,
AlarmCacheManager alarmCacheManager) {
this.alarmCommonReduce = alarmCommonReduce;
this.alertCollectorDao = alertCollectorDao;
this.alarmCacheManager = alarmCacheManager;
this.bundle = ResourceBundleUtil.getBundle("alerter");
}
/**
* handle collector online
*
* @param identity collector name
*/
public void online(final String identity) {
Collector collector = alertCollectorDao.findCollectorByName(identity);
if (collector == null) {
return;
}
Map<String, String> fingerPrints = new HashMap<>(8);
fingerPrints.put(KEY_COLLECTOR_NAME, collector.getName());
fingerPrints.put(KEY_COLLECTOR_VERSION, collector.getVersion());
fingerPrints.put(KEY_COLLECTOR_HOST, collector.getIp());
String fingerprint = AlertUtil.calculateFingerprint(fingerPrints);
SingleAlert firingAlert = alarmCacheManager.removeFiring(fingerprint);
if (firingAlert != null) {
firingAlert.setTriggerTimes(1);
firingAlert.setEndAt(System.currentTimeMillis());
firingAlert.setStatus(CommonConstants.ALERT_STATUS_RESOLVED);
firingAlert.setContent(this.bundle.getString("alerter.availability.collector.recover"));
alarmCommonReduce.reduceAndSendAlarm(firingAlert.clone());
}
}
/**
* handle collector offline
*
* @param identity collector name
*/
public void offline(final String identity) {
Collector collector = alertCollectorDao.findCollectorByName(identity);
if (collector == null) {
return;
}
long currentTimeMill = System.currentTimeMillis();
Map<String, String> fingerPrints = new HashMap<>(8);
fingerPrints.put(KEY_COLLECTOR_NAME, collector.getName());
fingerPrints.put(KEY_COLLECTOR_VERSION, collector.getVersion());
fingerPrints.put(KEY_COLLECTOR_HOST, collector.getIp());
String fingerprint = AlertUtil.calculateFingerprint(fingerPrints);
SingleAlert existingAlert = alarmCacheManager.getFiring(fingerprint);
if (existingAlert == null) {
SingleAlert newAlert = SingleAlert.builder()
.labels(fingerPrints)
.annotations(fingerPrints)
.content(this.bundle.getString("alerter.availability.collector.offline"))
.status(CommonConstants.ALERT_STATUS_FIRING)
.triggerTimes(1)
.startAt(currentTimeMill)
.activeAt(currentTimeMill)
.build();
alarmCacheManager.putFiring(fingerprint, newAlert);
alarmCommonReduce.reduceAndSendAlarm(newAlert.clone());
}
}
@EventListener(SystemConfigChangeEvent.class)
public void onSystemConfigChangeEvent(SystemConfigChangeEvent event) {
log.info("calculate alarm receive system config change event: {}.", event.getSource());
this.bundle = ResourceBundleUtil.getBundle("alerter");
}
}
@@ -1,72 +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.alert.calculate;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.jexl3.JexlException;
import org.apache.commons.jexl3.JexlExpression;
import org.apache.hertzbeat.common.util.JexlExpressionRunner;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* JexlExprCalculator is a utility class for evaluating JEXL expressions
*/
@Slf4j
@Component
public class JexlExprCalculator {
/**
* Execute an alert expression
* @param fieldValueMap The field value map for expression evaluation
* @param expr The expression to evaluate
* @param ignoreJexlException Whether to ignore JEXL exceptions
* @return true if the expression matches, false otherwise
*/
public boolean execAlertExpression(Map<String, Object> fieldValueMap, String expr, boolean ignoreJexlException) {
Boolean match;
JexlExpression expression;
try {
expression = JexlExpressionRunner.compile(expr);
} catch (JexlException jexlException) {
log.warn("Alarm Rule: {} Compile Error: {}.", expr, jexlException.getMessage());
throw jexlException;
} catch (Exception e) {
log.error("Alarm Rule: {} Unknown Error: {}.", expr, e.getMessage());
throw e;
}
try {
match = (Boolean) JexlExpressionRunner.evaluate(expression, fieldValueMap);
} catch (JexlException jexlException) {
if (ignoreJexlException) {
log.debug("Alarm Rule: {} Run Error: {}.", expr, jexlException.getMessage());
} else {
log.error("Alarm Rule: {} Run Error: {}.", expr, jexlException.getMessage());
}
throw jexlException;
} catch (Exception e) {
log.error("Alarm Rule: {} Unknown Error: {}.", expr, e.getMessage());
throw e;
}
return match != null && match;
}
}
@@ -0,0 +1,188 @@
/*
* 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 java.util.Arrays;
import java.util.HashMap;
import java.util.Objects;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.alert.service.DataSourceService;
import org.apache.hertzbeat.alert.util.AlertTemplateUtil;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Component;
/**
* Periodic Alert Calculator
*/
@Slf4j
@Component
public class PeriodicAlertCalculator {
private static final String VALUE = "__value__";
private static final String TIMESTAMP = "__timestamp__";
private final DataSourceService dataSourceService;
private final AlarmCommonReduce alarmCommonReduce;
/**
* The alarm in the process is triggered
* key - labels fingerprint
*/
private final Map<String, SingleAlert> pendingAlertMap;
/**
* The not recover alert
* key - labels fingerprint
*/
private final Map<String, SingleAlert> firingAlertMap;
public PeriodicAlertCalculator(DataSourceService dataSourceService, AlarmCommonReduce alarmCommonReduce) {
this.dataSourceService = dataSourceService;
this.alarmCommonReduce = alarmCommonReduce;
this.pendingAlertMap = new ConcurrentHashMap<>(8);
this.firingAlertMap = new ConcurrentHashMap<>(8);
}
public void calculate(AlertDefine rule) {
if (!rule.isEnable() || StringUtils.isEmpty(rule.getExpr())) {
log.error("Periodic rule {} is disabled or expression is empty", rule.getName());
return;
}
long currentTimeMilli = System.currentTimeMillis();
try {
// for prometheus is instant promql query, for db is sql query
// result: [{'value': 100, 'timestamp': 1343554, 'instance': 'node1'},{'value': 200, 'timestamp': 1343555, 'instance': 'node2'}]
// the return result should be matched with threshold
try {
List<Map<String, Object>> results = dataSourceService.calculate(
rule.getDatasource(),
rule.getExpr()
);
// if no match the expr threshold, the results item map {'value': null} should be null and others field keep
// if results has multi list, should trigger multi alert
if (CollectionUtils.isEmpty(results)) {
return;
}
for (Map<String, Object> result : results) {
Map<String, String> fingerPrints = new HashMap<>(8);
// here use the alert name as finger, not care the alert name may be changed
fingerPrints.put(CommonConstants.LABEL_ALERT_NAME, rule.getName());
fingerPrints.putAll(rule.getLabels());
for (Map.Entry<String, Object> entry : result.entrySet()) {
if (entry.getValue() != null && !VALUE.equals(entry.getKey())
&& !TIMESTAMP.equals(entry.getKey())) {
fingerPrints.put(entry.getKey(), entry.getValue().toString());
}
}
if (result.get(VALUE) == null) {
// recovery the alert
handleRecoveredAlert(fingerPrints);
continue;
}
Map<String, Object> fieldValueMap = new HashMap<>(8);
fieldValueMap.putAll(rule.getLabels());
fieldValueMap.put(CommonConstants.LABEL_ALERT_NAME, rule.getName());
for (Map.Entry<String, Object> entry : result.entrySet()) {
if (entry.getValue() != null) {
fieldValueMap.put(entry.getKey(), entry.getValue());
}
}
afterThresholdRuleMatch(currentTimeMilli, fingerPrints, fieldValueMap, rule);
}
} catch (Exception ignored) {
// ignore the query exception eg: no result, timeout, etc
return;
}
} catch (Exception e) {
log.error("Calculate periodic rule {} failed: {}", rule.getName(), e.getMessage());
}
}
private void afterThresholdRuleMatch(long currentTimeMilli, Map<String, String> fingerPrints,
Map<String, Object> fieldValueMap, AlertDefine define) {
String fingerprint = calculateFingerprint(fingerPrints);
SingleAlert existingAlert = pendingAlertMap.get(fingerprint);
Map<String, String> labels = new HashMap<>(8);
fieldValueMap.putAll(define.getLabels());
labels.putAll(fingerPrints);
int requiredTimes = define.getTimes() == null ? 1 : define.getTimes();
if (existingAlert == null) {
// First time triggering alert, create new alert and set to pending status
SingleAlert newAlert = SingleAlert.builder()
.labels(labels)
// todo render var content in annotations
.annotations(define.getAnnotations())
.content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap))
.status(CommonConstants.ALERT_STATUS_PENDING)
.triggerTimes(1)
.startAt(currentTimeMilli)
.activeAt(currentTimeMilli)
.build();
// If required trigger times is 1, set to firing status directly
if (requiredTimes <= 1) {
newAlert.setStatus(CommonConstants.ALERT_STATUS_FIRING);
firingAlertMap.put(fingerprint, newAlert);
alarmCommonReduce.reduceAndSendAlarm(newAlert.clone());
} else {
// Otherwise put into pending queue first
pendingAlertMap.put(fingerprint, newAlert);
}
} else {
// Update existing alert
existingAlert.setTriggerTimes(existingAlert.getTriggerTimes() + 1);
existingAlert.setActiveAt(currentTimeMilli);
// Check if required trigger times reached
if (existingAlert.getStatus().equals(CommonConstants.ALERT_STATUS_PENDING) && existingAlert.getTriggerTimes() >= requiredTimes) {
// Reached trigger times threshold, change to firing status
pendingAlertMap.remove(fingerprint);
existingAlert.setStatus(CommonConstants.ALERT_STATUS_FIRING);
firingAlertMap.put(fingerprint, existingAlert);
alarmCommonReduce.reduceAndSendAlarm(existingAlert.clone());
}
}
}
private void handleRecoveredAlert(Map<String, String> fingerprints) {
String fingerprint = calculateFingerprint(fingerprints);
SingleAlert firingAlert = firingAlertMap.remove(fingerprint);
if (firingAlert != null) {
// todo consider multi times to tig for resolved alert
firingAlert.setTriggerTimes(1);
firingAlert.setEndAt(System.currentTimeMillis());
firingAlert.setStatus(CommonConstants.ALERT_STATUS_RESOLVED);
alarmCommonReduce.reduceAndSendAlarm(firingAlert.clone());
}
pendingAlertMap.remove(fingerprint);
}
private String calculateFingerprint(Map<String, String> fingerPrints) {
List<String> keyList = fingerPrints.keySet().stream().filter(Objects::nonNull).sorted().toList();
List<String> valueList = fingerPrints.values().stream().filter(Objects::nonNull).sorted().toList();
return Arrays.hashCode(keyList.toArray(new String[0])) + "-"
+ Arrays.hashCode(valueList.toArray(new String[0]));
}
}
@@ -15,13 +15,10 @@
* limitations under the License.
*/
package org.apache.hertzbeat.alert.calculate.periodic;
package org.apache.hertzbeat.alert.calculate;
import static org.apache.hertzbeat.common.constants.CommonConstants.LOG_ALERT_THRESHOLD_TYPE_PERIODIC;
import static org.apache.hertzbeat.common.constants.CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_PERIODIC;
import static org.apache.hertzbeat.common.constants.CommonConstants.ALERT_THRESHOLD_TYPE_PERIODIC;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -42,15 +39,13 @@ import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
@Component
public class PeriodicAlertRuleScheduler implements CommandLineRunner {
private final MetricsPeriodicAlertCalculator metricsCalculator;
private final LogPeriodicAlertCalculator logCalculator;
private final PeriodicAlertCalculator calculator;
private final AlertDefineDao alertDefineDao;
private final ScheduledExecutorService scheduledExecutor;
private final Map<Long, ScheduledFuture<?>> scheduledFutures;
public PeriodicAlertRuleScheduler(MetricsPeriodicAlertCalculator metricsCalculator, LogPeriodicAlertCalculator logCalculator, AlertDefineDao alertDefineDao) {
this.metricsCalculator = metricsCalculator;
this.logCalculator = logCalculator;
public PeriodicAlertRuleScheduler(PeriodicAlertCalculator calculator, AlertDefineDao alertDefineDao) {
this.calculator = calculator;
this.alertDefineDao = alertDefineDao;
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
@@ -81,14 +76,9 @@ public class PeriodicAlertRuleScheduler implements CommandLineRunner {
return;
}
cancelSchedule(rule.getId());
if (rule.getType().equals(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC)
|| rule.getType().equals(LOG_ALERT_THRESHOLD_TYPE_PERIODIC)) {
if (rule.getType().equals(ALERT_THRESHOLD_TYPE_PERIODIC)) {
ScheduledFuture<?> future = scheduledExecutor.scheduleAtFixedRate(() -> {
if (rule.getType().equals(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC)) {
metricsCalculator.calculate(rule);
} else if (rule.getType().equals(LOG_ALERT_THRESHOLD_TYPE_PERIODIC)) {
logCalculator.calculate(rule);
}
calculator.calculate(rule);
}, 0, rule.getPeriod(), java.util.concurrent.TimeUnit.SECONDS);
scheduledFutures.put(rule.getId(), future);
}
@@ -97,11 +87,7 @@ public class PeriodicAlertRuleScheduler implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
log.info("Starting periodic alert rule scheduler...");
List<AlertDefine> metricsPeriodicRules = alertDefineDao.findAlertDefinesByTypeAndEnableTrue(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC);
List<AlertDefine> logPeriodicRules = alertDefineDao.findAlertDefinesByTypeAndEnableTrue(LOG_ALERT_THRESHOLD_TYPE_PERIODIC);
List<AlertDefine> periodicRules = new ArrayList<>(metricsPeriodicRules.size() + logPeriodicRules.size());
periodicRules.addAll(metricsPeriodicRules);
periodicRules.addAll(logPeriodicRules);
List<AlertDefine> periodicRules = alertDefineDao.findAlertDefinesByTypeAndEnableTrue(ALERT_THRESHOLD_TYPE_PERIODIC);
for (AlertDefine rule : periodicRules) {
updateSchedule(rule);
}
@@ -15,34 +15,33 @@
* limitations under the License.
*/
package org.apache.hertzbeat.alert.calculate.realtime;
package org.apache.hertzbeat.alert.calculate;
import java.util.Collections;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.jexl3.JexlException;
import org.apache.commons.jexl3.JexlExpression;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.alert.AlerterWorkerPool;
import org.apache.hertzbeat.alert.calculate.AlarmCacheManager;
import org.apache.hertzbeat.alert.calculate.JexlExprCalculator;
import org.apache.hertzbeat.alert.dao.SingleAlertDao;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.alert.service.AlertDefineService;
import org.apache.hertzbeat.alert.util.AlertTemplateUtil;
import org.apache.hertzbeat.alert.util.AlertUtil;
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.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.queue.CommonDataQueue;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.apache.hertzbeat.common.util.JexlExpressionRunner;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
@@ -51,76 +50,60 @@ import org.springframework.util.CollectionUtils;
*/
@Component
@Slf4j
public class MetricsRealTimeAlertCalculator {
public class RealTimeAlertCalculator {
private static final int CALCULATE_THREADS = 3;
private static final String KEY_INSTANCE = "__instance__";
private static final String KEY_INSTANCE_NAME = "__instancename__";
private static final String KEY_INSTANCE_HOST = "__instancehost__";
private static final String KEY_APP = "__app__";
private static final String KEY_METRICS = "__metrics__";
private static final String KEY_PRIORITY = "__priority__";
private static final String KEY_CODE = "__code__";
private static final String KEY_AVAILABLE = "__available__";
private static final String KEY_LABELS = "__labels__";
private static final String UP = "up";
private static final String DOWN = "down";
private static final String KEY_ROW = "__row__";
private static final Pattern APP_PATTERN = Pattern.compile("equals\\(__app__,\"([^\"]+)\"\\)");
private static final Pattern AVAILABLE_PATTERN = Pattern.compile("equals\\(__available__,\"([^\"]+)\"\\)");
private static final Pattern LABEL_PATTERN = Pattern.compile("contains\\(__labels__,\\s*\"([^\"]+)\"\\)");
private static final Pattern INSTANCE_PATTERN = Pattern.compile("equals\\(__instance__,\\s*\"(\\d+)\"\\)");
private static final Pattern INSTANCE_PATTERN = Pattern.compile("equals\\(__instance__,\"(\\d+)\"\\)");
private static final Pattern METRICS_PATTERN = Pattern.compile("equals\\(__metrics__,\"([^\"]+)\"\\)");
/**
* The alarm in the process is triggered
* key - labels fingerprint
*/
private final Map<String, SingleAlert> pendingAlertMap;
/**
* The not recover alert
* key - labels fingerprint
*/
private final Map<String, SingleAlert> firingAlertMap;
private final AlerterWorkerPool workerPool;
private final CommonDataQueue dataQueue;
private final AlertDefineService alertDefineService;
private final AlarmCommonReduce alarmCommonReduce;
private final AlarmCacheManager alarmCacheManager;
private final JexlExprCalculator jexlExprCalculator;
@Autowired
public MetricsRealTimeAlertCalculator(AlerterWorkerPool workerPool, CommonDataQueue dataQueue,
AlertDefineService alertDefineService, SingleAlertDao singleAlertDao,
AlarmCommonReduce alarmCommonReduce, AlarmCacheManager alarmCacheManager,
JexlExprCalculator jexlExprCalculator) {
this(workerPool, dataQueue, alertDefineService, singleAlertDao, alarmCommonReduce, alarmCacheManager, jexlExprCalculator, true);
}
/**
* Constructor for MetricsRealTimeAlertCalculator with a toggle to control whether to start alert calculation threads.
*
* @param workerPool The worker pool used for concurrent alert calculation.
* @param dataQueue The queue from which metric data is pulled and pushed.
* @param alertDefineService The service providing alert definition rules.
* @param singleAlertDao The DAO for fetching persisted alert states from storage.
* @param alarmCommonReduce The component responsible for reducing and sending alerts.
* @param alarmCacheManager The cache manager for managing alert states.
* @param start If true, the alert calculation threads will start automatically;
* set to false to disable thread start (useful for unit testing).
*/
public MetricsRealTimeAlertCalculator(AlerterWorkerPool workerPool, CommonDataQueue dataQueue,
AlertDefineService alertDefineService, SingleAlertDao singleAlertDao,
AlarmCommonReduce alarmCommonReduce, AlarmCacheManager alarmCacheManager,
JexlExprCalculator jexlExprCalculator, boolean start) {
public RealTimeAlertCalculator(AlerterWorkerPool workerPool, CommonDataQueue dataQueue,
AlertDefineService alertDefineService, SingleAlertDao singleAlertDao,
AlarmCommonReduce alarmCommonReduce) {
this.workerPool = workerPool;
this.dataQueue = dataQueue;
this.alarmCommonReduce = alarmCommonReduce;
this.alertDefineService = alertDefineService;
this.alarmCacheManager = alarmCacheManager;
this.jexlExprCalculator = jexlExprCalculator;
if (start) {
startCalculate();
this.pendingAlertMap = new ConcurrentHashMap<>(8);
this.firingAlertMap = new ConcurrentHashMap<>(8);
// Initialize firing stateAlertMap
List<SingleAlert> singleAlerts = singleAlertDao.querySingleAlertsByStatus(CommonConstants.ALERT_STATUS_FIRING);
for (SingleAlert singleAlert : singleAlerts) {
String fingerprint = calculateFingerprint(singleAlert.getLabels());
singleAlert.setId(null);
firingAlertMap.put(fingerprint, singleAlert);
}
startCalculate();
}
/**
* Start the alert calculation threads
*/
public void startCalculate() {
private void startCalculate() {
Runnable runnable = () -> {
while (!Thread.currentThread().isInterrupted()) {
try {
@@ -139,41 +122,28 @@ public class MetricsRealTimeAlertCalculator {
}
}
protected void calculate(CollectRep.MetricsData metricsData) {
private void calculate(CollectRep.MetricsData metricsData) {
long currentTimeMilli = System.currentTimeMillis();
String instance = String.valueOf(metricsData.getId());
String instanceName = metricsData.getInstanceName();
String instanceHost = metricsData.getInstanceHost();
String app = metricsData.getApp();
String metrics = metricsData.getMetrics();
if ((CommonConstants.PROMETHEUS_APP_PREFIX + instanceName).equals(metricsData.getApp())) {
app = CommonConstants.PROMETHEUS;
}
int priority = metricsData.getPriority();
int code = metricsData.getCode().getNumber();
Map<String, String> labels = metricsData.getLabels();
Map<String, String> annotations = metricsData.getAnnotations();
List<AlertDefine> thresholds = this.alertDefineService.getMetricsRealTimeAlertDefines();
// Filter thresholds by app, metrics, labels and instance
thresholds = filterThresholdsByAppAndMetrics(thresholds, app, metrics, labels, instance, priority);
List<AlertDefine> thresholds = this.alertDefineService.getRealTimeAlertDefines();
// Filter thresholds by app, metrics and instance
thresholds = filterThresholdsByAppAndMetrics(thresholds, app, metrics, instance, priority);
if (thresholds.isEmpty()) {
return;
}
Map<String, Object> commonContext = new HashMap<>(8);
commonContext.put(KEY_INSTANCE, instance);
commonContext.put(KEY_INSTANCE_NAME, instanceName);
commonContext.put(KEY_INSTANCE_HOST, instanceHost);
commonContext.put(KEY_APP, app);
commonContext.put(KEY_PRIORITY, priority);
commonContext.put(KEY_CODE, code);
commonContext.put(KEY_METRICS, metrics);
commonContext.put(KEY_LABELS, String.join(",", kvLabelsToKvStringSet(labels)));
if (priority == 0) {
commonContext.put(KEY_AVAILABLE, metricsData.getCode() == CollectRep.Code.SUCCESS ? UP : DOWN);
}
if (labels != null) {
commonContext.putAll(labels);
}
List<CollectRep.Field> fields = metricsData.getFields();
Map<String, Object> fieldValueMap = new HashMap<>(8);
int valueRowCount = metricsData.getValuesCount();
@@ -190,28 +160,21 @@ public class MetricsRealTimeAlertCalculator {
if (StringUtils.isBlank(expr)) {
continue;
}
Long defineId = define.getId();
Map<String, String> commonFingerPrints = new HashMap<>(8);
commonFingerPrints.put(CommonConstants.LABEL_INSTANCE, instance);
// here use the alert name as finger, not care the alert name may be changed
commonFingerPrints.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(define.getId()));
commonFingerPrints.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
commonFingerPrints.put(CommonConstants.LABEL_INSTANCE_NAME, instanceName);
commonFingerPrints.put(CommonConstants.LABEL_INSTANCE_HOST, instanceHost);
commonFingerPrints.putAll(define.getLabels());
if (labels != null) {
commonFingerPrints.putAll(labels);
}
{
// trigger the expr before the metrics data, due the available up down or others
try {
boolean match = jexlExprCalculator.execAlertExpression(fieldValueMap, expr, true);
boolean match = execAlertExpression(fieldValueMap, expr, true);
try {
Map<String, String> fingerPrints = new HashMap<>(8);
fingerPrints.put(CommonConstants.LABEL_INSTANCE, instance);
// here use the alert name as finger, not care the alert name may be changed
fingerPrints.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
fingerPrints.putAll(define.getLabels());
if (match) {
// If the threshold rule matches, the number of times the threshold has been triggered is determined and an alarm is triggered
afterThresholdRuleMatch(defineId, currentTimeMilli, commonFingerPrints, fieldValueMap, define, annotations);
afterThresholdRuleMatch(currentTimeMilli, fingerPrints, fieldValueMap, define);
} else {
handleRecoveredAlert(defineId, commonFingerPrints);
handleRecoveredAlert(fingerPrints);
}
// if this threshold pre compile success, ignore blew
continue;
@@ -229,14 +192,16 @@ public class MetricsRealTimeAlertCalculator {
fieldValueMap.put(KEY_ROW, valueRowCount);
fieldValueMap.putAll(commonContext);
fingerPrints.clear();
fingerPrints.putAll(commonFingerPrints);
fingerPrints.put(CommonConstants.LABEL_INSTANCE, instance);
fingerPrints.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
fingerPrints.putAll(define.getLabels());
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) {
@@ -260,12 +225,12 @@ public class MetricsRealTimeAlertCalculator {
}
}
try {
boolean match = jexlExprCalculator.execAlertExpression(fieldValueMap, expr, false);
boolean match = execAlertExpression(fieldValueMap, expr, false);
try {
if (match) {
afterThresholdRuleMatch(defineId, currentTimeMilli, fingerPrints, fieldValueMap, define, annotations);
afterThresholdRuleMatch(currentTimeMilli, fingerPrints, fieldValueMap, define);
} else {
handleRecoveredAlert(defineId, fingerPrints);
handleRecoveredAlert(fingerPrints);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
@@ -281,12 +246,11 @@ public class MetricsRealTimeAlertCalculator {
* @param thresholds Alert definitions to filter
* @param app Current app name
* @param metrics Current metrics name
* @param labels Current labels
* @param instance Current instance id
* @param priority Current priority
* @param priority Current priority
* @return Filtered alert definitions
*/
public List<AlertDefine> filterThresholdsByAppAndMetrics(List<AlertDefine> thresholds, String app, String metrics, Map<String, String> labels, String instance, int priority) {
private List<AlertDefine> filterThresholdsByAppAndMetrics(List<AlertDefine> thresholds, String app, String metrics, String instance, int priority) {
return thresholds.stream()
.filter(define -> {
if (StringUtils.isBlank(define.getExpr())) {
@@ -299,7 +263,7 @@ public class MetricsRealTimeAlertCalculator {
if (!appMatcher.find() || !app.equals(appMatcher.group(1))) {
return false;
}
// Extract and check available - required
if (priority != 0) {
Matcher availableMatcher = AVAILABLE_PATTERN.matcher(expr);
@@ -316,40 +280,27 @@ public class MetricsRealTimeAlertCalculator {
// Extract and check instance - optional with multiple values
Matcher instanceMatcher = INSTANCE_PATTERN.matcher(expr);
Matcher labelMatcher = LABEL_PATTERN.matcher(expr);
// If no instance and instance labels specified in expr, accept all instances
if (!instanceMatcher.find() && !labelMatcher.find()) {
// If no instance specified in expr, accept all instances
if (!instanceMatcher.find()) {
return true;
}
// Reset matcher to check all instances
instanceMatcher.reset();
labelMatcher.reset();
// If instances specified, current instance must match one of them
while (instanceMatcher.find()) {
if (Objects.equals(instance, instanceMatcher.group(1))) {
return true;
}
}
// If instance labels specified, current instance must match one of them
Set<String> labelKvStringSet = kvLabelsToKvStringSet(labels);
while (labelMatcher.find()) {
String label = labelMatcher.group(1);
if (labelKvStringSet.contains(label)) {
return true;
}
}
return false;
})
.collect(Collectors.toList());
}
/**
* Handle recovered alert
*/
private void handleRecoveredAlert(Long defineId, Map<String, String> fingerprints) {
String fingerprint = AlertUtil.calculateFingerprint(fingerprints);
SingleAlert firingAlert = alarmCacheManager.removeFiring(defineId, fingerprint);
private void handleRecoveredAlert(Map<String, String> fingerprints) {
String fingerprint = calculateFingerprint(fingerprints);
SingleAlert firingAlert = firingAlertMap.remove(fingerprint);
if (firingAlert != null) {
// todo consider multi times to tig for resolved alert
firingAlert.setTriggerTimes(1);
@@ -357,78 +308,88 @@ public class MetricsRealTimeAlertCalculator {
firingAlert.setStatus(CommonConstants.ALERT_STATUS_RESOLVED);
alarmCommonReduce.reduceAndSendAlarm(firingAlert.clone());
}
alarmCacheManager.removePending(defineId, fingerprint);
pendingAlertMap.remove(fingerprint);
}
/**
* Handle alert after threshold rule match
*/
private void afterThresholdRuleMatch(long defineId, long currentTimeMilli, Map<String, String> fingerPrints,
Map<String, Object> fieldValueMap, AlertDefine define,
Map<String, String> annotations) {
// fingerprint for the padding cache
String fingerprint = AlertUtil.calculateFingerprint(fingerPrints);
SingleAlert existingAlert = alarmCacheManager.getPending(defineId, fingerprint);
private void afterThresholdRuleMatch(long currentTimeMilli, Map<String, String> fingerPrints,
Map<String, Object> fieldValueMap, AlertDefine define) {
String fingerprint = calculateFingerprint(fingerPrints);
SingleAlert existingAlert = pendingAlertMap.get(fingerprint);
Map<String, String> labels = new HashMap<>(8);
fieldValueMap.putAll(define.getLabels());
labels.putAll(fingerPrints);
int requiredTimes = define.getTimes() == null ? 1 : define.getTimes();
if (existingAlert == null) {
// First time triggering alert, create new alert and set to pending status
Map<String, String> alertLabels = new HashMap<>(8);
alertLabels.putAll(fingerPrints);
Map<String, String> alertAnnotations = new HashMap<>(8);
if (annotations != null) {
alertAnnotations.putAll(annotations);
}
if (define.getAnnotations() != null) {
alertAnnotations.putAll(define.getAnnotations());
}
// render var content in annotations
for (Map.Entry<String, String> entry : alertAnnotations.entrySet()) {
entry.setValue(AlertTemplateUtil.render(entry.getValue(), fieldValueMap));
}
SingleAlert newAlert = SingleAlert.builder()
.labels(alertLabels)
.annotations(alertAnnotations)
// render var content in content
.labels(labels)
// todo render var content in annotations
.annotations(define.getAnnotations())
.content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap))
.status(CommonConstants.ALERT_STATUS_PENDING)
.triggerTimes(1)
.triggerTimes(1)
.startAt(currentTimeMilli)
.activeAt(currentTimeMilli)
.build();
// If required trigger times is 1, set to firing status directly
if (requiredTimes <= 1) {
newAlert.setStatus(CommonConstants.ALERT_STATUS_FIRING);
alarmCacheManager.putFiring(defineId, fingerprint, newAlert);
firingAlertMap.put(fingerprint, newAlert);
alarmCommonReduce.reduceAndSendAlarm(newAlert.clone());
} else {
// Otherwise put into pending queue first
alarmCacheManager.putPending(define.getId(), fingerprint, newAlert);
pendingAlertMap.put(fingerprint, newAlert);
}
} else {
// Update existing alert
existingAlert.setTriggerTimes(existingAlert.getTriggerTimes() + 1);
existingAlert.setActiveAt(currentTimeMilli);
// Check if required trigger times reached
if (existingAlert.getStatus().equals(CommonConstants.ALERT_STATUS_PENDING) && existingAlert.getTriggerTimes() >= requiredTimes) {
// Reached trigger times threshold, change to firing status
alarmCacheManager.removePending(defineId, fingerprint);
pendingAlertMap.remove(fingerprint);
existingAlert.setStatus(CommonConstants.ALERT_STATUS_FIRING);
alarmCacheManager.putFiring(defineId, fingerprint, existingAlert);
firingAlertMap.put(fingerprint, existingAlert);
alarmCommonReduce.reduceAndSendAlarm(existingAlert.clone());
}
}
}
private Set<String> kvLabelsToKvStringSet(Map<String, String> labels) {
if (labels == null || labels.isEmpty()) {
return Collections.singleton("");
private boolean execAlertExpression(Map<String, Object> fieldValueMap, String expr, boolean ignoreJexlException) {
Boolean match;
JexlExpression expression;
try {
expression = JexlExpressionRunner.compile(expr);
} catch (JexlException jexlException) {
log.warn("Alarm Rule: {} Compile Error: {}.", expr, jexlException.getMessage());
throw jexlException;
} catch (Exception e) {
log.error("Alarm Rule: {} Unknown Error: {}.", expr, e.getMessage());
throw e;
}
return labels.entrySet().stream()
.map(item -> item.getKey() + ":" + item.getValue())
.collect(Collectors.toSet());
try {
match = (Boolean) JexlExpressionRunner.evaluate(expression, fieldValueMap);
} catch (JexlException jexlException) {
if (ignoreJexlException) {
log.debug("Alarm Rule: {} Run Error: {}.", expr, jexlException.getMessage());
} else {
log.error("Alarm Rule: {} Run Error: {}.", expr, jexlException.getMessage());
}
throw jexlException;
} catch (Exception e) {
log.error("Alarm Rule: {} Unknown Error: {}.", expr, e.getMessage());
throw e;
}
return match != null && match;
}
private String calculateFingerprint(Map<String, String> fingerPrints) {
List<String> keyList = fingerPrints.keySet().stream().filter(Objects::nonNull).sorted().toList();
List<String> valueList = fingerPrints.values().stream().filter(Objects::nonNull).sorted().toList();
return Arrays.hashCode(keyList.toArray(new String[0])) + "-"
+ Arrays.hashCode(valueList.toArray(new String[0]));
}
}
@@ -1,232 +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.alert.calculate.periodic;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.alert.service.DataSourceService;
import org.apache.hertzbeat.alert.util.AlertTemplateUtil;
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.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Log Periodic Alert Calculator
*/
@Slf4j
@Component
public class LogPeriodicAlertCalculator {
private static final String ROWS = "__rows__";
private final DataSourceService dataSourceService;
private final AlarmCommonReduce alarmCommonReduce;
public LogPeriodicAlertCalculator(DataSourceService dataSourceService, AlarmCommonReduce alarmCommonReduce) {
this.alarmCommonReduce = alarmCommonReduce;
this.dataSourceService = dataSourceService;
}
public void calculate(AlertDefine define) {
if (!define.isEnable() || StringUtils.isEmpty(define.getExpr())) {
log.error("Log define {} is disabled or expression is empty", define.getName());
return;
}
try {
doCalculate(define);
} catch (Exception e) {
log.error("Calculate periodic define {} failed: {}", define.getName(), e.getMessage());
}
}
private void doCalculate(AlertDefine define) {
try {
// Log-based queries are SQL queries with log-specific expressions
List<Map<String, Object>> results = dataSourceService.query(define.getDatasource(), define.getExpr());
if (CollectionUtils.isEmpty(results)) {
return;
}
afterThresholdRuleMatch(results, define);
} catch (Exception ignored) {
// Ignore the query exception eg: no result, timeout, etc
}
}
String getAlertMode(AlertDefine alertDefine) {
String mode = null;
if (alertDefine.getLabels() != null) {
mode = alertDefine.getLabels().get(CommonConstants.ALERT_MODE_LABEL);
}
if (mode == null || mode.isEmpty()) {
return CommonConstants.ALERT_MODE_GROUP; // Default to group mode if not specified
} else {
return mode;
}
}
/**
* Handle alert after threshold rule match
*/
private void afterThresholdRuleMatch(List<Map<String, Object>> alertContext, AlertDefine define) {
// Determine alert mode from configuration
String alertMode = getAlertMode(define);
long currentTime = System.currentTimeMillis();
switch (alertMode) {
case CommonConstants.ALERT_MODE_INDIVIDUAL:
// Generate individual alerts for each matching log
for (Map<String, Object> context : alertContext) {
generateIndividualAlert(define, context, currentTime);
}
break;
case CommonConstants.ALERT_MODE_GROUP:
// Generate a single alert group for all matching logs
generateGroupAlert(define, alertContext, currentTime);
break;
default:
log.warn("Unknown alert mode for define {}: {}", define.getName(), alertMode);
}
}
private void generateIndividualAlert(AlertDefine define, Map<String, Object> context, long currentTime) {
Map<String, String> alertLabels = new HashMap<>(8);
Map<String, String> commonFingerPrints = createCommonFingerprints(define);
alertLabels.putAll(commonFingerPrints);
addContextToMap(context, alertLabels);
Map<String, Object> fieldValueMap = createFieldValueMap(context, define);
Map<String, String> alertAnnotations = createAlertAnnotations(define, fieldValueMap);
// Create and send group alert
SingleAlert alert = SingleAlert.builder()
.labels(alertLabels)
.annotations(alertAnnotations)
.content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap))
.status(CommonConstants.ALERT_STATUS_FIRING)
.triggerTimes(1)
.startAt(currentTime)
.activeAt(currentTime)
.build();
alarmCommonReduce.reduceAndSendAlarm(alert.clone());
log.debug("Generated individual alert for define: {}", define.getName());
}
private void addContextToMap(Map<String, Object> context, Map<String, String> alertLabels) {
for (Map.Entry<String, Object> entry : context.entrySet()) {
if (entry.getValue() != null) {
alertLabels.put(entry.getKey(), entry.getValue().toString());
}
}
}
private void generateGroupAlert(AlertDefine define, List<Map<String, Object>> alertContext, long currentTime) {
List<SingleAlert> alerts = new ArrayList<>(alertContext.size());
// Create fingerprints for group alert
Map<String, String> commonFingerPrints = createCommonFingerprints(define);
// Add context information to fingerprints
commonFingerPrints.put(ROWS, String.valueOf(alertContext.size()));
commonFingerPrints.put(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_GROUP);
for (Map<String, Object> context : alertContext) {
Map<String, String> alertLabels = new HashMap<>(8);
alertLabels.putAll(commonFingerPrints);
// add the context to commonFingerPrints
addContextToMap(context, alertLabels);
Map<String, Object> fieldValueMap = createFieldValueMap(context, define);
Map<String, String> alertAnnotations = createAlertAnnotations(define, fieldValueMap);
// Create and send group alert
SingleAlert alert = SingleAlert.builder()
.labels(alertLabels)
.annotations(alertAnnotations)
.content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap))
.status(CommonConstants.ALERT_STATUS_FIRING)
.triggerTimes(alertContext.size())
.startAt(currentTime)
.activeAt(currentTime)
.build();
alerts.add(alert.clone());
}
alarmCommonReduce.reduceAndSendAlarmGroup(commonFingerPrints, alerts);
log.debug("Generated group alert for define: {} with {} matching data",
define.getName(), alertContext.size());
}
private Map<String, String> createCommonFingerprints(AlertDefine define) {
Map<String, String> fingerprints = new HashMap<>(8);
fingerprints.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
fingerprints.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(define.getId()));
if (define.getLabels() != null) {
fingerprints.putAll(define.getLabels());
}
return fingerprints;
}
private Map<String, Object> createFieldValueMap(Map<String, Object> context, AlertDefine define) {
Map<String, Object> fieldValueMap = new HashMap<>(8);
for (Map.Entry<String, Object> entry : context.entrySet()) {
if (entry.getValue() != null) {
fieldValueMap.put(entry.getKey(), entry.getValue().toString());
}
}
if (define.getLabels() != null) {
fieldValueMap.putAll(define.getLabels());
}
return fieldValueMap;
}
private Map<String, String> createAlertAnnotations(AlertDefine define, Map<String, Object> fieldValueMap) {
Map<String, String> annotations = new HashMap<>(8);
if (define.getAnnotations() != null) {
for (Map.Entry<String, String> entry : define.getAnnotations().entrySet()) {
annotations.put(entry.getKey(),
AlertTemplateUtil.render(entry.getValue(), fieldValueMap));
}
}
return annotations;
}
}
@@ -1,186 +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.alert.calculate.periodic;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.alert.calculate.AlarmCacheManager;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.alert.service.DataSourceService;
import org.apache.hertzbeat.alert.util.AlertTemplateUtil;
import org.apache.hertzbeat.alert.util.AlertUtil;
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.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Metrics Periodic Alert Calculator
*/
@Slf4j
@Component
public class MetricsPeriodicAlertCalculator {
private static final String VALUE = "__value__";
private static final String TIMESTAMP = "__timestamp__";
private final DataSourceService dataSourceService;
private final AlarmCommonReduce alarmCommonReduce;
private final AlarmCacheManager alarmCacheManager;
public MetricsPeriodicAlertCalculator(DataSourceService dataSourceService, AlarmCommonReduce alarmCommonReduce,
AlarmCacheManager alarmCacheManager) {
this.alarmCommonReduce = alarmCommonReduce;
this.alarmCacheManager = alarmCacheManager;
this.dataSourceService = dataSourceService;
}
/**
* Calculate alerts for the given alert definition
* @param define The alert definition to calculate
*/
public void calculate(AlertDefine define) {
if (!define.isEnable() || StringUtils.isEmpty(define.getExpr())) {
log.error("Periodic define {} is disabled or expression is empty", define.getName());
return;
}
long currentTimeMilli = System.currentTimeMillis();
try {
doCalculate(define, currentTimeMilli);
} catch (Exception e) {
log.error("Calculate periodic define {} failed: {}", define.getName(), e.getMessage());
}
}
private void doCalculate(AlertDefine define, long currentTimeMilli) {
try {
List<Map<String, Object>> results = dataSourceService.calculate(
define.getDatasource(),
define.getExpr()
);
// If no match the expr threshold, the results item map {'value': null} should be null and others field keep
// If results has multi list, should trigger multi alert
if (CollectionUtils.isEmpty(results)) {
return;
}
for (Map<String, Object> result : results) {
Map<String, String> fingerPrints = new HashMap<>(8);
// Here use the alert name as finger, not care the alert name may be changed
fingerPrints.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(define.getId()));
fingerPrints.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
fingerPrints.putAll(define.getLabels());
for (Map.Entry<String, Object> entry : result.entrySet()) {
if (entry.getValue() != null && !VALUE.equals(entry.getKey())
&& !TIMESTAMP.equals(entry.getKey())) {
fingerPrints.put(entry.getKey(), entry.getValue().toString());
}
}
if (result.get(VALUE) == null) {
// Recovery the alert
handleRecoveredAlert(define.getId(), fingerPrints);
continue;
}
Map<String, Object> fieldValueMap = new HashMap<>(8);
fieldValueMap.putAll(define.getLabels());
fieldValueMap.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
for (Map.Entry<String, Object> entry : result.entrySet()) {
if (entry.getValue() != null) {
fieldValueMap.put(entry.getKey(), entry.getValue());
}
}
afterThresholdRuleMatch(currentTimeMilli, fingerPrints, fieldValueMap, define);
}
} catch (Exception ignored) {
// Ignore the query exception eg: no result, timeout, etc
}
}
/**
* Handle alert after threshold rule match
*/
private void afterThresholdRuleMatch(long currentTimeMilli, Map<String, String> fingerPrints,
Map<String, Object> fieldValueMap, AlertDefine define) {
Long defineId = define.getId();
String fingerprint = AlertUtil.calculateFingerprint(fingerPrints);
SingleAlert existingAlert = alarmCacheManager.getPending(defineId, fingerprint);
Map<String, String> labels = new HashMap<>(8);
fieldValueMap.putAll(define.getLabels());
labels.putAll(fingerPrints);
int requiredTimes = define.getTimes() == null ? 1 : define.getTimes();
if (existingAlert == null) {
// First time triggering alert, create new alert and set to pending status
SingleAlert newAlert = SingleAlert.builder()
.labels(labels)
.annotations(define.getAnnotations())
.content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap))
.status(CommonConstants.ALERT_STATUS_PENDING)
.triggerTimes(1)
.startAt(currentTimeMilli)
.activeAt(currentTimeMilli)
.build();
// If required trigger times is 1, set to firing status directly
if (requiredTimes <= 1) {
newAlert.setStatus(CommonConstants.ALERT_STATUS_FIRING);
alarmCacheManager.putFiring(defineId, fingerprint, newAlert);
alarmCommonReduce.reduceAndSendAlarm(newAlert.clone());
} else {
// Otherwise put into pending queue first
alarmCacheManager.putPending(defineId, fingerprint, newAlert);
}
} else {
// Update existing alert
existingAlert.setTriggerTimes(existingAlert.getTriggerTimes() + 1);
existingAlert.setActiveAt(currentTimeMilli);
// Check if required trigger times reached
if (existingAlert.getStatus().equals(CommonConstants.ALERT_STATUS_PENDING) && existingAlert.getTriggerTimes() >= requiredTimes) {
// Reached trigger times threshold, change to firing status
alarmCacheManager.removePending(defineId, fingerprint);
existingAlert.setStatus(CommonConstants.ALERT_STATUS_FIRING);
alarmCacheManager.putFiring(defineId, fingerprint, existingAlert);
alarmCommonReduce.reduceAndSendAlarm(existingAlert.clone());
}
}
}
/**
* Handle recovered alert
*/
private void handleRecoveredAlert(Long defineId, Map<String, String> fingerprints) {
String fingerprint = AlertUtil.calculateFingerprint(fingerprints);
SingleAlert firingAlert = alarmCacheManager.removeFiring(defineId, fingerprint);
if (firingAlert != null) {
firingAlert.setTriggerTimes(1);
firingAlert.setEndAt(System.currentTimeMillis());
firingAlert.setStatus(CommonConstants.ALERT_STATUS_RESOLVED);
alarmCommonReduce.reduceAndSendAlarm(firingAlert.clone());
}
alarmCacheManager.removePending(defineId, fingerprint);
}
}
@@ -1,140 +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.alert.calculate.realtime;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.calculate.realtime.window.LogWorker;
import org.apache.hertzbeat.alert.calculate.realtime.window.TimeService;
import org.apache.hertzbeat.common.entity.log.LogEntry;
import org.apache.hertzbeat.common.queue.CommonDataQueue;
import org.springframework.stereotype.Component;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* WindowedLogRealTimeAlertCalculator - Single entry point for log stream processing
* Responsible for:
* 1. Reading from original log stream
* 2. Extracting event timestamps
* 3. Maintaining maxTimestamp for watermark generation
* 4. Distributing logs to workers
*/
@Component
@Slf4j
public class WindowedLogRealTimeAlertCalculator implements Runnable {
private static final int CALCULATE_THREADS = 3;
private final CommonDataQueue dataQueue;
private final TimeService timeService;
private ThreadPoolExecutor dispatcherExecutor;
private final LogWorker logWorker;
public WindowedLogRealTimeAlertCalculator(CommonDataQueue dataQueue, TimeService timeService, LogWorker logWorker) {
this.dataQueue = dataQueue;
this.timeService = timeService;
this.logWorker = logWorker;
}
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
LogEntry logEntry = dataQueue.pollLogEntry();
if (logEntry != null) {
processLogEntry(logEntry);
dataQueue.sendLogEntryToStorage(logEntry);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (Exception e) {
log.error("Error in log dispatch loop: {}", e.getMessage(), e);
}
}
}
private void processLogEntry(LogEntry logEntry) {
// Extract event timestamp
long eventTimestamp = extractEventTimestamp(logEntry);
// Check if timestamp is within reasonable range
if (!timeService.isValidTimestamp(eventTimestamp)) {
log.warn("Dropping log with invalid timestamp: {}", eventTimestamp);
return;
}
// Check if this is late data based on current watermark
if (timeService.isLateData(eventTimestamp)) {
log.warn("Dropping late data, timestamp: {}, watermark: {}",
eventTimestamp, timeService.getCurrentWatermark());
return;
}
// Update max timestamp (only validated timestamps can update watermark)
timeService.updateMaxTimestamp(eventTimestamp);
logWorker.reduceAndSendLogTask(logEntry);
}
private long extractEventTimestamp(LogEntry logEntry) {
if (logEntry.getTimeUnixNano() != null && logEntry.getTimeUnixNano() != 0) {
return logEntry.getTimeUnixNano() / 1_000_000; // Convert to milliseconds
}
if (logEntry.getObservedTimeUnixNano() != null && logEntry.getObservedTimeUnixNano() != 0) {
return logEntry.getObservedTimeUnixNano() / 1_000_000; // Convert to milliseconds
}
return System.currentTimeMillis();
}
@PostConstruct
public void start() {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("Alerter workerExecutor has uncaughtException.");
log.error(throwable.getMessage(), throwable);
})
.setDaemon(true)
.setNameFormat("log-dispatcher-%d")
.build();
// Create dispatcher thread executor
this.dispatcherExecutor = new ThreadPoolExecutor(
CALCULATE_THREADS,
CALCULATE_THREADS,
10,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
threadFactory,
new ThreadPoolExecutor.AbortPolicy()
);
for (int i = 0; i < CALCULATE_THREADS; i++) {
dispatcherExecutor.execute(this);
}
}
@PreDestroy
public void stop() {
if (dispatcherExecutor != null) {
dispatcherExecutor.shutdownNow();
}
}
}
@@ -1,317 +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.alert.calculate.realtime.window;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.springframework.stereotype.Component;
import org.apache.hertzbeat.alert.util.AlertTemplateUtil;
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.apache.hertzbeat.common.entity.log.LogEntry;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* Alarm Evaluator - Final alarm logic trigger
* Responsible for:
* 1. Receiving closed window data from WindowAggregator
* 2. Applying alert logic (count threshold, alert mode)
* 3. Generating individual alerts or alert groups
* 4. Sending alerts to AlarmCommonReduce
*/
@Component
@Slf4j
public class AlarmEvaluator {
private static final String WINDOW_START_TIME = "window_start_time";
private static final String WINDOW_END_TIME = "window_end_time";
private static final String MATCHING_LOGS_COUNT = "matching_logs_count";
private final AlarmCommonReduce alarmCommonReduce;
private ThreadPoolExecutor workerExecutor;
public AlarmEvaluator(AlarmCommonReduce alarmCommonReduce) {
this.alarmCommonReduce = alarmCommonReduce;
initAlarmEvaluator();
}
public void initAlarmEvaluator() {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("alerter-reduce-worker has uncaughtException.");
log.error(throwable.getMessage(), throwable);
})
.setDaemon(true)
.setNameFormat("alerter-reduce-worker-%d")
.build();
workerExecutor = new ThreadPoolExecutor(2,
10,
10,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
threadFactory,
new ThreadPoolExecutor.AbortPolicy());
}
public void sendAndProcessWindowData(WindowAggregator.WindowData windowData) {
workerExecutor.execute(processWindowData(windowData));
}
private Runnable processWindowData(WindowAggregator.WindowData windowData) {
return () -> {
AlertDefine alertDefine = windowData.getAlertDefine();
List<MatchingLogEvent> matchingLogs = windowData.getMatchingLogs();
if (matchingLogs.isEmpty()) {
return;
}
// Check if count threshold is met
int requiredTimes = alertDefine.getTimes() != null ? alertDefine.getTimes() : 1;
if (matchingLogs.size() < requiredTimes) {
log.debug("Window {} has {} matching logs, but requires {} times",
windowData.getWindowKey(), matchingLogs.size(), requiredTimes);
return;
}
// Determine alert mode from configuration
String alertMode = getAlertMode(alertDefine);
long currentTime = System.currentTimeMillis();
switch (alertMode) {
case CommonConstants.ALERT_MODE_INDIVIDUAL:
// Generate individual alerts for each matching log
for (MatchingLogEvent matchingLog : matchingLogs) {
generateIndividualAlert(matchingLog, currentTime);
}
break;
case CommonConstants.ALERT_MODE_GROUP:
// Generate a single alert group for all matching logs
generateGroupAlert(windowData, matchingLogs, currentTime);
break;
default:
log.warn("Unknown alert mode for define {}: {}", alertDefine.getName(), alertMode);
}
};
}
private void generateIndividualAlert(MatchingLogEvent matchingLog, long currentTime) {
AlertDefine define = matchingLog.getAlertDefine();
LogEntry logEntry = matchingLog.getLogEntry();
Map<String, String> alertLabels = new HashMap<>(8);
// Create fingerprints for group alert
Map<String, String> commonFingerPrints = createCommonFingerprints(define);
alertLabels.putAll(commonFingerPrints);
// add the log data to commonFingerPrints
addLogEntryToMap(logEntry, alertLabels);
Map<String, Object> fieldValueMap = createFieldValueMap(logEntry, define);
Map<String, String> alertAnnotations = createAlertAnnotations(define, fieldValueMap);
// Create and send group alert
SingleAlert alert = SingleAlert.builder()
.labels(alertLabels)
.annotations(alertAnnotations)
.content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap))
.status(CommonConstants.ALERT_STATUS_FIRING)
.triggerTimes(1)
.startAt(currentTime)
.activeAt(currentTime)
.build();
alarmCommonReduce.reduceAndSendAlarm(alert.clone());
log.debug("Generated individual alert for define: {}", define.getName());
}
private void generateGroupAlert(WindowAggregator.WindowData windowData,
List<MatchingLogEvent> matchingLogs, long currentTime) {
List<SingleAlert> alerts = new ArrayList<>(matchingLogs.size());
AlertDefine define = windowData.getAlertDefine();
// Create fingerprints for group alert
Map<String, String> commonFingerPrints = createCommonFingerprints(define);
// Add window information to fingerprints
commonFingerPrints.put(WINDOW_START_TIME, String.valueOf(windowData.getStartTime()));
commonFingerPrints.put(WINDOW_END_TIME, String.valueOf(windowData.getEndTime()));
commonFingerPrints.put(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_GROUP);
commonFingerPrints.put(MATCHING_LOGS_COUNT, String.valueOf(matchingLogs.size()));
for (MatchingLogEvent event: matchingLogs) {
LogEntry logEntry = event.getLogEntry();
Map<String, String> alertLabels = new HashMap<>(8);
alertLabels.putAll(commonFingerPrints);
// add the log data to commonFingerPrints
addLogEntryToMap(logEntry, alertLabels);
Map<String, Object> fieldValueMap = createFieldValueMap(logEntry, define);
Map<String, String> alertAnnotations = createAlertAnnotations(define, fieldValueMap);
// Create and send group alert
SingleAlert alert = SingleAlert.builder()
.labels(alertLabels)
.annotations(alertAnnotations)
.content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap))
.status(CommonConstants.ALERT_STATUS_FIRING)
.triggerTimes(matchingLogs.size())
.startAt(currentTime)
.activeAt(currentTime)
.build();
alerts.add(alert.clone());
}
alarmCommonReduce.reduceAndSendAlarmGroup(commonFingerPrints, alerts);
log.debug("Generated group alert for define: {} with {} matching logs",
define.getName(), matchingLogs.size());
}
private String getAlertMode(AlertDefine alertDefine) {
String mode = null;
if (alertDefine.getLabels() != null) {
mode = alertDefine.getLabels().get(CommonConstants.ALERT_MODE_LABEL);
}
if (mode == null || mode.isEmpty()) {
return CommonConstants.ALERT_MODE_GROUP; // Default to group mode if not specified
} else {
return mode;
}
}
private Map<String, String> createCommonFingerprints(AlertDefine define) {
Map<String, String> fingerprints = new HashMap<>(8);
fingerprints.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
fingerprints.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(define.getId()));
if (define.getLabels() != null) {
fingerprints.putAll(define.getLabels());
}
return fingerprints;
}
private Map<String, Object> createFieldValueMap(LogEntry logEntry, AlertDefine define) {
Map<String, Object> fieldValueMap = new HashMap<>(8);
fieldValueMap.put("log", logEntry);
if (define.getLabels() != null) {
fieldValueMap.putAll(define.getLabels());
}
return fieldValueMap;
}
private Map<String, String> createAlertAnnotations(AlertDefine define, Map<String, Object> fieldValueMap) {
Map<String, String> annotations = new HashMap<>(8);
if (define.getAnnotations() != null) {
for (Map.Entry<String, String> entry : define.getAnnotations().entrySet()) {
annotations.put(entry.getKey(),
AlertTemplateUtil.render(entry.getValue(), fieldValueMap));
}
}
return annotations;
}
/**
* Add the content from LogEntry object (except timestamp) to commonFingerPrints
*
* @param logEntry log entry object
* @param context context
*/
private void addLogEntryToMap(LogEntry logEntry, Map<String, String> context) {
// Add basic fields
if (logEntry.getSeverityNumber() != null) {
context.put("severityNumber", String.valueOf(logEntry.getSeverityNumber()));
}
if (logEntry.getSeverityText() != null) {
context.put("severityText", logEntry.getSeverityText());
}
if (logEntry.getBody() != null) {
context.put("body", String.valueOf(logEntry.getBody()));
}
if (logEntry.getDroppedAttributesCount() != null) {
context.put("droppedAttributesCount", String.valueOf(logEntry.getDroppedAttributesCount()));
}
if (logEntry.getTraceId() != null) {
context.put("traceId", logEntry.getTraceId());
}
if (logEntry.getSpanId() != null) {
context.put("spanId", logEntry.getSpanId());
}
if (logEntry.getTraceFlags() != null) {
context.put("traceFlags", String.valueOf(logEntry.getTraceFlags()));
}
// Add attributes
if (logEntry.getAttributes() != null && !logEntry.getAttributes().isEmpty()) {
for (Map.Entry<String, Object> entry : logEntry.getAttributes().entrySet()) {
if (entry.getValue() != null) {
context.put("attr_" + entry.getKey(), String.valueOf(entry.getValue()));
}
}
}
// Add resource
if (logEntry.getResource() != null && !logEntry.getResource().isEmpty()) {
for (Map.Entry<String, Object> entry : logEntry.getResource().entrySet()) {
if (entry.getValue() != null) {
context.put("resource_" + entry.getKey(), String.valueOf(entry.getValue()));
}
}
}
// Add instrumentationScope
if (logEntry.getInstrumentationScope() != null) {
LogEntry.InstrumentationScope scope = logEntry.getInstrumentationScope();
if (scope.getName() != null) {
context.put("scope_name", scope.getName());
}
if (scope.getVersion() != null) {
context.put("scope_version", scope.getVersion());
}
if (scope.getDroppedAttributesCount() != null) {
context.put("scope_droppedAttributesCount", String.valueOf(scope.getDroppedAttributesCount()));
}
if (scope.getAttributes() != null && !scope.getAttributes().isEmpty()) {
for (Map.Entry<String, Object> entry : scope.getAttributes().entrySet()) {
if (entry.getValue() != null) {
context.put("scope_attr_" + entry.getKey(), String.valueOf(entry.getValue()));
}
}
}
}
}
}
@@ -1,123 +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.alert.calculate.realtime.window;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.AlerterWorkerPool;
import org.apache.hertzbeat.alert.calculate.JexlExprCalculator;
import org.apache.hertzbeat.alert.service.AlertDefineService;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.entity.log.LogEntry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* LogWorker Responsible for:
* 1. Receiving logs from WindowedLogRealTimeAlertCalculator
* 2. Evaluating alert expressions against logs
* 3. Sending matching logs to WindowAggregator
*/
@Slf4j
@Component
public class LogWorker {
private static final String LOG_PREFIX = "log";
private final AlertDefineService alertDefineService;
private final JexlExprCalculator jexlExprCalculator;
private final WindowAggregator windowAggregator;
private final AlerterWorkerPool workerPool;
@Autowired
public LogWorker(AlertDefineService alertDefineService,
JexlExprCalculator jexlExprCalculator, WindowAggregator windowAggregator,
AlerterWorkerPool workerPool) {
this.alertDefineService = alertDefineService;
this.jexlExprCalculator = jexlExprCalculator;
this.windowAggregator = windowAggregator;
this.workerPool = workerPool;
}
public void reduceAndSendLogTask(LogEntry logEntry) {
workerPool.executeLogJob(reduceLogTask(logEntry));
}
public Runnable reduceLogTask(LogEntry logEntry) {
return () -> {
try {
processLogEntry(logEntry);
} catch (Exception e) {
log.error("Error processing log entry in worker: {}", e.getMessage(), e);
}
};
}
private void processLogEntry(LogEntry logEntry) {
// Get all log alert definitions
List<AlertDefine> alertDefines = alertDefineService.getLogRealTimeAlertDefines();
// Create context for expression evaluation
Map<String, Object> context = new HashMap<>(8);
context.put(LOG_PREFIX, logEntry);
// Process each alert definition
for (AlertDefine define : alertDefines) {
if (define.getExpr() == null || define.getExpr().trim().isEmpty()) {
continue;
}
try {
// Evaluate alert expression
boolean match = jexlExprCalculator.execAlertExpression(context, define.getExpr(), false);
if (match) {
// Create matching log event and send to WindowAggregator
MatchingLogEvent event = createMatchingLogEvent(logEntry, define);
windowAggregator.addMatchingLog(event);
}
} catch (Exception e) {
log.warn("Error evaluating expression for alert define {}: {}", define.getName(), e.getMessage());
}
}
}
private MatchingLogEvent createMatchingLogEvent(LogEntry logEntry, AlertDefine define) {
long eventTimestamp = extractEventTimestamp(logEntry);
return MatchingLogEvent.builder()
.logEntry(logEntry)
.alertDefine(define)
.eventTimestamp(eventTimestamp)
.workerTimestamp(System.currentTimeMillis())
.build();
}
private long extractEventTimestamp(LogEntry logEntry) {
if (logEntry.getTimeUnixNano() != null && logEntry.getTimeUnixNano() != 0) {
return logEntry.getTimeUnixNano() / 1_000_000; // Convert to milliseconds
}
if (logEntry.getObservedTimeUnixNano() != null && logEntry.getObservedTimeUnixNano() != 0) {
return logEntry.getObservedTimeUnixNano() / 1_000_000; // Convert to milliseconds
}
return System.currentTimeMillis();
}
}
@@ -1,40 +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.alert.calculate.realtime.window;
import lombok.Builder;
import lombok.Data;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.entity.log.LogEntry;
/**
* Represents a log entry that matched an alert expression
* Used for communication between LogWorker and WindowAggregator
*/
@Data
@Builder
public class MatchingLogEvent {
private LogEntry logEntry;
private AlertDefine alertDefine;
private long eventTimestamp;
private long workerTimestamp;
}
@@ -1,213 +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.alert.calculate.realtime.window;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
/**
* Watermark Generator responsible for:
* 1. Receiving maxTimestamp updates from WindowedLogRealTimeAlertCalculator
* 2. Calculating watermarks based on configurable delay
* 3. Broadcasting watermarks to all subscribers (WindowAggregator)
*/
@Component
@Slf4j
public class TimeService {
static final long DEFAULT_WATERMARK_DELAY_MS = 30_000; // 30 seconds
private static final long WATERMARK_BROADCAST_INTERVAL_MS = 5_000; // 5 seconds
// Define acceptable timestamp range to filter abnormal timestamps
private static final long MAX_FUTURE_TIME_MS = 60_000; // Allow 1 minute future time
private static final long MAX_PAST_TIME_MS = 24 * 60 * 60 * 1000; // Allow 24 hours past time
private final AtomicLong maxTimestamp = new AtomicLong(0);
private final AtomicLong currentWatermark = new AtomicLong(0);
private final CopyOnWriteArrayList<WatermarkListener> listeners = new CopyOnWriteArrayList<>();
private ScheduledExecutorService scheduler;
public TimeService(List<WatermarkListener> initialListeners) {
listeners.addAll(initialListeners);
}
@PostConstruct
public void start() {
// Create internal scheduled executor
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("TimeService scheduler has uncaughtException.");
log.error(throwable.getMessage(), throwable);
})
.setDaemon(true)
.setNameFormat("timeservice-scheduler-%d")
.build();
this.scheduler = Executors.newSingleThreadScheduledExecutor(threadFactory);
// Start watermark broadcast scheduler
scheduler.scheduleAtFixedRate(
this::broadcastWatermark,
0,
WATERMARK_BROADCAST_INTERVAL_MS,
TimeUnit.MILLISECONDS
);
log.info("TimeService started with watermark delay: {}ms", DEFAULT_WATERMARK_DELAY_MS);
}
@PreDestroy
public void stop() {
if (scheduler != null && !scheduler.isShutdown()) {
log.info("Shutting down TimeService scheduler...");
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(10, TimeUnit.SECONDS)) {
log.warn("TimeService scheduler did not terminate within 10 seconds, forcing shutdown");
scheduler.shutdownNow();
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
log.error("TimeService scheduler did not terminate");
}
}
} catch (InterruptedException e) {
log.warn("Interrupted while waiting for TimeService scheduler to terminate");
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
}
log.info("TimeService stopped");
}
/**
* Check if timestamp is within acceptable range
*/
public boolean isValidTimestamp(long timestamp) {
long currentTime = System.currentTimeMillis();
return timestamp >= (currentTime - MAX_PAST_TIME_MS) && timestamp <= (currentTime + MAX_FUTURE_TIME_MS);
}
/**
* Check if data is late based on current watermark
*/
public boolean isLateData(long timestamp) {
return timestamp < getCurrentWatermark();
}
/**
* Update max timestamp from WindowedLogRealTimeAlertCalculator
*/
public void updateMaxTimestamp(long timestamp) {
maxTimestamp.getAndUpdate(current -> Math.max(current, timestamp));
}
/**
* Add watermark listener
*/
public void addWatermarkListener(WatermarkListener listener) {
listeners.add(listener);
}
/**
* Remove watermark listener
*/
public void removeWatermarkListener(WatermarkListener listener) {
listeners.remove(listener);
}
/**
* Get current watermark
*/
public long getCurrentWatermark() {
return currentWatermark.get();
}
/**
* Get max timestamp
*/
public long getMaxTimestamp() {
return maxTimestamp.get();
}
/**
* Calculate and broadcast watermark
*/
private void broadcastWatermark() {
try {
long maxTs = maxTimestamp.get();
if (maxTs <= 0) {
return;
}
// Calculate watermark: maxTimestamp - delay
long newWatermark = maxTs - DEFAULT_WATERMARK_DELAY_MS;
long currentWm = currentWatermark.get();
// Only advance watermark (monotonic property)
if (newWatermark <= currentWm) {
return;
}
if (currentWatermark.compareAndSet(currentWm, newWatermark)) {
// Broadcast to all listeners
Watermark watermark = new Watermark(newWatermark);
for (WatermarkListener listener : listeners) {
try {
listener.onWatermark(watermark);
} catch (Exception e) {
log.error("Error notifying watermark listener: {}", e.getMessage(), e);
}
}
log.debug("Broadcast watermark: {} (maxTimestamp: {})", newWatermark, maxTs);
}
} catch (Exception e) {
log.error("Error in watermark broadcast: {}", e.getMessage(), e);
}
}
/**
* Watermark data class
*/
@Data
@AllArgsConstructor
@Getter
public static class Watermark {
private final long timestamp;
}
/**
* Interface for watermark listeners
*/
public interface WatermarkListener {
void onWatermark(Watermark watermark);
}
}
@@ -1,251 +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.alert.calculate.realtime.window;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
/**
* Window Aggregator Responsible for:
* 1. Managing window data structure for all active windows
* 2. Receiving matching logs from Workers
* 3. Receiving watermarks from TimeService
* 4. Sending closed windows to AlarmEvaluator
*/
@Component
@Slf4j
public class WindowAggregator implements TimeService.WatermarkListener, Runnable {
private static final long DEFAULT_WINDOW_SIZE_MS = 1 * 60 * 1000; // 1 minutes
private final AlarmEvaluator alarmEvaluator;
private final BlockingQueue<MatchingLogEvent> eventQueue = new LinkedBlockingQueue<>();
private final Map<WindowKey, WindowData> activeWindows = new HashMap<>();
private final Object windowLock = new Object();
private ExecutorService aggregatorExecutor;
public WindowAggregator(AlarmEvaluator alarmEvaluator) {
this.alarmEvaluator = alarmEvaluator;
}
public void addMatchingLog(MatchingLogEvent event) {
try {
eventQueue.put(event);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("Interrupted while adding matching log to aggregator");
}
}
@Override
public void onWatermark(TimeService.Watermark watermark) {
List<WindowData> closedWindows;
synchronized (windowLock) {
closedWindows = new ArrayList<>();
// Find windows that should be closed based on watermark
Iterator<Map.Entry<WindowKey, WindowData>> iterator = activeWindows.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<WindowKey, WindowData> entry = iterator.next();
WindowData windowData = entry.getValue();
// Close window if its end time <= watermark timestamp
if (windowData.getEndTime() <= watermark.getTimestamp()) {
closedWindows.add(windowData);
iterator.remove();
log.debug("Closing window: {} with {} matching logs",
entry.getKey(), windowData.getMatchingLogs().size());
}
}
}
for (WindowData windowData : closedWindows) {
alarmEvaluator.sendAndProcessWindowData(windowData);
}
}
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
MatchingLogEvent event = eventQueue.take();
processMatchingLogEvent(event);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (Exception e) {
log.error("Error processing matching log event: {}", e.getMessage(), e);
}
}
}
private void processMatchingLogEvent(MatchingLogEvent event) {
// Determine window size from alert define (if specified) or use default
long windowSizeMs = getWindowSize(event.getAlertDefine());
// Calculate window boundaries
long eventTime = event.getEventTimestamp();
long windowStart = (eventTime / windowSizeMs) * windowSizeMs;
long windowEnd = windowStart + windowSizeMs;
// Create window key
WindowKey windowKey = new WindowKey(
event.getAlertDefine().getId(),
windowStart,
windowEnd
);
synchronized (windowLock) {
// Get or create window data
WindowData windowData = activeWindows.computeIfAbsent(windowKey,
key -> new WindowData(key, event.getAlertDefine()));
// Add matching log to window
windowData.addMatchingLog(event);
log.debug("Added matching log to window: {} (total logs: {})",
windowKey, windowData.getMatchingLogs().size());
}
}
private long getWindowSize(AlertDefine alertDefine) {
// Check if alert define has custom window size configuration
if (alertDefine.getPeriod() != null) {
return alertDefine.getPeriod() * 1000; // Convert seconds to milliseconds
}
log.info("Using default window size of {} ms for alert define: {}",
DEFAULT_WINDOW_SIZE_MS, alertDefine.getName());
return DEFAULT_WINDOW_SIZE_MS;
}
@PostConstruct
public void start() {
// Create internal executor
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("WindowAggregator executor has uncaughtException.");
log.error(throwable.getMessage(), throwable);
})
.setDaemon(true)
.setNameFormat("window-aggregator-%d")
.build();
aggregatorExecutor = Executors.newSingleThreadExecutor(threadFactory);
// Submit aggregation task
aggregatorExecutor.submit(this);
log.info("WindowAggregator started");
}
@PreDestroy
public void stop() {
if (aggregatorExecutor != null && !aggregatorExecutor.isShutdown()) {
log.info("Shutting down WindowAggregator executor...");
aggregatorExecutor.shutdown();
try {
if (!aggregatorExecutor.awaitTermination(10, TimeUnit.SECONDS)) {
log.warn("WindowAggregator executor did not terminate within 10 seconds, forcing shutdown");
aggregatorExecutor.shutdownNow();
if (!aggregatorExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
log.error("WindowAggregator executor did not terminate");
}
}
} catch (InterruptedException e) {
log.warn("Interrupted while waiting for WindowAggregator executor to terminate");
aggregatorExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
}
log.info("WindowAggregator stopped");
}
/**
* Window key for identifying unique windows
*/
@Data
@Getter
@AllArgsConstructor
public static class WindowKey {
private final long alertDefineId;
private final long startTime;
private final long endTime;
}
/**
* Window data container
*/
public static class WindowData {
@Getter
private final WindowKey windowKey;
@Getter
private final AlertDefine alertDefine;
private final List<MatchingLogEvent> matchingLogs = new ArrayList<>();
@Getter
private final long createdTime;
public WindowData(WindowKey windowKey, AlertDefine alertDefine) {
this.windowKey = windowKey;
this.alertDefine = alertDefine;
this.createdTime = System.currentTimeMillis();
}
public void addMatchingLog(MatchingLogEvent event) {
matchingLogs.add(event);
}
public List<MatchingLogEvent> getMatchingLogs() {
return new ArrayList<>(matchingLogs);
}
/**
* Get start time of the window
* @return start time
*/
public long getStartTime() { return windowKey.getStartTime(); }
/**
* Get end time of the window
* @return end time
*/
public long getEndTime() { return windowKey.getEndTime(); }
}
}
@@ -19,21 +19,17 @@
package org.apache.hertzbeat.alert.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
/**
* SSE manager for alert
*/
@Slf4j
@Component
public class AlertSseManager {
private final Map<Long, SseEmitter> emitters = new ConcurrentHashMap<>();
@@ -42,7 +38,6 @@ public class AlertSseManager {
SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);
emitter.onCompletion(() -> removeEmitter(clientId));
emitter.onTimeout(() -> removeEmitter(clientId));
emitter.onError((ex) -> removeEmitter(clientId));
emitters.put(clientId, emitter);
return emitter;
}
@@ -55,26 +50,14 @@ public class AlertSseManager {
.id(String.valueOf(System.currentTimeMillis()))
.name("ALERT_EVENT")
.data(data));
} catch (IOException | IllegalStateException e) {
tryCompleteAndClean(clientId, emitter);
} catch (Exception exception) {
log.error("Failed to broadcast alert data to client: {}", exception.getMessage());
tryCompleteAndClean(clientId, emitter);
} catch (IOException e) {
emitter.complete();
removeEmitter(clientId);
}
});
}
private void tryCompleteAndClean(Long clientId, SseEmitter emitter) {
try {
Optional.ofNullable(emitter).ifPresent(ResponseBodyEmitter::complete);
} catch (Throwable e) {
log.debug("Failed to complete emitter for client {}: {}", clientId, e.getMessage());
}
// execute clear
removeEmitter(clientId);
}
private void removeEmitter(Long clientId) {
emitters.remove(clientId);
}
}
}
@@ -17,16 +17,17 @@
package org.apache.hertzbeat.alert.controller;
import static org.apache.hertzbeat.common.constants.CommonConstants.MONITOR_NOT_EXIST_CODE;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.util.Objects;
import org.apache.hertzbeat.alert.service.AlertDefineService;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.entity.dto.Message;
import org.apache.hertzbeat.common.support.exception.AlertExpressionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
@@ -35,17 +36,8 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static org.apache.hertzbeat.common.constants.CommonConstants.FAIL_CODE;
import static org.apache.hertzbeat.common.constants.CommonConstants.MONITOR_NOT_EXIST_CODE;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
/**
* Alarm definition management API
*/
@@ -98,18 +90,4 @@ public class AlertDefineController {
return ResponseEntity.ok(Message.success("Delete success"));
}
@GetMapping(path = "/preview/{datasource}")
@Operation(summary = "Alarm definition expression preview",
description = "If the expression is formal, then the result of the query will be returned, otherwise it will respond with an error")
public ResponseEntity<Message<List<Map<String, Object>>>> getDefinePreview(
@Parameter(description = "Data Source Type", example = "promql") @PathVariable("datasource") String datasource,
@Parameter(description = "alert threshold type:realtime,periodic") @RequestParam String type,
@Parameter(description = "alert threshold expression") @RequestParam String expr) {
try {
return ResponseEntity.ok(Message.successWithData(alertDefineService.getDefinePreview(datasource, type, expr)));
} catch (AlertExpressionException ae) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Message.fail(FAIL_CODE, ae.getMessage()));
}
}
}
@@ -38,9 +38,9 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Alarm Inhibit management API
* Alarm Silence management API
*/
@Tag(name = "Alert Inhibit API")
@Tag(name = "Alert Silence API")
@RestController
@RequestMapping(path = "/api/alert/inhibit", produces = {APPLICATION_JSON_VALUE})
public class AlertInhibitController {
@@ -49,7 +49,7 @@ public class AlertInhibitController {
private AlertInhibitService alertInhibitService;
@PostMapping
@Operation(summary = "New Alarm Inhibit", description = "Added an alarm Inhibit")
@Operation(summary = "New Alarm Silence", description = "Added an alarm Silence")
public ResponseEntity<Message<Void>> addNewAlertInhibit(@Valid @RequestBody AlertInhibit alertInhibit) {
alertInhibitService.validate(alertInhibit, false);
alertInhibitService.addAlertInhibit(alertInhibit);
@@ -57,7 +57,7 @@ public class AlertInhibitController {
}
@PutMapping
@Operation(summary = "Modifying an Alarm Inhibit", description = "Modify an existing alarm Inhibit")
@Operation(summary = "Modifying an Alarm Silence", description = "Modify an existing alarm Silence")
public ResponseEntity<Message<Void>> modifyAlertInhibit(@Valid @RequestBody AlertInhibit alertInhibit) {
alertInhibitService.validate(alertInhibit, true);
alertInhibitService.modifyAlertInhibit(alertInhibit);
@@ -65,10 +65,10 @@ public class AlertInhibitController {
}
@GetMapping(path = "/{id}")
@Operation(summary = "Querying Alarm Inhibit",
description = "You can obtain alarm Inhibit information based on the alarm Inhibit ID")
@Operation(summary = "Querying Alarm Silence",
description = "You can obtain alarm Silence information based on the alarm Silence ID")
public ResponseEntity<Message<AlertInhibit>> getAlertInhibit(
@Parameter(description = "Alarm Inhibit ID", example = "6565463543") @PathVariable("id") long id) {
@Parameter(description = "Alarm Silence ID", example = "6565463543") @PathVariable("id") long id) {
AlertInhibit alertInhibit = alertInhibitService.getAlertInhibit(id);
return Objects.isNull(alertInhibit)
@@ -36,9 +36,9 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* Inhibit the batch API for alarms
* Silence the batch API for alarms
*/
@Tag(name = "Alert Inhibit Batch API")
@Tag(name = "Alert Silence Batch API")
@RestController
@RequestMapping(path = "/api/alert/inhibits", produces = {APPLICATION_JSON_VALUE})
public class AlertInhibitsController {
@@ -50,7 +50,7 @@ public class AlertInhibitsController {
@Operation(summary = "Query the alarm inhibit list",
description = "You can obtain the list of alarm inhibit by querying filter items")
public ResponseEntity<Message<Page<AlertInhibit>>> getAlertInhibits(
@Parameter(description = "Alarm Inhibit ID", example = "6565463543") @RequestParam(required = false) List<Long> ids,
@Parameter(description = "Alarm Silence ID", example = "6565463543") @RequestParam(required = false) List<Long> ids,
@Parameter(description = "Search Name", example = "x") @RequestParam(required = false) String search,
@Parameter(description = "Sort field, default id", example = "id") @RequestParam(defaultValue = "id") String sort,
@Parameter(description = "Sort mode: asc: ascending, desc: descending", example = "desc") @RequestParam(defaultValue = "desc") String order,
@@ -64,7 +64,7 @@ public class AlertInhibitsController {
@Operation(summary = "Delete alarm inhibit in batches",
description = "Delete alarm inhibit in batches based on the alarm inhibit ID list")
public ResponseEntity<Message<Void>> deleteAlertDefines(
@Parameter(description = "Alarm Inhibit IDs", example = "6565463543") @RequestParam(required = false) List<Long> ids
@Parameter(description = "Alarm Silence IDs", example = "6565463543") @RequestParam(required = false) List<Long> ids
) {
if (ids != null && !ids.isEmpty()) {
alertInhibitService.deleteAlertInhibits(new HashSet<>(ids));
@@ -1,35 +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.alert.dao;
import org.apache.hertzbeat.common.entity.manager.Collector;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
/**
* Alert Collector Dao
*/
public interface AlertCollectorDao extends JpaRepository<Collector, Long>, JpaSpecificationExecutor<Collector> {
/**
* Query collector by name
* @param name collector name
* @return collector
*/
Collector findCollectorByName(String name);
}
@@ -1,184 +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.alert.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* Alibaba Cloud 'Simple Log Service(SLS)' alert content entity.
*
* @see <a href="https://help.aliyun.com/zh/sls/user-guide/variables-in-new-alert-templates"/>
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AlibabaCloudSlsExternAlert {
/**
* The id of the instance on which the alarm was triggered
*/
@JsonProperty("alert_instance_id")
private String alertInstanceId;
/**
* Alarm rule id, unique within project
*/
@JsonProperty("alert_id")
private String alertId;
/**
* Alarm rule name
*/
@JsonProperty("alert_name")
private String alertName;
/**
* Region
*/
private String region;
/**
* Alarm rule belongs to Project
*/
private String project;
/**
* Time of this evaluation
*/
@JsonProperty("alert_time")
private int alertTime;
/**
* First trigger time
*/
@JsonProperty("fire_time")
private int fireTime;
/**
* Alarm recovery time
* If the alarm status is firing, the value is 0.
* If the alarm state is resolved, the value is the specific recovery time.
*/
@JsonProperty("resolve_time")
private int resolveTime;
/**
* Alarm status.
* firing: Triggers an alarm.
* resolved: Notification of resumption.
*/
private String status;
/**
* The total number of entries in the data that triggered the alert,
* which may be more than 100, for example after a Cartesian product operation.
*/
@JsonProperty("fire_results_count")
private int fireResultsCount;
/**
* Tag list
* Example: {"env":"test"}
*/
private Map<String, String> labels;
/**
* Labeled lists
* Example: { "title": "Alarm title","desc": "Alarm desc" }
*/
private Map<String, String> annotations;
/**
* Alarm severity.
*
* 10: Critical
* 8: High
* 6: Medium
* 4: Low
* 2: Report only
*/
private int severity;
/**
*
*/
@JsonProperty("signin_url")
private String signinUrl;
public String getAnnotation(String key) {
if (null == this.annotations || this.annotations.isEmpty()) {
return "N/A";
}
return this.annotations.get(key);
}
/**
* Severity
*/
public enum Severity {
CRITICAL(10, "Critical"),
HIGH(8, "High"),
MEDIUM(6, "Medium"),
LOW(4, "Low"),
REPORT_ONLY(2, "Report only");
private static final Map<Integer, Severity> STATUS_MAP;
static {
STATUS_MAP = Arrays.stream(Severity.values()).collect(Collectors.toMap(Severity::getStatus, t -> t, (oldVal, newVal) -> newVal));
}
private final int status;
private final String alias;
Severity(int status, String alias) {
this.status = status;
this.alias = alias;
}
public static Optional<Severity> convert(int severity) {
return Optional.ofNullable(STATUS_MAP.get(severity));
}
public int getStatus() {
return status;
}
public String getAlias() {
return alias;
}
}
}
@@ -1,237 +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.alert.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Arrays;
/**
* Huawei Cloud (CES) alert content entity.
*
* @see <a href="https://support.huaweicloud.com/usermanual-ces/ces_01_0218.html"/>
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class HuaweiCloudExternAlert {
public static final String FIELD_MESSAGE = "message";
public static final String FIELD_MESSAGE_ID = "message_id";
public static final String FIELD_TIMESTAMP = "timestamp";
public static final String FIELD_TOPIC_URN = "topic_urn";
public static final String FIELD_TYPE = "type";
public static final String FIELD_SUBJECT = "subject";
public static final String FIELD_SUBSCRIBE_URL = "subscribe_url";
/**
* Signature information.
*/
private String signature;
/**
* Subject
*/
private String subject;
/**
* The unique identifier of a topic, indicating the topic to which the message belongs.
*/
@JsonProperty("topic_urn")
private String topicUrn;
/**
* Message unique identifier.
*/
@JsonProperty("message_id")
private String messageId;
/**
* Message
*/
private String message;
/**
* message types, the message types are respectively:
* SubscriptionConfirmation、Notification、UnsubscribeConfirmation
*/
private String type;
/**
* Subscription confirms the URL that needs to be accessed
*/
@JsonProperty("subscribe_url")
private String subscribeUrl;
/**
* The certificate URL used for message signing, which does not require authentication and can be accessed directly.
*/
@JsonProperty("signing_cert_url")
private String signingCertUrl;
/**
* The timestamp of when the message was first sent.
*/
private String timestamp;
/**
* Alert message
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class AlertMessage {
private String version;
private AlertData data;
}
/**
* Alert data
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class AlertData {
/**
* Whether an alarm occurs.
* Note: Empty and false are both recovery notifications.
* Note: There are no recovery notifications for event types
*/
@JsonProperty("IsAlarm")
private Boolean alarm;
/**
* Alarm time
*/
@JsonProperty("AlarmTime")
private String alarmTime;
/**
* Resource ID
*/
@JsonProperty("ResourceId")
private String resourceId;
/**
* The name of the metric
*/
@JsonProperty("MetricName")
private String metricName;
/**
* Specifies the alarm severity, which can be Critical, Major, Minor, or Informational.
*/
@JsonProperty("AlarmLevel")
private String alarmLevel;
/**
* Namespace
*/
@JsonProperty("Namespace")
private String namespace;
/**
* Region
*/
@JsonProperty("Region")
private String region;
/**
* Dimension name
*/
@JsonProperty("DimensionName")
private String dimensionName;
/**
* Resource name
*/
@JsonProperty("ResourceName")
private String resourceName;
/**
* Alarm record ID
*/
@JsonProperty("AlarmRecordID")
private String alarmRecordId;
/**
* Current data
*/
@JsonProperty("CurrentData")
private String currentData;
/**
* The comparison conditions for the alarm thresholds can be >, =, <, >=, <=.
*/
@JsonProperty("ComparisonOperator")
private String comparisonOperator;
/**
* Alarm value
*/
@JsonProperty("Value")
private String value;
/**
* Number of consecutive occurrences of triggered alarms
*/
@JsonProperty("Count")
private int count;
}
/**
* Huawei cloud alert type
*/
public enum AlertType {
SUBSCRIPTION("SubscriptionConfirmation"),
UNSUBSCRIBE("UnsubscribeConfirmation"),
NOTIFICATION("Notification");
private final String type;
AlertType(String type) {
this.type = type;
}
public static boolean valid(String type) {
if (null == type || type.isEmpty()) {
return false;
}
return Arrays.stream(AlertType.values()).anyMatch(alertType -> alertType.getType().equals(type));
}
public String getType() {
return type;
}
}
}
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.common.entity.dto;
package org.apache.hertzbeat.alert.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Max;
@@ -1,66 +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.alert.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* SkyWalking Alert Content Entity
* @see <a href="https://skywalking.apache.org/docs/main/latest/en/setup/backend/backend-alarm/">backend-alarm</a>
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class SkyWalkingExternAlert {
private Long scopeId;
private String scope;
private String name;
private String id0;
private String id1;
private String ruleName;
private String alarmMessage;
private Long startTime;
private List<Tag> tags;
/**
* SkyWalking Tag Entity
*/
@Data
public static class Tag {
private String key;
private String value;
}
}
@@ -118,11 +118,11 @@ public class TencentCloudExternAlert {
/**
* Transaction alarm
*/
public static final String EVENT = "event";
private static final String EVENT = "event";
/**
* Indicator alarm
*/
public static final String METRIC = "metric";
private static final String METRIC = "metric";
}
@@ -1,133 +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.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;
}
}
@@ -1,121 +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.alert.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* volcengine event alert entity class
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class VolcEngineExternEventAlert {
@JsonProperty("Type")
private String type;
@JsonProperty("AccountId")
private String accountId;
@JsonProperty("Source")
private String source;
@JsonProperty("ProjectName")
private String projectName;
@JsonProperty("Id")
private String id;
@JsonProperty("EventType")
private String eventType;
@JsonProperty("DescriptionCN")
private String descriptionCn;
@JsonProperty("HappenedAt")
private Long happenedAt;
@JsonProperty("Region")
private String region;
@JsonProperty("Details")
private EventDetails details;
@JsonProperty("Rules")
private List<EventRule> rules;
/**
* event details
*/
@Data
public static class EventDetails {
@JsonProperty("specversion")
private String specVersion;
@JsonProperty("id")
private String id;
@JsonProperty("source")
private String source;
@JsonProperty("type")
private String type;
@JsonProperty("volcpublishtime")
private String volcPublishTime;
@JsonProperty("volcregion")
private String volcRegion;
@JsonProperty("volcaccountid")
private String volcAccountId;
@JsonProperty("volceventbusname")
private String volcEventBusName;
@JsonProperty("volcresourcename")
private String volcResourceName;
@JsonProperty("subject")
private String subject;
@JsonProperty("time")
private String time;
}
/**
* event rule
*/
@Data
public static class EventRule {
@JsonProperty("RuleId")
private String ruleId;
@JsonProperty("RuleName")
private String ruleName;
@JsonProperty("Level")
private String level;
}
}
@@ -1,203 +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.alert.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* Volcengine alarm entity class
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class VolcEngineExternMetricAlert {
@JsonProperty("Type")
private String type;
@JsonProperty("AccountId")
private String accountId;
@JsonProperty("RuleName")
private String ruleName;
@JsonProperty("RuleId")
private String ruleId;
@JsonProperty("Namespace")
private String namespace;
@JsonProperty("SubNamespace")
private String subNamespace;
@JsonProperty("Level")
private String level;
@JsonProperty("HappenedAt")
private String happenedAt;
@JsonProperty("RuleCondition")
private String ruleCondition;
@JsonProperty("Resources")
private List<Resource> resources;
@JsonProperty("RecoveredResources")
private List<Resource> recoveredResources;
@JsonProperty("NoDataResources")
private List<NoDataResource> noDataResources;
@JsonProperty("NoDataRecoveredResources")
private List<NoDataRecoveredResource> noDataRecoveredResources;
/**
* alert resource entity class
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class Resource {
@JsonProperty("Id")
private String id;
@JsonProperty("Name")
private String name;
@JsonProperty("Region")
private String region;
@JsonProperty("FirstAlertTime")
private Long firstAlertTime;
@JsonProperty("LastAlertTime")
private Long lastAlertTime;
@JsonProperty("Metrics")
private List<Metric> metrics;
@JsonProperty("AlertGroupId")
private String alertGroupId;
@JsonProperty("Dimensions")
private List<Dimension> dimensions;
}
/**
* alert no data resource entity class
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public static class NoDataResource extends Resource {
@JsonProperty("NoDataMetrics")
private List<Metric> noDataMetrics;
}
/**
* alert no data recovered resource entity class
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public static class NoDataRecoveredResource extends Resource {
@JsonProperty("DroppedMetrics")
private List<DroppedMetric> droppedMetrics;
}
/**
* alert metric entity class
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Metric {
@JsonProperty("Name")
private String name;
@JsonProperty("Unit")
private String unit;
@JsonProperty("Threshold")
private Double threshold;
@JsonProperty("CurrentValue")
private Object currentValue;
@JsonProperty("Description")
private String description;
@JsonProperty("Warning")
private Boolean warning;
}
/**
* alert dropped metric entity class
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public static class DroppedMetric extends Metric {
@JsonProperty("Reason")
private String reason;
}
/**
* resource dimension entity class
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Dimension {
@JsonProperty("Name")
private String name;
@JsonProperty("NameCN")
private String nameCn;
@JsonProperty("Value")
private String value;
@JsonProperty("Description")
private String description;
}
public static final String ALERT_TYPE_METRIC = "Metric";
public static final String ALERT_TYPE_EVENT = "Event";
public static final String ALERT_TYPE_METRIC_RECOVERED = "MetricRecovered";
public static final String ALERT_TYPE_METRICS_NODATA = "MetricsNoData";
public static final String ALERT_TYPE_NO_DATA_RECOVERED = "NoDataRecovered";
}
@@ -1,462 +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.
*/
// Generated from AlertExpression.g4 by ANTLR 4.13.2
package org.apache.hertzbeat.alert.expr;
import org.antlr.v4.runtime.tree.AbstractParseTreeVisitor;
/**
* This class provides an empty implementation of {@link AlertExpressionVisitor},
* which can be extended to create a visitor which only needs to handle a subset
* of the available methods.
*
* @param <T> The return type of the visit operation. Use {@link Void} for
* operations with no return type.
*/
@SuppressWarnings("CheckReturnValue")
public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> implements AlertExpressionVisitor<T> {
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitExpression(AlertExpressionParser.ExpressionContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitAndExpr(AlertExpressionParser.AndExprContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitComparisonExpr(AlertExpressionParser.ComparisonExprContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitUnlessExpr(AlertExpressionParser.UnlessExprContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitSqlExpr(AlertExpressionParser.SqlExprContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitSqlCallExpr(AlertExpressionParser.SqlCallExprContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitLiteralExpr(AlertExpressionParser.LiteralExprContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitParenExpr(AlertExpressionParser.ParenExprContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitPromqlCallExpr(AlertExpressionParser.PromqlCallExprContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitPromqlExpr(AlertExpressionParser.PromqlExprContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitOrExpr(AlertExpressionParser.OrExprContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitFunctionCall(AlertExpressionParser.FunctionCallContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitParameterList(AlertExpressionParser.ParameterListContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitParameter(AlertExpressionParser.ParameterContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitNumber(AlertExpressionParser.NumberContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitString(AlertExpressionParser.StringContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitDuration(AlertExpressionParser.DurationContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitFunctionName(AlertExpressionParser.FunctionNameContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitSelectSql(AlertExpressionParser.SelectSqlContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitSelectFieldList(AlertExpressionParser.SelectFieldListContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitSelectField(AlertExpressionParser.SelectFieldContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitGroupByList(AlertExpressionParser.GroupByListContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitOrderByList(AlertExpressionParser.OrderByListContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitOrderByField(AlertExpressionParser.OrderByFieldContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitLimitClause(AlertExpressionParser.LimitClauseContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitRelList(AlertExpressionParser.RelListContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitRelation(AlertExpressionParser.RelationContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitConditionList(AlertExpressionParser.ConditionListContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitCompOp(AlertExpressionParser.CompOpContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitCondition(AlertExpressionParser.ConditionContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitConditionUnit(AlertExpressionParser.ConditionUnitContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitPromql(AlertExpressionParser.PromqlContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitMetricSelector(AlertExpressionParser.MetricSelectorContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitLabelMatcherList(AlertExpressionParser.LabelMatcherListContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitLabelMatcherItem(AlertExpressionParser.LabelMatcherItemContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitLabelMatcherOp(AlertExpressionParser.LabelMatcherOpContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitLabelList(AlertExpressionParser.LabelListContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitInstantVectorOp(AlertExpressionParser.InstantVectorOpContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitAggregationOperator(AlertExpressionParser.AggregationOperatorContext ctx) {
return visitChildren(ctx);
}
}
@@ -1,322 +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.alert.expr;
import org.antlr.v4.runtime.CommonTokenStream;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.hertzbeat.common.support.exception.ExpressionVisitorException;
import org.apache.hertzbeat.warehouse.db.QueryExecutor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Alert expression visitor implement
*/
public class AlertExpressionEvalVisitor extends AlertExpressionBaseVisitor<List<Map<String, Object>>> {
private static final String SCALAR = "__scalar__";
private static final String NAME = "__name__";
private static final String VALUE = "__value__";
private static final String TIMESTAMP = "__timestamp__";
private final QueryExecutor executor;
private final CommonTokenStream tokens;
public AlertExpressionEvalVisitor(QueryExecutor executor, CommonTokenStream tokens) {
this.executor = executor;
this.tokens = tokens;
}
@Override
public List<Map<String, Object>> visitExpression(AlertExpressionParser.ExpressionContext ctx) {
return visit(ctx.expr());
}
@Override
public List<Map<String, Object>> visitParenExpr(AlertExpressionParser.ParenExprContext ctx) {
return visit(ctx.expr());
}
@Override
public List<Map<String, Object>> visitComparisonExpr(AlertExpressionParser.ComparisonExprContext ctx) {
List<Map<String, Object>> leftResult = visit(ctx.left);
List<Map<String, Object>> rightResult = visit(ctx.right);
int type = ctx.op.getType();
boolean boolModifier = ctx.BOOL() != null;
boolean leftIsScalar = isScalar(leftResult);
boolean rightIsScalar = isScalar(rightResult);
List<Map<String, Object>> results = new ArrayList<>();
// scalar and scalar
if (leftIsScalar && rightIsScalar) {
if (!boolModifier) {
// Between two scalars,
// the bool modifier must be provided and these operators result in another scalar that is either 0 (false) or 1 (true), depending on the comparison result.
return results;
}
Object leftVal = leftResult.get(0).get(SCALAR);
Object rightVal = rightResult.get(0).get(SCALAR);
Boolean match = compareOp(leftVal, type, rightVal);
// returns a result only if the comparison condition is met.
Map<String, Object> result = new HashMap<>();
result.put(VALUE, match ? 1 : 0);
results.add(result);
return results;
}
// scalar and vector
if (leftIsScalar) {
Object leftVal = leftResult.get(0).get(SCALAR);
for (Map<String, Object> rightItem : rightResult) {
Object rightVal = rightItem.getOrDefault(VALUE, null);
if (isValidValue(rightVal)) {
continue;
}
Boolean match = compareOp(leftVal, type, rightVal);
Map<String, Object> result = new HashMap<>(rightItem);
if (boolModifier) {
result.put(VALUE, match ? 1 : 0);
results.add(result);
} else {
result.put(VALUE, match ? rightVal : null);
results.add(result);
}
}
return results;
}
// vector and scalar
if (rightIsScalar) {
Object rightVal = rightResult.get(0).get(SCALAR);
for (Map<String, Object> leftItem : leftResult) {
Object leftVal = leftItem.getOrDefault(VALUE, null);
if (isValidValue(leftVal)) {
continue;
}
Boolean match = compareOp(leftVal, type, rightVal);
Map<String, Object> result = new HashMap<>(leftItem);
if (boolModifier) {
result.put(VALUE, match ? 1 : 0);
results.add(result);
} else {
result.put(VALUE, match ? leftVal : null);
results.add(result);
}
}
return results;
}
// vector and vector
Map<String, Map<String, Object>> rightMap = rightResult.stream()
.filter(item -> item.get(VALUE) != null)
.collect(Collectors.toMap(this::labelKey, item -> item, (existing, replacement) -> existing));
for (Map<String, Object> leftItem : leftResult) {
Object leftVal = leftItem.getOrDefault(VALUE, null);
if (isValidValue(leftVal)) {
continue;
}
Map<String, Object> rightItem = rightMap.get(labelKey(leftItem));
if (rightItem == null) {
continue;
}
Object rightVal = rightItem.get(VALUE);
if (isValidValue(rightVal)) {
continue;
}
Boolean match = compareOp(leftVal, type, rightVal);
Map<String, Object> result = new HashMap<>(leftItem);
if (boolModifier) {
result.put(VALUE, match ? 1 : 0);
results.add(result);
} else {
result.put(VALUE, match ? leftVal : null);
results.add(result);
}
}
return results;
}
@Override
public List<Map<String, Object>> visitAndExpr(AlertExpressionParser.AndExprContext ctx) {
List<Map<String, Object>> leftOperand = visit(ctx.left);
List<Map<String, Object>> rightOperand = visit(ctx.right);
List<Map<String, Object>> results = new ArrayList<>();
// build a hash set of the right-side tag collection
Set<String> rightLabelsSet = rightOperand.stream()
.filter(item -> item.get(VALUE) != null)
.map(this::labelKey)
.collect(Collectors.toSet());
// iterate over the left side, O(1) match
for (Map<String, Object> leftItem : leftOperand) {
Object leftVal = leftItem.get(VALUE);
if (leftVal == null) {
continue;
}
String labelKey = labelKey(leftItem);
if (rightLabelsSet.contains(labelKey)) {
results.add(new HashMap<>(leftItem));
}
}
return results;
}
@Override
public List<Map<String, Object>> visitOrExpr(AlertExpressionParser.OrExprContext ctx) {
List<Map<String, Object>> leftOperand = visit(ctx.left);
List<Map<String, Object>> rightOperand = visit(ctx.right);
// build a hashMap of the left-hand label collection
Map<String, Map<String, Object>> leftLabelMap = leftOperand.stream()
.filter(item -> item.get(VALUE) != null)
.collect(Collectors.toMap(this::labelKey, HashMap::new, (k1, k2) -> k1));
// first add all the non-empty items on the left side
List<Map<String, Object>> results = new ArrayList<>(leftLabelMap.values());
// add the term that has a value on the right side and not on the left side
for (Map<String, Object> rightItem : rightOperand) {
Object rightVal = rightItem.get(VALUE);
if (rightVal == null) {
continue;
}
String key = labelKey(rightItem);
if (!leftLabelMap.containsKey(key)) {
results.add(new HashMap<>(rightItem));
}
}
return results;
}
@Override
public List<Map<String, Object>> visitUnlessExpr(AlertExpressionParser.UnlessExprContext ctx) {
List<Map<String, Object>> leftOperand = visit(ctx.left);
List<Map<String, Object>> rightOperand = visit(ctx.right);
List<Map<String, Object>> results = new ArrayList<>();
// build a hash set of the right-side tag collection
Set<String> rightLabelSet = rightOperand.stream()
.filter(item -> item.get(VALUE) != null)
.map(this::labelKey)
.collect(Collectors.toSet());
// iterate over the left side, O(1) match
for (Map<String, Object> leftItem : leftOperand) {
Object leftVal = leftItem.get(VALUE);
if (leftVal == null) {
continue;
}
if (!rightLabelSet.contains(labelKey(leftItem))) {
results.add(new HashMap<>(leftItem));
}
}
return results;
}
@Override
public List<Map<String, Object>> visitLiteralExpr(AlertExpressionParser.LiteralExprContext ctx) {
double value = Double.parseDouble(ctx.number().getText());
List<Map<String, Object>> numAsList = new ArrayList<>();
Map<String, Object> valueMap = new HashMap<>();
valueMap.put(SCALAR, value);
numAsList.add(valueMap);
return numAsList;
}
@Override
public List<Map<String, Object>> visitPromqlExpr(AlertExpressionParser.PromqlExprContext ctx) {
String rawPromql = tokens.getText(ctx.promql());
return executor.execute(rawPromql);
}
@Override
public List<Map<String, Object>> visitSqlExpr(AlertExpressionParser.SqlExprContext ctx) {
String rawSql = tokens.getText(ctx.selectSql());
return executor.execute(rawSql);
}
@Override
public List<Map<String, Object>> visitSqlCallExpr(AlertExpressionParser.SqlCallExprContext ctx) {
return callSqlOrPromql(tokens.getText(ctx.string()));
}
@Override
public List<Map<String, Object>> visitPromqlCallExpr(AlertExpressionParser.PromqlCallExprContext ctx) {
return callSqlOrPromql(tokens.getText(ctx.string()));
}
private List<Map<String, Object>> callSqlOrPromql(String text) {
String script = text.substring(1, text.length() - 1);
return executor.execute(script);
}
/**
* Generate tag key (excluding `__name__` and `__value__` and `__timestamp__`)
*/
private String labelKey(Map<String, Object> labelsMap) {
if (null == labelsMap || labelsMap.isEmpty()) {
return "-";
}
String key = labelsMap.entrySet().stream()
.filter(e -> !e.getKey().equals(VALUE) && !e.getKey().equals(NAME) && !e.getKey().equals(TIMESTAMP))
.sorted(Map.Entry.comparingByKey())
.map(e -> e.getKey() + "=" + (e.getValue() == null ? "" : e.getValue()))
.collect(Collectors.joining(","));
return key.isEmpty() ? "-" : key;
}
private boolean isScalar(List<Map<String, Object>> context) {
return CollectionUtils.isNotEmpty(context)
&& context.size() == 1
&& context.get(0).containsKey(SCALAR)
&& null != context.get(0).get(SCALAR);
}
private double parseStrToDouble(String text) {
try {
return Double.parseDouble(text);
} catch (NumberFormatException e) {
throw new ExpressionVisitorException("number format exception", e);
}
}
private boolean isValidValue(Object val) {
return val == null || val instanceof List<?>;
}
private Boolean compareOp(Object leftVal, int opType, Object rightVal) {
double left = parseStrToDouble(leftVal.toString());
double right = parseStrToDouble(rightVal.toString());
return switch (opType) {
case AlertExpressionParser.GT -> left > right;
case AlertExpressionParser.GE -> left >= right;
case AlertExpressionParser.LT -> left < right;
case AlertExpressionParser.LE -> left <= right;
case AlertExpressionParser.EQ -> left == right;
case AlertExpressionParser.NE -> left != right;
default -> false;
};
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,354 +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.
*/
// Generated from AlertExpression.g4 by ANTLR 4.13.2
package org.apache.hertzbeat.alert.expr;
import org.antlr.v4.runtime.tree.ParseTreeVisitor;
/**
* This interface defines a complete generic visitor for a parse tree produced
* by {@link AlertExpressionParser}.
*
* @param <T> The return type of the visit operation. Use {@link Void} for
* operations with no return type.
*/
public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by {@link AlertExpressionParser#expression}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitExpression(AlertExpressionParser.ExpressionContext ctx);
/**
* Visit a parse tree produced by the {@code AndExpr}
* labeled alternative in {@link AlertExpressionParser#expr}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitAndExpr(AlertExpressionParser.AndExprContext ctx);
/**
* Visit a parse tree produced by the {@code ComparisonExpr}
* labeled alternative in {@link AlertExpressionParser#expr}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitComparisonExpr(AlertExpressionParser.ComparisonExprContext ctx);
/**
* Visit a parse tree produced by the {@code UnlessExpr}
* labeled alternative in {@link AlertExpressionParser#expr}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitUnlessExpr(AlertExpressionParser.UnlessExprContext ctx);
/**
* Visit a parse tree produced by the {@code SqlExpr}
* labeled alternative in {@link AlertExpressionParser#expr}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitSqlExpr(AlertExpressionParser.SqlExprContext ctx);
/**
* Visit a parse tree produced by the {@code SqlCallExpr}
* labeled alternative in {@link AlertExpressionParser#expr}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitSqlCallExpr(AlertExpressionParser.SqlCallExprContext ctx);
/**
* Visit a parse tree produced by the {@code LiteralExpr}
* labeled alternative in {@link AlertExpressionParser#expr}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitLiteralExpr(AlertExpressionParser.LiteralExprContext ctx);
/**
* Visit a parse tree produced by the {@code ParenExpr}
* labeled alternative in {@link AlertExpressionParser#expr}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitParenExpr(AlertExpressionParser.ParenExprContext ctx);
/**
* Visit a parse tree produced by the {@code PromqlCallExpr}
* labeled alternative in {@link AlertExpressionParser#expr}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitPromqlCallExpr(AlertExpressionParser.PromqlCallExprContext ctx);
/**
* Visit a parse tree produced by the {@code PromqlExpr}
* labeled alternative in {@link AlertExpressionParser#expr}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitPromqlExpr(AlertExpressionParser.PromqlExprContext ctx);
/**
* Visit a parse tree produced by the {@code OrExpr}
* labeled alternative in {@link AlertExpressionParser#expr}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitOrExpr(AlertExpressionParser.OrExprContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#functionCall}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitFunctionCall(AlertExpressionParser.FunctionCallContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#parameterList}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitParameterList(AlertExpressionParser.ParameterListContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#parameter}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitParameter(AlertExpressionParser.ParameterContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#number}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitNumber(AlertExpressionParser.NumberContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#string}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitString(AlertExpressionParser.StringContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#duration}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitDuration(AlertExpressionParser.DurationContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#functionName}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitFunctionName(AlertExpressionParser.FunctionNameContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#selectSql}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitSelectSql(AlertExpressionParser.SelectSqlContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#selectFieldList}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitSelectFieldList(AlertExpressionParser.SelectFieldListContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#selectField}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitSelectField(AlertExpressionParser.SelectFieldContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#groupByList}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitGroupByList(AlertExpressionParser.GroupByListContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#orderByList}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitOrderByList(AlertExpressionParser.OrderByListContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#orderByField}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitOrderByField(AlertExpressionParser.OrderByFieldContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#limitClause}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitLimitClause(AlertExpressionParser.LimitClauseContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#relList}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitRelList(AlertExpressionParser.RelListContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#relation}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitRelation(AlertExpressionParser.RelationContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#conditionList}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitConditionList(AlertExpressionParser.ConditionListContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#compOp}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitCompOp(AlertExpressionParser.CompOpContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#condition}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitCondition(AlertExpressionParser.ConditionContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#conditionUnit}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitConditionUnit(AlertExpressionParser.ConditionUnitContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#promql}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitPromql(AlertExpressionParser.PromqlContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#metricSelector}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitMetricSelector(AlertExpressionParser.MetricSelectorContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#labelMatcherList}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitLabelMatcherList(AlertExpressionParser.LabelMatcherListContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#labelMatcherItem}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitLabelMatcherItem(AlertExpressionParser.LabelMatcherItemContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#labelMatcherOp}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitLabelMatcherOp(AlertExpressionParser.LabelMatcherOpContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#labelList}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitLabelList(AlertExpressionParser.LabelListContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#instantVectorOp}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitInstantVectorOp(AlertExpressionParser.InstantVectorOpContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#aggregationOperator}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitAggregationOperator(AlertExpressionParser.AggregationOperatorContext ctx);
}
@@ -80,7 +80,7 @@ public class AlertNoticeDispatch {
if (noticeTemplate == null) {
noticeTemplate = noticeConfigService.getDefaultNoticeTemplateByType(alertNotifyHandler.type());
}
if (noticeTemplate == null && alertNotifyHandler.type() != 0) {
if (noticeTemplate == null) {
log.error("alert does not have mapping default notice template. type: {}.", alertNotifyHandler.type());
throw new NullPointerException(alertNotifyHandler.type() + " does not have mapping default notice template");
}
@@ -0,0 +1,56 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.notice.impl;
import java.util.ResourceBundle;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.notice.AlertNoticeException;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
import org.apache.hertzbeat.common.util.ResourceBundleUtil;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
/**
* Send alarm information through Alibaba Cloud SMS
*/
@Component
@RequiredArgsConstructor
@Slf4j
@ConditionalOnProperty("common.sms.aliyun.app-id")
final class AliYunAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl {
private final ResourceBundle bundle = ResourceBundleUtil.getBundle("alerter");
@Override
public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) {
// SMS notification
try {
// todo send aliyun sms
} catch (Exception e) {
throw new AlertNoticeException("[Sms Notify Error] " + e.getMessage());
}
}
@Override
public byte type() {
return 0;
}
}
@@ -52,84 +52,86 @@ final class DbAlertStoreHandlerImpl implements AlertStoreHandler {
log.error("The Group Alerts is empty, ignore store");
return groupAlert;
}
// Process individual alerts
// 1. Find existing alert group
GroupAlert existGroupAlert = groupAlertDao.findByGroupKey(groupAlert.getGroupKey());
// 2. Process individual alerts
Set<String> alertFingerprints = new HashSet<>(8);
List<SingleAlert> originalAlerts = groupAlert.getAlerts();
List<SingleAlert> newAlerts = new ArrayList<>();
for (SingleAlert singleAlert : originalAlerts) {
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 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());
singleAlert.setActiveAt(existAlert.getActiveAt());
singleAlert.setTriggerTimes(existAlert.getTriggerTimes());
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());
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);
// 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);
// 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);
}
// 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);
}
}
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;
}
}
@@ -24,7 +24,7 @@ import java.util.Properties;
import java.util.ResourceBundle;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.entity.dto.MailServerConfig;
import org.apache.hertzbeat.alert.dto.MailServerConfig;
import org.apache.hertzbeat.alert.notice.AlertNoticeException;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.manager.GeneralConfig;
@@ -17,6 +17,8 @@
package org.apache.hertzbeat.alert.notice.impl;
import java.util.Arrays;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
@@ -27,14 +29,11 @@ import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.stream.Collectors;
/**
* Send alert information through FeiShu
*/
@@ -48,18 +47,19 @@ final class FlyBookAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl
*/
private static final String[] TITLE_COLOR = {"red", "yellow", "orange"};
@Override
public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) {
try {
String notificationContent = JsonUtil.toJson(renderContent(noticeTemplate, alert));
// todo priority custom the color
// todo priority custom the color
String cardMessage = createLarkMessage(receiver.getUserId(), notificationContent, (byte) 1);
String webHookUrl = alerterProperties.getFlyBookWebhookUrl() + receiver.getAccessToken();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> flyEntity = new HttpEntity<>(cardMessage, headers);
ResponseEntity<CommonRobotNotifyResp> entity = restTemplate.postForEntity(webHookUrl,
flyEntity, CommonRobotNotifyResp.class);
flyEntity, CommonRobotNotifyResp.class);
if (entity.getStatusCode() == HttpStatus.OK) {
assert entity.getBody() != null;
if (entity.getBody().getCode() == null || entity.getBody().getCode() == 0) {
@@ -87,120 +87,115 @@ final class FlyBookAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl
*/
private String createLarkMessage(String userId, String notificationContent, byte priority) {
String larkCardMessage = """
{
"msg_type": "interactive",
"card": {
"schema": "2.0",
"config": {
"update_multi": true,
"locales": [
"en_us",
"zh_cn"
],
"style": {
"text_size": {
"normal_v2": {
"default": "normal",
"pc": "normal",
"mobile": "heading"
}
}
}
},
"body": {
"direction": "vertical",
"padding": "12px 12px 12px 12px",
"elements": [
{
"tag": "markdown",
"content": "%s",
"i18n_content": {
"en_us": ""
},
"text_align": "left",
"text_size": "normal_v2",
"margin": "0px 0px 0px 0px"
},
{
"tag": "hr",
"margin": "0px 0px 0px 0px"
},
{
"tag": "column_set",
"horizontal_align": "left",
"columns": [
{
"tag": "column",
"width": "weighted",
"elements": [
{
"tag": "button",
"text": {
"tag": "plain_text",
"content": "登入控制台",
"i18n_content": {
"en_us": "Login In"
}
},
"type": "default",
"width": "default",
"size": "medium",
"behaviors": [
{
"type": "open_url",
"default_url": "%s",
"pc_url": "",
"ios_url": "",
"android_url": ""
}
]
}
],
"direction": "horizontal",
"vertical_spacing": "8px",
"horizontal_align": "left",
"vertical_align": "top",
"weight": 1
}
],
"margin": "0px 0px 0px 0px"
}
]
},
"header": {
"title": {
"tag": "plain_text",
"content": "HertzBeat 告警",
"i18n_content": {
"en_us": "HertzBeat Alarm"
}
},
"subtitle": {
"tag": "plain_text",
"content": ""
},
"template": "%s",
"padding": "12px 12px 12px 12px"
}
}
{
"msg_type": "interactive",
"card": {
"config": {
"update_multi": true
},
"i18n_elements": {
"zh_cn": [
{
"tag": "column_set",
"flex_mode": "none",
"horizontal_spacing": "default",
"background_style": "default",
"columns": [
{
"tag": "column",
"elements": [
{
"tag": "div",
"text": {
"tag": "plain_text",
"content": "",
"text_size": "normal",
"text_align": "left",
"text_color": "default"
}
}
],
"width": "weighted",
"weight": 1
}
]
},
{
"tag": "column_set",
"flex_mode": "none",
"horizontal_spacing": "default",
"background_style": "default",
"columns": [
{
"tag": "column",
"elements": [
{
"tag": "div",
"text": {
"tag": "plain_text",
"content": %s,
"text_size": "normal",
"text_align": "left",
"text_color": "default"
}
}
],
"width": "weighted",
"weight": 1
}
]
},
%s
{
"tag": "action",
"actions": [
{
"tag": "button",
"text": {
"tag": "plain_text",
"content": "登入控制台"
},
"type": "default",
"complex_interaction": true,
"width": "default",
"size": "medium",
"multi_url": {
"url": "%s"
}
}
]
}
]
},
"i18n_header": {
"zh_cn": {
"title": {
"tag": "plain_text",
"content": "HertzBeat 告警"
},
"template": "%s"
}
}
}
""";
}
""";
String atUserElement = "";
if (StringUtils.isNotBlank(userId)) {
atUserElement = "\\n" + Arrays.stream(userId.split(","))
.map(id -> "<at id=" + id + "></at>")
.collect(Collectors.joining(" "));
String atUserId = Arrays.stream(userId.split(","))
.map(id -> "<at id=" + id + "></at>")
.collect(Collectors.joining(" "));
atUserElement = String.format("""
{
"tag": "div",
"text": {
"content": "%s",
"tag": "lark_md"
}
},
""", atUserId);
}
if (notificationContent.startsWith("\"") && notificationContent.endsWith("\"")) {
notificationContent = StringUtils.removeStart(notificationContent, "\"");
notificationContent = StringUtils.removeEnd(notificationContent, "\"");
}
return String.format(larkCardMessage,
notificationContent.replace("\"", "\\\"") + atUserElement,
alerterProperties.getConsoleUrl(), TITLE_COLOR[priority]);
return String.format(larkCardMessage, notificationContent, atUserElement, alerterProperties.getConsoleUrl(), TITLE_COLOR[priority]);
}
@Override
@@ -31,8 +31,6 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* Send alarm information through Server
*/
@@ -56,16 +54,7 @@ public class ServerChanAlertNotifyHandlerImpl extends AbstractAlertNotifyHandler
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<ServerChanAlertNotifyHandlerImpl.ServerChanWebHookDto> httpEntity = new HttpEntity<>(serverChanWebHookDto, headers);
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", "https://sctapi.ftqq.com");
boolean isValidUrl = allowedBaseUrls.stream().anyMatch(webHookUrl::startsWith);
if (!isValidUrl) {
throw new AlertNoticeException("Invalid webhook URL: " + webHookUrl);
}
String webHookUrl = String.format(alerterProperties.getServerChanWebhookUrl(), receiver.getServerChanToken());
ResponseEntity<CommonRobotNotifyResp> responseEntity = restTemplate.postForEntity(webHookUrl,
httpEntity, CommonRobotNotifyResp.class);
if (responseEntity.getStatusCode() == HttpStatus.OK) {
@@ -17,7 +17,6 @@
package org.apache.hertzbeat.alert.notice.impl;
import java.net.URI;
import java.util.Objects;
import lombok.Builder;
import lombok.Data;
@@ -53,12 +52,7 @@ final class SlackAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<SlackNotifyDTO> slackNotifyEntity = new HttpEntity<>(slackNotify, headers);
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);
var entity = restTemplate.postForEntity(receiver.getSlackWebHookUrl(), slackNotifyEntity, String.class);
if (entity.getStatusCode() == HttpStatus.OK && entity.getBody() != null) {
var body = entity.getBody();
if (Objects.equals(SUCCESS, body)) {
@@ -87,21 +81,4 @@ 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;
}
}
}
@@ -21,12 +21,12 @@ import java.util.ResourceBundle;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.notice.AlertNoticeException;
import org.apache.hertzbeat.alert.service.SmsClient;
import org.apache.hertzbeat.alert.service.SmsClientFactory;
import org.apache.hertzbeat.alert.service.TencentSmsClient;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
import org.apache.hertzbeat.common.util.ResourceBundleUtil;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
/**
@@ -35,22 +35,35 @@ import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
@Slf4j
@ConditionalOnProperty("common.sms.tencent.app-id")
@Deprecated
final class SmsAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl {
private final SmsClientFactory smsFactory;
private final TencentSmsClient tencentSmsClient;
private final ResourceBundle bundle = ResourceBundleUtil.getBundle("alerter");
@Override
public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) {
// SMS notification todo use the rest api not sdk
try {
SmsClient smsClient = smsFactory.getSmsClient();
if (smsClient == null) {
throw new AlertNoticeException("No SMS Service available, please check the configuration");
String instance = null;
String priority = null;
String content = null;
if (alert.getCommonLabels() != null) {
instance = alert.getCommonLabels().get("instance");
priority = alert.getCommonLabels().get("priority");
content = alert.getCommonAnnotations().get("summary");
content = content == null ? alert.getCommonAnnotations().get("description") : content;
if (content == null) {
content = alert.getCommonAnnotations().values().stream().findFirst().orElse(null);
}
}
if (!smsClient.checkConfig()) {
throw new AlertNoticeException(smsClient.getType() + " SMS Service configuration is invalid, please check the configuration");
}
smsClient.sendMessage(receiver, noticeTemplate, alert);
String[] params = new String[3];
params[0] = instance == null ? alert.getGroupKey() : instance;
params[1] = priority == null ? "unknown" : priority;
params[2] = content;
tencentSmsClient.sendMessage(params, new String[]{receiver.getPhone()});
} catch (Exception e) {
throw new AlertNoticeException("[Sms Notify Error] " + e.getMessage());
}

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