Compare commits

..
2 Commits
Author SHA1 Message Date
tomsun28 abdf3df92c Merge branch 'master' into datazoom 2025-02-13 09:57:13 +08:00
Logic 0211a80f6b feat(web-app): update monitor chart configuration and GreptimeDB version
- Remove dataZoom feature from monitor chart
- Disable zoom and move interactions in chart- Update GreptimeDB version from 0.9.1 to 0.11.0
2025-02-12 20:34:15 +08:00
7649 changed files with 148880 additions and 867360 deletions
+36 -451
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,355 +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",
"code"
]
},
{
"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"
]
},
{
"login": "KOYR",
"name": "KOYR",
"avatar_url": "https://avatars.githubusercontent.com/u/53216619?v=4",
"profile": "https://github.com/KOYR",
"contributions": [
"code"
]
},
{
"login": "Lathika226",
"name": "LathikaBaddam",
"avatar_url": "https://avatars.githubusercontent.com/u/178710568?v=4",
"profile": "https://github.com/Lathika226",
"contributions": [
"doc"
]
},
{
"login": "Sahil-Shadwal",
"name": "Sahil Shadwal",
"avatar_url": "https://avatars.githubusercontent.com/u/119167601?v=4",
"profile": "https://shadwal.space/",
"contributions": [
"code"
]
},
{
"login": "BhanuNidumolu",
"name": "N.Bhanu Prasad",
"avatar_url": "https://avatars.githubusercontent.com/u/180380413?v=4",
"profile": "https://github.com/BhanuNidumolu",
"contributions": [
"code"
]
},
{
"login": "Prakash1185",
"name": "Prakash Kumar",
"avatar_url": "https://avatars.githubusercontent.com/u/183058331?v=4",
"profile": "https://prakashh-portfolio.vercel.app/",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,
+6 -9
View File
@@ -16,15 +16,11 @@
#
github:
description: An AI-powered next-generation open source real-time observability system.
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:
- ai
- agent
- llm
- logs
- monitor
- monitoring
- monitor
- notifications
- alerting
- self-hosted
@@ -34,11 +30,15 @@ github:
- metrics
- observability
- uptime
- uptime-monitoring
- status
- status-page
- devops
- server
- linux
- database
- mysql
- cloud
enabled_merge_buttons:
squash: true
merge: false
@@ -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",
-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.
#
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
# The line max length is 120, match checkstyle rules.
max_line_length = 120
insert_final_newline = true
trim_trailing_whitespace = true
[*.json]
tab_width = 2
indent_size = 2
[*.{yml,yaml}]
tab_width = 2
indent_size = 2
[*.xml]
ij_xml_attribute_wrap = off
ij_xml_text_wrap = off
ij_xml_keep_blank_lines = 1
[pom.xml]
indent_size = 2
[*.java]
indent_size = 4
tab_width = 4
[*.{js,jsx,mjs,cjs,ts,tsx}]
indent_size = 2
tab_width = 2
-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
-23
View File
@@ -1,23 +0,0 @@
#!/bin/sh
# 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.
set -eu
common_hook="$(git rev-parse --git-common-dir)/hooks/post-commit"
if [ -x "$common_hook" ]; then
exec "$common_hook" "$@"
fi
-24
View File
@@ -1,24 +0,0 @@
#!/bin/sh
# 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.
set -eu
repo_root=$(git rev-parse --show-toplevel)
cd "$repo_root/web-app"
node scripts/check-toolchain.mjs
exec pnpm format:staged
+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"
+10 -10
View File
@@ -90,24 +90,24 @@ e2e:
backend:
- changed-files:
- any-glob-to-any-file:
- "hertzbeat-alerter/**/*"
- "hertzbeat-collector/**/*"
- "hertzbeat-common/**/*"
- "hertzbeat-grafana/**/*"
- "hertzbeat-manager/**/*"
- "hertzbeat-push/**/*"
- "hertzbeat-remoting/**/*"
- "hertzbeat-warehouse/**/*"
- "alerter/**/*"
- "collector/**/*"
- "common/**/*"
- "grafana/**/*"
- "manager/**/*"
- "push/**/*"
- "remoting/**/*"
- "warehouse/**/*"
collector:
- changed-files:
- any-glob-to-any-file:
- "hertzbeat-collector/**/*"
- "collector/**/*"
monitoring-template:
- changed-files:
- any-glob-to-any-file:
- "hertzbeat-manager/src/main/resources/define/**/*"
- "manager/src/main/resources/define/**/*"
HIP:
- changed-files:
+10 -50
View File
@@ -20,74 +20,33 @@
name: Backend CI
permissions:
contents: read
concurrency:
group: backend-ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches: [ master, dev, action*, '2.*' ]
branches: [ master, dev, action* ]
paths-ignore:
- '**.md'
- 'home/**'
- 'web-app/**'
- 'script/**'
- 'material/**'
pull_request:
branches: [ master, dev, '2.*' ]
branches: [ master, dev ]
paths-ignore:
- '**.md'
- 'home/**'
- 'web-app/**'
- 'script/**'
- 'material/**'
jobs:
backend-build:
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- uses: actions/checkout@v4
- uses: ./script/ci/github-actions/setup-deps
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Enable pnpm
run: |
corepack enable
corepack prepare pnpm@10.9.0 --activate
- name: Resolve pnpm store
id: pnpm-store
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- uses: actions/cache@v4
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: pnpm-${{ runner.os }}-${{ hashFiles('web-app/pnpm-lock.yaml') }}
restore-keys: |
pnpm-${{ runner.os }}-
- name: Verify server release layout
run: |
./script/ci/verify-server-release-layout.sh
python3 -m unittest script.ci.test_verify_server_release_package
- name: Build frontend release assets
working-directory: web-app
run: |
pnpm install --frozen-lockfile
pnpm build
- name: Build with Maven
run: mvnd clean -B package -Prelease -Dmaven.test.skip=false --file pom.xml
- name: Verify server release package
run: |
release_version=$(sed -n 's:.*<hzb.version>\([^<][^<]*\)</hzb.version>.*:\1:p' pom.xml | head -1)
server_archive="dist/apache-hertzbeat-${release_version}-bin.tar.gz"
test -f "$server_archive"
python3 script/ci/verify-server-release-package.py "$server_archive"
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
@@ -107,11 +66,11 @@ jobs:
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
sudo docker-compose version
sudo docker-compose up --exit-code-from testing --remove-orphans
# upload application logs
- name: Upload logs & API test reports
uses: actions/upload-artifact@v4
@@ -121,3 +80,4 @@ jobs:
path: |
e2e/logs/
e2e/report/
-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
-88
View File
@@ -1,88 +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: DOC Deploy
on:
# Trigger on push to master branch when home directory files are changed
push:
branches: [ master ]
paths:
- 'home/**'
# Allow manual workflow trigger
workflow_dispatch:
# Set permissions for deployment
permissions:
contents: write
jobs:
deploy:
name: Deploy Documentation to asf-site Branch
runs-on: ubuntu-latest
steps:
# Checkout the repository code
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0
# Setup Node.js environment
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
# Setup pnpm
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: latest
# Install dependencies in home directory
- name: Install Dependencies
working-directory: home
run: pnpm install --frozen-lockfile
# Build the documentation site
- name: Build Documentation
working-directory: home
run: pnpm run build
env:
NODE_OPTIONS: --max-old-space-size=4096
# Deploy to asf-site branch using peaceiris/actions-gh-pages
- name: Deploy to asf-site Branch
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./home/build
publish_branch: asf-site
force_orphan: true
user_name: 'github-actions[bot]'
user_email: 'github-actions[bot]@users.noreply.github.com'
commit_message: 'Deploy documentation to asf-site branch'
# Output deployment status
- name: Deployment Summary
run: |
echo "Documentation deployed successfully!"
echo "Deployment branch: asf-site"
echo "Source branch: ${{ github.ref_name }}"
echo "Commit SHA: ${{ github.sha }}"
+1 -2
View File
@@ -17,8 +17,7 @@
name: 'HertzBeat Doc Pdf Builder'
on:
push:
branches: [ page_build* ]
page_build
jobs:
build:
+10 -35
View File
@@ -15,54 +15,29 @@
# specific language governing permissions and limitations
# under the License.
name: Frontend CI
# 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
permissions:
contents: read
concurrency:
group: frontend-ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
name: Frontend CI
on:
push:
branches: [ master, dev, 'action*', 'codex/**', 'feature/**', 'release/**', '2.*' ]
branches: [ master, dev ]
paths:
- 'web-app/**'
- '.github/workflows/frontend-build-test.yml'
pull_request:
branches: [ master, dev, 'codex/**', 'feature/**', 'release/**', '2.*' ]
branches: [ master, dev ]
paths:
- 'web-app/**'
- '.github/workflows/frontend-build-test.yml'
jobs:
verify:
build:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Enable pnpm
run: |
corepack enable
corepack prepare pnpm@10.9.0 --activate
- name: Resolve pnpm store
id: pnpm-store
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- uses: actions/cache@v4
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: pnpm-${{ runner.os }}-${{ hashFiles('web-app/pnpm-lock.yaml') }}
restore-keys: |
pnpm-${{ runner.os }}-
- name: Install
- name: Yarn Install
working-directory: web-app
run: pnpm install --frozen-lockfile
- name: Verify
run: yarn
- name: EsLint Test
working-directory: web-app
run: pnpm verify
run: yarn lint:ts
@@ -1,233 +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: Hybrid Collector Release Gate
on:
pull_request:
paths:
- '.github/workflows/hybrid-collector-release.yml'
- '.github/workflows/nightly-build.yml'
- 'pom.xml'
- 'hertzbeat-collector/**'
- 'hertzbeat-common-core/**'
- 'hertzbeat-manager/**'
- 'hertzbeat-observability/**'
# Negative paths: run the module-boundary contract if a legacy module reappears.
- 'hertzbeat-log/**'
- 'hertzbeat-otel/**'
- 'hertzbeat-otel-runtime/**'
- 'hertzbeat-startup/**'
- 'script/assembly/collector/**'
- 'script/ci/*hybrid-collector*'
- 'script/ci/test_observability_module_boundary.py'
- 'script/ci/*otel-runtime*'
- 'script/docker/collector/**'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: hybrid-collector-${{ github.ref }}
cancel-in-progress: true
jobs:
contract:
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v6
with:
go-version-file: hertzbeat-otel-runtime/go.mod
cache-dependency-path: hertzbeat-otel-runtime/go.mod
- uses: graalvm/setup-graalvm@v1
with:
java-version: 25
distribution: graalvm
github-token: ${{ secrets.GITHUB_TOKEN }}
cache: maven
- name: Verify source and dependency contracts
run: |
python3 script/ci/test_observability_module_boundary.py -v
python3 script/ci/test_verify_hybrid_collector_release_content.py -v
python3 script/ci/test_verify_hybrid_collector_jvm_package.py -v
python3 script/ci/test_verify_otel_runtime_sbom_platform.py -v
python3 script/ci/test_prepare_hybrid_collector_native_container_context.py -v
python3 script/ci/test_hybrid_collector_systemd_install.py -v
mkdir -p target/release-policy
git archive --format=tar.gz --output target/release-policy/hertzbeat-source.tar.gz HEAD
python3 script/ci/verify-hybrid-collector-release-content.py \
--source target/release-policy/hertzbeat-source.tar.gz
./script/ci/verify-hybrid-collector-release-layout.sh
./script/ci/verify-otel-runtime-layout.sh
./script/ci/verify-hybrid-collector-native-layout.sh
make -C hertzbeat-otel-runtime validate
make -C hertzbeat-otel-runtime license-check
- name: Verify Go binary reproducibility
run: ./script/ci/verify-otel-runtime-reproducibility.sh
- name: Verify Java runtime and Spring AOT
run: |
./script/ci/verify-hybrid-collector-java.sh
make -C hertzbeat-otel-runtime release-assets
./mvnw -pl hertzbeat-collector/hertzbeat-collector-collector -am \
-Pruntime -DskipTests package
set -- dist/apache-hertzbeat-collector-*-bin.tar.gz
test "$#" -eq 1 && test -f "$1"
sh ./script/ci/verify-hybrid-collector-jvm-package.sh "$1" generic
set -- dist/apache-hertzbeat-collector-*-bin-linux_amd64.tar.gz
test "$#" -eq 1 && test -f "$1"
sh ./script/ci/verify-hybrid-collector-jvm-package.sh "$1" linux-amd64
source-policy:
strategy:
fail-fast: false
matrix:
runner: [ubuntu-24.04, macos-15, windows-2025]
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: graalvm/setup-graalvm@v1
with:
java-version: 25
distribution: graalvm
github-token: ${{ secrets.GITHUB_TOKEN }}
cache: maven
- name: Verify cross-platform source policy
shell: bash
run: |
./mvnw -pl hertzbeat-collector/hertzbeat-collector-collector -am \
-Dtest='ManagedOtelRuntimeConfigTest,OtelRuntimeConfigRendererTest,OtelRuntimeSourcePolicyTest' \
test -DskipITs -Dsurefire.failIfNoSpecifiedTests=false -DfailIfNoTests=false
native-release:
if: github.event_name == 'workflow_dispatch'
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-24.04
platform: linux-amd64
- runner: ubuntu-24.04-arm
platform: linux-arm64
- runner: macos-15-intel
platform: macos-amd64
- runner: macos-15
platform: macos-arm64
- runner: windows-2025
platform: windows-amd64
runs-on: ${{ matrix.runner }}
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v6
with:
go-version-file: hertzbeat-otel-runtime/go.mod
cache-dependency-path: hertzbeat-otel-runtime/go.mod
- uses: graalvm/setup-graalvm@v1
with:
java-version: 25
distribution: graalvm
github-token: ${{ secrets.GITHUB_TOKEN }}
cache: maven
- name: Install make on Windows
if: runner.os == 'Windows'
shell: pwsh
run: choco install make --no-progress -y
- name: Build release assets
shell: bash
run: make -C hertzbeat-otel-runtime release-assets
- name: Build native Collector
shell: bash
run: ./mvnw -pl hertzbeat-collector/hertzbeat-collector-collector -am -Pnative -DskipTests package
- name: Verify native package
shell: bash
run: |
archive=$(find dist -maxdepth 1 -type f -name "*-${{ matrix.platform }}-bin.*" -print -quit)
test -n "$archive"
./script/ci/verify-hybrid-collector-native-package.sh "$archive" "${{ matrix.platform }}"
- uses: actions/upload-artifact@v4
with:
name: hybrid-collector-package-${{ matrix.platform }}
path: dist/*-${{ matrix.platform }}-bin.*
if-no-files-found: error
native-container:
if: github.event_name == 'workflow_dispatch'
needs: native-release
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
pattern: hybrid-collector-package-linux-*
path: dist
merge-multiple: true
- name: Prepare verified native container inputs
run: ./script/ci/prepare-hybrid-collector-native-container-context.sh dist target/native-container-context
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- name: Build runnable amd64 native image
uses: docker/build-push-action@v6
with:
context: target/native-container-context
file: script/docker/collector/Dockerfile.native
platforms: linux/amd64
load: true
tags: apache/hertzbeat-collector:native-test
- name: Verify native image runtime
run: ./script/ci/verify-hybrid-collector-native-image.sh apache/hertzbeat-collector:native-test linux/amd64
- name: Build multi-platform native image
uses: docker/build-push-action@v6
with:
context: target/native-container-context
file: script/docker/collector/Dockerfile.native
platforms: linux/amd64,linux/arm64
push: false
outputs: type=oci,dest=/tmp/hybrid-collector.oci.tar
tags: apache/hertzbeat-collector:native-test
- name: Verify multi-platform native image layers
run: |
python3 script/ci/verify-hybrid-collector-release-content.py \
--container-image /tmp/hybrid-collector.oci.tar
- uses: actions/upload-artifact@v4
with:
name: hybrid-collector-native-oci
path: /tmp/hybrid-collector.oci.tar
if-no-files-found: error
native-checksums:
if: github.event_name == 'workflow_dispatch'
needs: native-release
runs-on: ubuntu-24.04
steps:
- uses: actions/download-artifact@v4
with:
pattern: hybrid-collector-package-*
path: release
merge-multiple: true
- name: Generate archive checksums
working-directory: release
run: find . -maxdepth 1 -type f -print0 | sort -z | xargs -0 sha512sum > SHA512SUMS
- uses: actions/upload-artifact@v4
with:
name: hybrid-collector-native-checksums
path: release/SHA512SUMS
if-no-files-found: error
+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
+8 -59
View File
@@ -17,13 +17,6 @@
name: Nightly CI
permissions:
contents: read
concurrency:
group: nightly-ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
schedule:
# trigger at 00:00 everyday
@@ -34,56 +27,23 @@ on:
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 180
if: ${{ github.repository == 'apache/hertzbeat' }}
steps:
- uses: actions/checkout@v4
- uses: ./script/ci/github-actions/setup-deps
- uses: actions/setup-node@v4
with:
node-version: 22
- uses: actions/setup-go@v6
with:
go-version-file: hertzbeat-otel-runtime/go.mod
cache-dependency-path: hertzbeat-otel-runtime/go.mod
- uses: actions/setup-java@v4
with:
distribution: zulu
java-version: 25
- name: Build the Frontend
run: |
corepack enable
corepack prepare pnpm@10.9.0 --activate
cd web-app
pnpm install --frozen-lockfile
pnpm build
- name: Build and verify managed Collector runtimes
run: |
make -C hertzbeat-otel-runtime release-assets
./script/ci/verify-otel-runtime-package-layout.sh
yarn install
yarn package
- name: Build the Backend
run: |
mvn clean install
mvn clean package -Prelease -DskipTests
./mvnw -pl hertzbeat-collector/hertzbeat-collector-collector -am \
-Pruntime -DskipTests package
- name: Verify Linux Collector release content
run: |
set -- dist/apache-hertzbeat-collector-*-bin-linux_amd64.tar.gz
if [ "$#" -ne 1 ] || [ ! -f "$1" ]; then
echo "expected exactly one generated linux-amd64 Hybrid Collector archive" >&2
exit 1
fi
./script/ci/verify-hybrid-collector-jvm-package.sh "$1" linux-amd64
set -- dist/apache-hertzbeat-collector-*-bin-linux_arm64.tar.gz
if [ "$#" -ne 1 ] || [ ! -f "$1" ]; then
echo "expected exactly one generated linux-arm64 Hybrid Collector archive" >&2
exit 1
fi
./script/ci/verify-hybrid-collector-jvm-package.sh "$1" linux-arm64
cd collector
mvn clean package -Pcluster -DskipTests
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
@@ -102,22 +62,11 @@ jobs:
push: true
tags: apache/hertzbeat:nightly
- name: Build Collector image for verification
- name: Build and Push Collector
uses: docker/build-push-action@v6
with:
context: ./dist
file: ./script/docker/collector/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: apache/hertzbeat-collector:nightly
outputs: type=oci,dest=/tmp/hybrid-collector-nightly.oci.tar
- name: Verify final Collector image content
run: python3 script/ci/verify-hybrid-collector-release-content.py --container-image /tmp/hybrid-collector-nightly.oci.tar
- name: Push verified Collector image
run: |
sudo apt-get update
sudo apt-get install -y skopeo
skopeo copy --all --authfile "$HOME/.docker/config.json" \
oci-archive:/tmp/hybrid-collector-nightly.oci.tar \
docker://apache/hertzbeat-collector:nightly
+3 -21
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
@@ -46,18 +46,6 @@ nbdist/
node_modules
.docusaurus
# codex / browser / screenshot noise
.codex-artifacts/
.playwright-mcp/
progress.md
test-results/
playwright-report/
reports/
screenshots/
# local temp runtime data
.tmp/
# h2 data
*.mv.db
*.trace.db
@@ -66,9 +54,3 @@ screenshots/
application-dev.yml
application-mysql.yml
application-pg.yml
/package.json
/yarn.lock
.omx/
# legacy frontend build output
/web-app/dist/
-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 21.0.2-tem && \
sdk default java 21.0.2-tem"
+5 -6
View File
@@ -1,10 +1,7 @@
image:
file: .gitpod.Dockerfile
tasks:
- name: Run backend
before: cd hertzbeat-startup
before: cd hertzbeat-manager
command: |
gp sync-await setup-backend
mvn spring-boot:run
@@ -13,11 +10,13 @@ tasks:
before: cd web-app
command: |
gp sync-await setup-frontend
pnpm start --public-host "`gp url 4200`"
yarn start --public-host "`gp url 4200`"
openMode: split-right
- 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
@@ -25,7 +24,7 @@ tasks:
- name: Setup frontend
init: |
cd web-app && pnpm install
cd web-app && yarn install
command: |
gp sync-done setup-frontend
exit 0
-3
View File
@@ -39,7 +39,6 @@ header:
- '**/*.js'
- '**/*.less'
- '**/*.txt'
- '**/*.st'
- '**/target/**'
- '.gitattributes'
- '**/.gitignore'
@@ -67,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/**'
+5 -6
View File
@@ -15,7 +15,6 @@
// limitations under the License.
//
{
"globs": ["home/**/*.md"],
"config": {
"default": true,
"MD001": true,
@@ -31,7 +30,7 @@
"MD025": true,
"MD029": true,
"MD033": {
"allowed_elements": ["table", "tr", "td", "a", "img", "sub", "b", "br", "img", "tbody", "mark", "font", "Tabs", "TabItem"]
"allowed_elements": ["table", "tr", "td", "a", "img", "sub", "b", "br", "img", "tbody", "mark", "font"]
},
"MD036": false,
"MD040": 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
+48 -38
View File
@@ -20,39 +20,43 @@
- 💬**Suggestion**: You can also make some suggestions to the project or community to promote the healthy development of the community;
More see [Contribution Types](https://allcontributors.org/emoji-key)
More see [Contribution Types](https://allcontributors.org/docs/en/emoji-key)
Even small corrections to typos are very welcome :)
### Getting HertzBeat up and running
> To get HertzBeat code running on your development tools, and able to debug with breakpoints.
> This is a front-end and back-end separation project. To start the local code, the back-end [startup](hertzbeat-startup) and the front-end [web-app](web-app) must be started separately.
> This is a front-end and back-end separation project. To start the local code, the back-end [manager](manager) and the front-end [web-app](web-app) must be started separately.
#### Backend start
1. Requires `maven3+`, Java 25 and `lombok` environments
1. Requires `maven3+`, `java17` and `lombok` environments
2. (Optional) Modify the configuration file: `hertzbeat-startup/src/main/resources/application.yml`
2. (Optional) Modify the configuration file: `manager/src/main/resources/application.yml`
3. Execute under the project root directory: `mvn clean install -DskipTests`
4. Add VM Options: `--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED`
5. Start `springboot startup` service: `hertzbeat-startup/src/main/java/org/apache/hertzbeat/startup/HertzBeatApplication.java`
5. Start `springboot manager` service: `manager/src/main/java/org/apache/hertzbeat/hertzbeat-manager/Manager.java`
#### Frontend start
1. Need `Node.js 22` and `pnpm 10`
1. Need `Node Yarn` Environment, Make sure `Node.js >= 18`
2. Cd to the `web-app` directory: `cd web-app`
3. Install dependencies: `corepack pnpm@10.9.0 install --frozen-lockfile`
3. Install yarn if not existed `npm install -g yarn`
4. After the local backend is started, start the local frontend in the `web-app` directory: `corepack pnpm@10.9.0 dev`
4. Install Dependencies: `yarn install` or `yarn install --registry=https://registry.npmmirror.com` in `web-app`
5. Browser access to localhost:4200 to start, default account/password is *admin/hertzbeat*
5. Install angular-cli globally: `yarn global add @angular/cli@15` or `yarn global add @angular/cli@15 --registry=https://registry.npmmirror.com`
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
@@ -87,16 +91,6 @@ Of course, if you have a good idea, you can also propose it directly on GitHub D
Please note that the title of the PR needs to conform to our spec, and write the necessary description in the PR to facilitate code review by Committers and other contributors.
### AI-assisted contributions
AI-assisted code is welcome when it is reviewed and owned by a human contributor. Please follow [AI-Assisted Development Guidelines](AGENTS.md):
1. Treat AI output as a draft and keep each PR narrowly scoped.
2. Do not include proprietary, private, license-incompatible, or secret material from AI prompts or outputs.
3. Do not invent product behavior, API responses, topology data, metrics, health states, screenshots, or release claims.
4. Write or update the smallest useful test/contract first for risky changes, then include the validation commands and results in the PR description.
5. If AI helped, add a short PR note: `AI assistance`, `Human validation`, and `Risk notes`.
### Wait for the code to be merged
After submitting the PR, the Committee or the community's friends will review the code you submitted (Code Review), and will propose some modification suggestions or conduct some discussions. Please pay attention to your PR in time.
@@ -139,6 +133,17 @@ Add WeChat account `ahertzbeat` to pull you into the WeChat group.
## 🥐 Architecture
- **[manager](https://github.com/apache/hertzbeat/tree/master/hertzbeat-manager)** Provide monitoring management, system management basic services.
> Provides monitoring management, monitoring configuration management, system user management, etc.
- **[collector](https://github.com/apache/hertzbeat/tree/master/collector)** Provide metrics data collection services.
> Use common protocols to remotely collect and obtain peer-to-peer metrics data.
- **[warehouse](https://github.com/apache/hertzbeat/tree/master/warehouse)** Provide monitoring data warehousing services.
> Metrics data management, data query, calculation and statistics.
- **[alerter](https://github.com/apache/hertzbeat/tree/master/hertzbeat-alerter)** Provide alert service.
> Alarm calculation trigger, monitoring status linkage, alarm configuration, and alarm notification.
- **[web-app](https://github.com/apache/hertzbeat/tree/master/web-app)** Provide web ui.
> Angular Web UI.
![hertzBeat](home/static/img/docs/hertzbeat-arch.png)
<br>
@@ -169,39 +174,43 @@ Add WeChat account `ahertzbeat` to pull you into the WeChat group.
- 💬**建议**:也可以对项目或者社区提出一些建议,促进社区的良性发展;
更多贡献方式参见 [Contribution Types](https://allcontributors.org/emoji-key)
更多贡献方式参见 [Contribution Types](https://allcontributors.org/docs/en/emoji-key)
即便是小到错别字的修正我们也都非常欢迎 :)
### 让 HertzBeat 运行起来
> 让 HertzBeat 的代码在您的开发工具上运行起来,并且能够断点调试。
> 此为前后端分离项目,本地代码启动需将后端[hertzbeat-startup](hertzbeat-startup)和前端[web-app](web-app)分别启动生效。
> 此为前后端分离项目,本地代码启动需将后端[manager](manager)和前端[web-app](web-app)分别启动生效。
#### 后端启动
1. 需要 `maven3+`、Java 25 和 `lombok` 环境
1. 需要 `maven3+`, `java17` 和 `lombok` 环境
2. (可选)修改配置文件配置信息-`hertzbeat-startup/src/main/resources/application.yml`
2. (可选)修改配置文件配置信息-`manager/src/main/resources/application.yml`
3. 在项目根目录运行编译: `mvn clean install -DskipTests`
4. 在 `jvm` 加入参数 `--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED`
5. 启动`springboot startup`服务 `hertzbeat-startup/src/main/java/org/apache/hertzbeat/startup/HertzBeatApplication.java`
5. 启动`springboot manager`服务 `manager/src/main/java/org/apache/hertzbeat/hertzbeat-manager/Manager.java`
#### 前端启动
1. 需要 `Node.js 22` 和 `pnpm 10`
1. 需要 `nodejs yarn` 环境, Make sure `Node.js >= 18`
2. 进入 `web-app` 目录: `cd web-app`
3. 安装依赖: `corepack pnpm@10.9.0 install --frozen-lockfile`
3. 安装yarn: `npm install -g yarn`
4. 待本地后端启动后,在 `web-app` 目录下启动本地前端: `corepack pnpm@10.9.0 dev`
4. 在前端工程目录 `web-app` 下执行: `yarn install` or `yarn install --registry=https://registry.npmmirror.com` in `web-app`
5. 浏览器访问 localhost:4200 即可开始,默认账号密码 *admin/hertzbeat*
5. 全局安装 `angular-cli`: `yarn global add @angular/cli@15` or `yarn global add @angular/cli@15 --registry=https://registry.npmmirror.com`
6. 待本地后端启动后,在web-app目录下启动本地前端 `ng serve --open`
7. 浏览器访问 localhost:4200 即可开始,默认账号密码 *admin/hertzbeat*
### 寻找任务
@@ -235,16 +244,6 @@ Add WeChat account `ahertzbeat` to pull you into the WeChat group.
请注意 PR 的标题需要符合我们的规范,并且在 PR 中写上必要的说明,来方便 Committer 和其他贡献者进行代码审查。
### AI 辅助贡献
欢迎使用 AI 辅助写代码,但 PR 必须由人类贡献者负责、审查和验证。请遵循 [AI-Assisted Development Guidelines](AGENTS.md)
1. 把 AI 输出当作草稿,每个 PR 保持清晰、收敛的范围。
2. 不要提交来自 AI 提示词或输出中的私有、专有、许可证不兼容内容,也不要包含密钥或凭据。
3. 不要编造产品行为、API 响应、拓扑数据、指标、健康状态、截图或发布结论。
4. 对高风险或用户可见改动,先写或更新最小测试/合约,再在 PR 描述中写明验证命令和结果。
5. 如果使用了 AI,请在 PR 描述中简短说明:`AI assistance`、`Human validation`、`Risk notes`。
### 等待PR代码被合并
在提交了 PR 后,Committer 或者社区的小伙伴们会对您提交的代码进行审查(Code Review),会提出一些修改建议,或者是进行一些讨论,请及时关注您的PR。
@@ -287,4 +286,15 @@ Add WeChat account `ahertzbeat` to pull you into the WeChat group.
### 模块
- **[manager](https://github.com/apache/hertzbeat/tree/master/hertzbeat-manager)** 提供监控管理,系统管理基础服务
> 提供对监控的管理,监控应用配置的管理,系统用户租户后台管理等。
- **[collector](https://github.com/apache/hertzbeat/tree/master/collector)** 提供监控数据采集服务
> 使用通用协议远程采集获取对端指标数据。
- **[warehouse](https://github.com/apache/hertzbeat/tree/master/warehouse)** 提供监控数据仓储服务
> 采集指标结果数据管理,数据落盘,查询,计算统计。
- **[alerter](https://github.com/apache/hertzbeat/tree/master/hertzbeat-alerter)** 提供告警服务
> 告警计算触发,任务状态联动,告警配置,告警通知。
- **[web-app](https://github.com/apache/hertzbeat/tree/master/web-app)** 提供可视化控制台页面
> 监控告警系统可视化控制台前端
![hertzBeat](home/static/img/docs/hertzbeat-arch.png)
+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/).
+62 -108
View File
@@ -4,22 +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 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)
[![zread](https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTg0QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff)](https://zread.ai/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**
@@ -27,23 +26,28 @@
## 🎡 <font color="green">Introduction</font>
[Apache HertzBeat](https://github.com/apache/hertzbeat) is an AI-powered next-generation open source real-time observability system. Unified metrics and logs collection, centralized alerting distribution, intelligent management and analysis. No Agent required, high performance cluster, provides 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
- Integrates **collection + analysis + alerting + notification** into one platform, with new AI-powered interactions and features under HertzBeat AI, and built-in MCP Server capabilities.
- Unified metrics platform, agentless, Prometheus-compatible, supports application services, programs, databases, caches, operating systems, big data, middleware, web servers, cloud-native, networks, custom monitoring and more.
- Unified logging platform, seamlessly integrates multiple log sources through OTLP protocol for reporting.
- Unified alerting platform, integrates internal alerts with various external alert sources, unified alert processing and analysis, flexible real-time and periodic threshold rules, grouping convergence, silence, suppression, etc.
- Unified message distribution, alerts processed by the alerting platform are distributed via `Email` `Discord` `Slack` `Telegram` `DingTalk` `WeChat` `FeiShu` `SMS` `Webhook` `ServerChan` and other methods.
- Makes protocols such as `Http, Jmx, Ssh, Snmp, Jdbc, Prometheus` configurable, allowing you to collect any metrics by simply configuring the template `YML` file online. Imagine being able to quickly adapt to a new monitoring type like K8s or Docker simply by configuring online with HertzBeat.
- High performance, supports horizontal expansion of multi-collector clusters, multi-isolated network monitoring and cloud-edge collaboration.
- Provides powerful status page building capabilities, easily communicate the real-time status of your service to users.
* Combines **monitoring, alarm, and notification** features into one platform, and supports monitoring for web service, program, database, cache, os, webserver, middleware, bigdata, cloud-native, network, custom and more.
* Easy to use and agentless, web-based and with one-click monitoring and alerting, zero learning curve.
* Makes protocols such as `Http, Jmx, Ssh, Snmp, Jdbc, Prometheus` configurable, allowing you to collect any metrics by simply configuring the template `YML` file online. Imagine being able to quickly adapt to a new monitoring type like K8s or Docker simply by configuring online with HertzBeat.
* Compatible with the `Prometheus` ecosystem and more, can monitoring what `Prometheus` can monitoring with few clicks on webui.
* High performance, supports horizontal expansion of multi-collector clusters, multi-isolated network monitoring and cloud-edge collaboration.
* Provides flexible alarm threshold rules and timely notifications delivered via `Discord` `Slack` `Telegram` `Email` `Dingtalk` `WeChat` `FeiShu` `Webhook` `SMS` `ServerChan`.
* Provides powerful status page building capabilities, easily communicate the real-time status of your service to users.
> HertzBeat's unified platform, AI intelligence, powerful customization, multi-type support, high performance, and easy expansion, aims to help users quickly and conveniently achieve observability requirements.
> HertzBeat's powerful customization, multi-type support, high performance, easy expansion, and low coupling, aims to help users quickly build their own monitoring system.
----
![hertzbeat](home/static/img/home/1.png)
![hertzbeat](home/static/img/home/status.png)
![hertzbeat](home/static/img/home/9.png)
----
## 🥐 Architecture
@@ -52,8 +56,8 @@
## ⛄ Supported
> We define all metrics collection types such as `mysql`, `jvm`, and `k8s` as `YML` templates, allowing users to import them to support corresponding types of monitoring.
> Welcome everyone to contribute your customized general metrics type YML template during use.
> We define all monitoring collection types such as `mysql`, `jvm`, and `k8s` as `YML` monitoring templates, allowing users to import them to support corresponding types of monitoring.
> Welcome everyone to contribute your customized general monitoring type YML template during use.
- [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),
@@ -98,9 +102,6 @@
## 🐕 Quick Start
- Community alpha preview users can start from [docs/alpha-preview.md](docs/alpha-preview.md) for the current
Next.js frontend, entity-centered observability, local smoke, and known alpha limitations.
- If you wish to deploy HertzBeat locally, please refer to the following Deployment Documentation for instructions.
### 🍞 Install HertzBeat
@@ -158,9 +159,9 @@ Detailed config refer to [Install HertzBeat via Package](https://hertzbeat.apach
##### 3Start via source code
1. Local source code debugging needs to start the back-end project `hertzbeat-startup` and the front-end project `web-app`.
2. Backend: need `maven3+`, Java 25, and `lombok`; add VM options in IDE: ` --add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED `, then start the `hertzbeat-startup` service.
3. Web: need `Node.js 22` and `pnpm 10`, run `corepack pnpm@10.9.0 install --frozen-lockfile && corepack pnpm@10.9.0 dev` in `web-app` after backend startup.
1. Local source code debugging needs to start the back-end project `manager` and the front-end project `web-app`.
2. Backendneed `maven3+`, `java17`, `lombok`, add VM options in IDE: ` --add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED `, then start the `manager` service.
3. Webneed `nodejs npm angular-cli` environment, Run `ng serve --open` in `web-app` directory after backend startup.
4. Access `http://localhost:4200` to start, default account: `admin/hertzbeat`
Detailed steps refer to [CONTRIBUTING](CONTRIBUTING.md)
@@ -221,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>
@@ -235,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>
@@ -277,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>
@@ -293,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>
@@ -307,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>
@@ -358,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>
@@ -367,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>
@@ -381,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>
@@ -398,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>
@@ -428,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>
@@ -453,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>
@@ -482,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>
@@ -497,62 +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> <a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Code">💻</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>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KOYR"><img src="https://avatars.githubusercontent.com/u/53216619?v=4?s=100" width="100px;" alt="KOYR"/><br /><sub><b>KOYR</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=KOYR" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Lathika226"><img src="https://avatars.githubusercontent.com/u/178710568?v=4?s=100" width="100px;" alt="LathikaBaddam"/><br /><sub><b>LathikaBaddam</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Lathika226" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://shadwal.space/"><img src="https://avatars.githubusercontent.com/u/119167601?v=4?s=100" width="100px;" alt="Sahil Shadwal"/><br /><sub><b>Sahil Shadwal</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Sahil-Shadwal" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BhanuNidumolu"><img src="https://avatars.githubusercontent.com/u/180380413?v=4?s=100" width="100px;" alt="N.Bhanu Prasad"/><br /><sub><b>N.Bhanu Prasad</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=BhanuNidumolu" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://prakashh-portfolio.vercel.app/"><img src="https://avatars.githubusercontent.com/u/183058331?v=4?s=100" width="100px;" alt="Prakash Kumar"/><br /><sub><b>Prakash Kumar</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Prakash1185" title="Code">💻</a></td>
</tr>
</tbody>
</table>
@@ -574,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)
@@ -589,15 +543,15 @@ WeChat Public : Search ID `usthecom`.
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 Next.js React Tailwind CSS Node.js Npm ECharts G6 MonacoEditor Docusaurus ...`
- `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>
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
+62 -103
View File
@@ -4,14 +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,自定义监控和状态页构建能力。
[![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)
@@ -26,24 +27,29 @@
## 🎡 <font color="green">介绍</font>
[Apache HertzBeat](https://github.com/apache/hertzbeat) 是 AI 驱动的下一代开源实时观测系统。指标日志统一收集,告警一站分发,智能管控分析。无需 Agent,高性能集群,提供强大的自定义监控和状态页构建能力。
[Apache HertzBeat](https://github.com/apache/hertzbeat) (incubating)是一个易用友好的开源实时监控告警系统,无需 Agent,高性能集群,兼容 Prometheus,提供强大的自定义监控和状态页构建能力。
### 特点
-```采集+分析+告警+通知```为一体,HertzBeat AI 驱动下的新交互与功能,也内置 MCP Server 对外能力
- 统一的指标平台,无需 Agent,兼容 Prometheus,支持应用服务,程序,数据库,缓存,操作系统,大数据,中间件,Web 服务器,云原生,网络,自定义等。
- 统一的日志平台,通过 OTLP 协议多日志源无缝对接上报。
- 统一的告警平台,内部告警与外部多种告警源集成接入,统一告警处理分析,灵活的实时与周期阈值规则,分组收敛,静默,抑制等
- 统一的消息分发,告警平台处理后通过 `邮件` `Discord` `Slack` `Telegram` `钉钉` `微信` `飞书` `短信` `Webhook` `Server酱` 等方式分发通知。
-`Http, Jmx, Ssh, Snmp, Jdbc, Prometheus` 等协议规范可配置化,只需配置模板 `YML` 就能自定义采集指标。您相信只需简单配置即可快速适配一款 `K8s``Docker` 等新的监控类型吗?
- **监控+告警+通知** 为一体,支持对应用服务,应用程序,数据库,缓存,操作系统,大数据,中间件,Web 服务器,云原生,网络,自定义等监控阈值告警通知一步到位
- 易用友好,无需 `Agent`,全 `WEB` 页面操作,鼠标点一点就能监控告警,零上手学习成本。
- `Http, Jmx, Ssh, Snmp, Jdbc, Prometheus` 等协议规范可配置化,只需在浏览器配置监控模版 `YML` 就能使用这些协议去自定义采集想要的指标。您相信只需配置下就能立刻适配一款 `K8s``Docker` 等新的监控类型吗?
- 兼容 `Prometheus` 的系统生态并且更多,只需页面操作就可以监控 `Prometheus` 所能监控的
- 高性能,支持多采集器集群横向扩展,支持多隔离网络监控,云边协同。
- 自由的告警阈值规则,`邮件` `Discord` `Slack` `Telegram` `钉钉` `微信` `飞书` `短信` `Webhook` `Server酱` 等方式消息及时送达。
- 提供强大的状态页构建能力,轻松向用户传达您产品服务的实时状态。
> `HertzBeat`的统一平台,AI智能,强大自定义,多类型支持,高性能,易扩展,希望能帮助用户快速方便实现观测需求
> `HertzBeat`的强大自定义,多类型支持,高性能,易扩展,低耦合,希望能帮助用户快速搭建自有监控系统
----
![hertzbeat](home/static/img/home/1.png)
![hertzbeat](home/static/img/home/status.png)
![hertzbeat](home/static/img/home/9.png)
----
## 🥐 模块
@@ -52,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),
@@ -91,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酱`
## 🐕 快速开始
@@ -119,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。
@@ -152,9 +158,9 @@
##### 方式三:本地代码启动
1. 此为前后端分离项目,本地代码调试需要分别启动后端工程 `hertzbeat-startup` 和前端工程 `web-app`
2. 后端:需要 `maven3+`、Java 25 和 `lombok` 环境,修改 `YML` 配置信息,添加JVM参数`--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED`后启动 `hertzbeat-startup` 服务即可。
3. 前端:需要 `Node.js 22` 和 `pnpm 10` 环境,待本地后端启动后,在 `web-app` 目录下执行 `corepack pnpm@10.9.0 install --frozen-lockfile && corepack pnpm@10.9.0 dev`
1. 此为前后端分离项目,本地代码调试需要分别启动后端工程 `manager` 和前端工程 `web-app`
2. 后端:需要 `maven3+`, `java17` 和 `lombok` 环境,修改 `YML` 配置信息,添加JVM参数`--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)
@@ -215,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>
@@ -229,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>
@@ -271,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>
@@ -287,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>
@@ -301,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>
@@ -352,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>
@@ -361,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>
@@ -375,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>
@@ -392,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>
@@ -422,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>
@@ -447,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>
@@ -476,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>
@@ -491,62 +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> <a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Code">💻</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>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KOYR"><img src="https://avatars.githubusercontent.com/u/53216619?v=4?s=100" width="100px;" alt="KOYR"/><br /><sub><b>KOYR</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=KOYR" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Lathika226"><img src="https://avatars.githubusercontent.com/u/178710568?v=4?s=100" width="100px;" alt="LathikaBaddam"/><br /><sub><b>LathikaBaddam</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Lathika226" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://shadwal.space/"><img src="https://avatars.githubusercontent.com/u/119167601?v=4?s=100" width="100px;" alt="Sahil Shadwal"/><br /><sub><b>Sahil Shadwal</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Sahil-Shadwal" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BhanuNidumolu"><img src="https://avatars.githubusercontent.com/u/180380413?v=4?s=100" width="100px;" alt="N.Bhanu Prasad"/><br /><sub><b>N.Bhanu Prasad</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=BhanuNidumolu" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://prakashh-portfolio.vercel.app/"><img src="https://avatars.githubusercontent.com/u/183058331?v=4?s=100" width="100px;" alt="Prakash Kumar"/><br /><sub><b>Prakash Kumar</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Prakash1185" 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)
@@ -584,7 +543,7 @@ Thanks these wonderful people, welcome to join us:
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 Next.js React Tailwind CSS Node.js Npm ECharts G6 MonacoEditor Docusaurus ...`
- `TypeScript Angular NG-ZORRO NG-ALAIN NodeJs Npm Html Less Echarts Rxjs ZoneJs MonacoEditor SlickCarousel Docusaurus ...`
## Landscape
@@ -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
-603
View File
@@ -1,603 +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>
[![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) は AI 駆動の次世代オープンソースリアルタイム観測システムです。メトリクスとログの統一収集、アラートの一元配信、インテリジェント管理分析。エージェント不要、高性能クラスタ、強力なカスタム監視とステータスページ構築機能を提供します。
### 特性
- **収集+分析+アラート+通知**を一つのプラットフォームに統合し、HertzBeat AI 駆動の新しいインタラクションと機能、内蔵 MCP Server 機能を提供。
- 統一メトリクスプラットフォーム、エージェントレス、Prometheus互換、アプリケーションサービス、プログラム、データベース、キャッシュ、オペレーティングシステム、ビッグデータ、ミドルウェア、Webサーバー、クラウドネイティブ、ネットワーク、カスタム監視などをサポート。
- 統一ログプラットフォーム、OTLP プロトコルを通じて複数のログソースをシームレスに統合してレポート。
- 統一アラートプラットフォーム、内部アラートと様々な外部アラートソースを統合接続、統一アラート処理分析、柔軟なリアルタイムと周期的閾値ルール、グループ収束、サイレンス、抑制など。
- 統一メッセージ配信、アラートプラットフォームで処理後、`メール` `Discord` `Slack` `Telegram` `DingTalk` `WeChat` `FeiShu` `SMS` `Webhook` `ServerChan` などの方法で配信通知。
- `Http、Jmx、Ssh、Snmp、Jdbc、Prometheus`などのプロトコルを設定可能にし、テンプレート`YML`ファイルをオンラインで設定するだけで、あらゆるメトリクスを収集できるようにします。HertzBeatでオンライン設定するだけで、`K8s``Docker`のような新しい監視タイプに素早く対応できることを想像してみてください。
- 高性能で、コレクタークラスタの水平拡張、マルチ分離ネットワーク監視、クラウドエッジ協調をサポート。
- 強力なステータスページ構築機能を提供し、サービスのリアルタイムステータスをユーザーに簡単に伝達。
> `HertzBeat`の統一プラットフォーム、AI インテリジェンス、強力なカスタマイズ、多種類サポート、高性能、容易な拡張性は、ユーザーが迅速かつ便利に観測要件を実現することを支援することを目的としています。
----
----
## 🥐 モジュール
![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. ローカルの実行には、バックエンドのプロジェクト`hertzbeat-startup`とフロントエンドのプロジェクト`web-app`を起動する必要があります。
2. バックエンド:`maven3+`、Java 25、`lombok`の環境が必要です。`YML` 設定を修正し、Java仮想マシンパラメータに`--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED` を追加し、`hertzbeat-startup` を起動します。
3. フロントエンド:`Node.js 22` と `pnpm 10` の環境が必要です。ローカルのバックエンドが立ち上がったら、`web-app` ディレクトリで `corepack pnpm@10.9.0 install --frozen-lockfile && corepack pnpm@10.9.0 dev` を実行します。
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> <a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Code">💻</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>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KOYR"><img src="https://avatars.githubusercontent.com/u/53216619?v=4?s=100" width="100px;" alt="KOYR"/><br /><sub><b>KOYR</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=KOYR" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Lathika226"><img src="https://avatars.githubusercontent.com/u/178710568?v=4?s=100" width="100px;" alt="LathikaBaddam"/><br /><sub><b>LathikaBaddam</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Lathika226" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://shadwal.space/"><img src="https://avatars.githubusercontent.com/u/119167601?v=4?s=100" width="100px;" alt="Sahil Shadwal"/><br /><sub><b>Sahil Shadwal</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Sahil-Shadwal" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BhanuNidumolu"><img src="https://avatars.githubusercontent.com/u/180380413?v=4?s=100" width="100px;" alt="N.Bhanu Prasad"/><br /><sub><b>N.Bhanu Prasad</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=BhanuNidumolu" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://prakashh-portfolio.vercel.app/"><img src="https://avatars.githubusercontent.com/u/183058331?v=4?s=100" width="100px;" alt="Prakash Kumar"/><br /><sub><b>Prakash Kumar</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Prakash1185" 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 Next.js React Tailwind CSS Node.js Npm ECharts G6 MonacoEditor 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/)
-176
View File
@@ -1,176 +0,0 @@
# HertzBeat Community Alpha Preview
This document is for the GitHub community alpha preview branch. It is not a
stable Apache HertzBeat release checklist. Use it to try the new entity-centered
observability and Vite React frontend, report issues, and help harden the project
toward a beta.
## What Is In Scope
- Entity-centered observability read models for metrics, logs, traces, topology,
alerts, owners, runbooks, and handoff context.
- Vite React `web-app` workflows for the operational shell, entity management,
topology, instrumentation, and the three-signal query workspace.
- OTLP trace/log/metric query paths backed by the existing warehouse and
Greptime integrations.
- Local demo topology relation seeding for the small Checkout API -> Payment API
-> Orders DB example when the `local` Spring profile is active.
## Local Source Quickstart
1. Start the backend from `hertzbeat-startup` with Java 25.
Keep the Arrow JVM open option when running locally:
```shell
--add-opens=java.base/java.nio=ALL-UNNAMED
```
The local backend listens on `http://127.0.0.1:1157` by default.
2. Start the Vite React frontend:
```shell
cd web-app
corepack pnpm@10.9.0 install --frozen-lockfile
corepack pnpm@10.9.0 dev
```
The preview frontend listens on `http://127.0.0.1:4200`.
3. Open `http://127.0.0.1:4200` and sign in with the local development account
configured for your environment.
## Alpha Validation Checklist
Before opening an alpha issue or pull request, run the smallest check that
matches the area you changed.
Backend entity, topology, observability, and Greptime query changes:
```shell
./mvnw -pl hertzbeat-manager,hertzbeat-observability,hertzbeat-warehouse \
-Dtest=EntityDetailObservabilityReadModelServiceTest,EntityTopologyQueryServiceTest,EntityWorkspaceAccessServiceTest,EntityWorkspaceQueryServiceTest,TraceCallTopologyQueryServiceTest,LocalTopologyDemoRelationSeederTest,LogQueryControllerTest,EntityObservabilityGatewayImplTest,EntityTraceQueryServiceImplTest,GreptimeTraceQueryRepositoryTest,GreptimeDbDataStorageTest \
test -DskipITs -Dsurefire.failIfNoSpecifiedTests=false -DfailIfNoTests=false
```
Startup source package proof:
```shell
./mvnw -pl hertzbeat-startup -am -DskipTests package
```
Topology, entity, observability, shell, session, and shared UI changes:
```shell
cd web-app
corepack pnpm@10.9.0 test -- \
src/features/entity src/features/topology src/features/instrumentation \
src/features/explore
```
Complete frontend release gate:
```shell
cd web-app
corepack pnpm@10.9.0 verify
```
Visible changes also require a production build, a real backend, and a Browser
check of the affected workflow. Do not use mocked topology or signal data as
release evidence.
## Three-Signal SigNoz-Alignment Alpha Cutoff
HertzBeat 2.0 treats SigNoz as a product-shape reference for the alpha
observability workspace, not as a full parity claim. The alpha cutoff is the
operator workflow where metrics, logs, and traces can stay attached to the same
HertzBeat entity context, be saved as reusable views, be composed into
dashboards, and drill from service or operation metrics into related traces and
logs.
This alpha scope includes:
- OTLP metrics, logs, and traces carrying HertzBeat entity context through
`entityId`, `entityType`, `entityName`, service, namespace, environment,
collector, template, and source query parameters.
- Saved query views, dashboard panel drafts, dashboard variables, and persisted
dashboards for metrics, logs, and traces.
- A service overview dashboard with RED-style metrics, Apdex, database calls,
external calls, key operations, logs, log errors, traces, trace errors,
exceptions, exception messages, and firing alerts.
- An operation drilldown dashboard from `operationName` context, with metrics
filtered by `operation`, logs filtered by `http.route`, traces filtered by
`operationName`, and exceptions grouped by `exception.type`.
- Runtime dashboard evidence flows from metric/log/trace points into related
signal handoffs, evidence panel drafts, and breakout panel drafts.
- Stable entity binding from OpenTelemetry resource identity such as
`service.name`, host, and Kubernetes pod attributes.
This alpha scope does not claim full SigNoz parity. In particular, it does not
claim public dashboard sharing, Terraform-managed dashboards, a prebuilt
dashboard template marketplace, a ClickHouse SQL dashboard builder, a log
pipeline builder, cost-meter dashboards, full flamegraph parity, or automatic
APM RED metric derivation from traces beyond the explicit telemetry/query data
seeded or stored in the current HertzBeat runtime.
Runtime signal dimensions such as `trace_id`, `span.name`, `http.route`,
`operation`, `operationName`, `exception.type`, and `exception.message` are
filter, group-by, handoff, and drilldown dimensions. They must not be promoted
into long-lived `ObserveEntity` identities unless a later design explicitly
adds an endpoint or operation entity model.
The focused live proof for this cutoff is:
```shell
TRACE_ID=6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b6b bash script/dev/run-three-signal-live-proof.sh
```
That proof starts a non-persistent H2 backend, seeds OTLP metrics/logs/traces,
verifies entity binding and signal query breakouts, starts `web-app`, and checks
that the active React routes are served. Product-level browser acceptance remains
a separate real-backend validation step.
## Local Scale Proof Data
Large topology scale proof data is not seeded by default. The local demo seeder
only creates the small demo relation repair during normal `local` profile
startup.
To explicitly seed the local mixed scale proof entity catalog, start the backend
with:
```shell
--hertzbeat.topology.local-scale-proof-seed=true
```
This is intended for local performance investigation only. Do not treat it as
required alpha setup.
## Known Alpha Limitations
- The Vite React frontend is still an alpha preview. Parity claims require
route, action, API read/write, refresh, context-handoff, and Browser evidence.
- Three-signal work is scoped to the alpha cutoff above. Do not describe it as
full SigNoz parity unless a later release adds and proves the omitted product
capabilities.
- Topology large-graph behavior is optimized for inspection with render
windows, table drilldown, and optional browser smoke. Continue filing cases
where real data feels confusing or slow.
- Runtime verification may depend on local H2 or Greptime data shape. Greptime
scale proof fixtures are local-only and should not be committed as generated
proof artifacts.
- Source package rebuilds are expected to pass for the alpha candidate. If a
local module or dependency warning blocks packaging, include the focused
Maven command and output with the issue report.
## Reporting Issues
When filing alpha feedback, include:
- The route or API endpoint.
- The storage mode, for example H2-only or Greptime-backed traces.
- The browser viewport and whether the in-app refresh was used.
- Expected versus actual node, edge, table, or handoff behavior.
- Any focused test, Maven, Vitest, ESLint, or browser-smoke command that
reproduces the issue.
@@ -1,340 +0,0 @@
# HertzBeat Hybrid Collector 完整交付计划
> 状态:执行基线
> 最近更新:2026-07-15
> 实施分支:`codex/hybrid-collector-phase0`
> 实施工作树:`/Users/zhaoqingran/IdeaProjects/hertzbeat-hybrid-collector`
## 1. 计划用途
本文件是 Hybrid Collector 后续实现、评审和验收的唯一范围基线。后续会话不得依赖聊天记忆决定范围:
1. 每次只推进一个里程碑;
2. 开始前先读取本文件和 `progress.md`
3. 新增或删除能力必须先修改本文件并说明原因;
4. 验证失败时停在当前里程碑修复,不带失败进入下一项;
5. 所有里程碑和最终验收全部通过后,才能声明 Hybrid Collector 完成。
本计划已经吸收 2026-07-10 Hybrid Collector 架构评审的结论和架构红线,不重新发明另一套采集器。
## 2. 最终产品定义
Hybrid Collector 必须同时具备两类能力:
- **主动采集**:采集主机指标、Prometheus 指标、批准的本地日志,并通过随包提供的官方自动埋点采集 Java 应用链路和 JVM 指标;
- **接收转发**:通过标准 OTLP/HTTP 和 OTLP/gRPC 接收应用或其他 OTel SDK/Agent 的 Metrics、Logs、Traces,经过保护和补充后直接发送 HertzBeat Server。
对用户保持以下产品边界:
- 一个安装包;
- 一个启动入口和系统服务;
- 一个 Collector ID
- Java Collector 是唯一注册、配置和生命周期主控;
- Go OTel Runtime 不作为第二个 Collector 注册;
- Go Runtime 直接向 HertzBeat Server 发送遥测,Java 不代理数据;
- 不要求用户另装第三方 OpenTelemetry Collector
- 默认配置可工作,高级能力通过受控语义配置开启,不允许远程下发任意 YAML。
## 3. 完成后的固定组件清单
使用 OCB 构建裁剪后的官方组件集合。除下表外,不增加组件。
### 3.1 Receiver
| 组件 | 最终用途 | 状态 |
| --- | --- | --- |
| `otlpreceiver` | OTLP HTTP/gRPC 接收 Metrics、Logs、Traces | 已实现 |
| `hostmetricsreceiver` | 主动采集 CPU、内存、磁盘、文件系统、网络、负载、进程指标 | 已实现 |
| `prometheusreceiver` | 主动抓取受管 Prometheus/OpenMetrics 端点 | 已实现 |
| `filelogreceiver` | 主动采集本地策略批准的日志文件并处理轮转 | 已实现 |
### 3.2 Processor
| 组件 | 最终用途 | 状态 |
| --- | --- | --- |
| `memorylimiterprocessor` | 有界内存和快速拒绝 | 已实现 |
| `batchprocessor` | 三信号有界批处理 | 已实现 |
| `resourceprocessor` | 写入 HertzBeat Collector/环境等固定资源属性 | 已实现 |
| `resourcedetectionprocessor` | 检测 host、OS、process、container 和获准的云环境资源 | 已实现 |
| `attributesprocessor` | 删除禁止属性并补充固定低基数属性 | 已实现 |
| `filterprocessor` | 使用产品预置规则过滤健康探测等明确噪声;不开放任意远程 OTTL | 已实现 |
### 3.3 Exporter、Extension 和 Provider
| 组件 | 最终用途 | 状态 |
| --- | --- | --- |
| `otlphttpexporter` | 直接发送 HertzBeat OTLP HTTP 接口 | 已实现 |
| `filestorage` | 日志 offset 和三信号持久发送队列 | 已实现 |
| `healthcheckextension` | Java Supervisor 的本地健康判断 | 已实现 |
| `bearertokenauthextension` | 非 loopback OTLP Gateway 模式的入站认证 | 已实现 |
| `envprovider` / `fileprovider` / `yamlprovider` | 读取 Java 生成配置和环境 secret | 已实现 |
### 3.4 自动埋点
自动埋点不是 Collector Contrib Receiver,但属于完整交付范围:
| 能力 | 实现方式 | 状态 |
| --- | --- | --- |
| Java 应用 Trace | 随包提供官方 OpenTelemetry Java Agent | 待实现 |
| JVM Runtime Metrics | 官方 Java Agent runtime instrumentation | 待实现 |
| 日志 Trace/Span 关联 | Java Agent 注入日志上下文,日志由 File Log 或 OTLP Logs 进入 | 待实现 |
| 其他语言 | 提供标准本地 OTLP 地址、Header 和 Resource 配置,不捆绑其他语言 Agent | 待实现文档和 smoke |
不得通过伪造 Span 或把普通日志推断成 Trace 来满足“三信号”。链路必须来自真实应用执行或标准 OTLP 上游。
## 4. 明确不在本次交付范围
以下能力不进入本计划,也不能在实现过程中顺手加入:
- 完整 Contrib Distribution
- 任意 Raw OpenTelemetry YAML
- 自研 OTLP Receiver、Exporter、队列、存储或日志 tail
- Java 中转遥测数据;
- JNI、JNA、cgo 或 fork OpenTelemetry Collector Core
- 自动扫描并注入任意本机进程;
- 捆绑 Python、Node.js、.NET 和 Go 自动埋点发行物;
- 边缘 Tail Sampling、Span Metrics 和 Service Graph;这些需要全局 Trace 视角,属于 HertzBeat Server
- Kubernetes 全家桶、Docker Stats、云厂商、数据库和中间件 Contrib Receiver;现有 HertzBeat Java Agentless 能力继续负责这些目标,独立需求另行评审;
- OpAMPManager 到 Java Collector 的现有控制通道继续作为唯一控制面;
- 自定义 Go control extension、热重载和 Runtime 在线升级;
- pprof、zPages 等默认生产诊断端口;
- 远程下发任意绝对日志路径、明文 Token 或证书私钥。
## 5. 固定里程碑
### M1:服务端 Desired Config 和安全切换
状态:**已完成**`353aeadcd`
交付内容:
- Server 持久化版本化 `ManagedOtelRuntimeConfig`
- 复用现有 Collector 心跳响应下发,不增加新协议和消息类型;
- Collector 只接受严格递增 revision
- Java 渲染与随包 Runtime 版本匹配的 YAML
- 新配置先生成 candidate、实际 Runtime validate,再原子激活;
- validate 失败不停止当前 Runtime
- readiness 失败只回滚一次 last-known-good
- 配置存储或解析失败不影响普通 Collector 心跳和 Agentless 任务;
- 管理 API 支持查询和更新语义配置;
- H2、MySQL、PostgreSQL 迁移齐全。
完成门禁:
- Manager 配置服务、API、心跳新旧版本兼容测试;
- Collector 下发解析、revision、candidate/LKG 和进程切换测试;
- Java 25 Checkstyle、Spring Boot 4 AOT
- `git diff --check`
### M2:资源识别、归属和固定数据治理
状态:**已完成**`652f2a836`
交付内容:
- 加入 `resourcedetectionprocessor``attributesprocessor``filterprocessor`
- 检测 host、OS、process、container;云环境 detector 仅在显式启用时运行;
- 统一三信号上的 Collector ID、runtime version、service/environment 属性;
- 明确用户属性、检测属性和 HertzBeat 固定属性的覆盖顺序;
- 过滤规则使用产品枚举,不接受任意 OTTL;
- 删除认证 Header、Token、cookie 等禁止属性;
- 高基数属性不自动提升为资源维度;
- 三信号保持相同的资源归属语义。
完成门禁:
- 三信号资源属性一致性测试;
- 属性覆盖、脱敏、过滤和错误配置测试;
- OCB validate 和真实 Runtime export smoke
- 资源识别关闭时无额外云 metadata 请求。
### M3:主动指标与日志采集闭环
状态:**已完成**`f27b2f184`
交付内容:
- Host Metrics 支持采集间隔和 scraper 白名单;
- Prometheus 支持最多 32 个受管端点、HTTPS、抓取间隔、超时和静态 Header secret 引用;
- File Log 只使用本机管理员定义的 path profile
- realpath、符号链接逃逸、deny path、glob 数、文件数、单行大小和读取速率均有上限;
- 日志默认 `start_at: end`,轮转和 copytruncate 行为明确;
- 文件 offset 和 exporter queue 共用 owner-only storage
- Manager 展示 source 的 desired、active、rejected 状态及最后错误;
- 删除或修改 source 后不会留下失效 pipeline。
完成门禁:
- Linux/macOS/Windows 路径策略单元测试;
- Prometheus 超时、TLS、错误响应和高基数样本测试;
- File Log 轮转、截断、重启 offset、拒绝敏感路径测试;
- Host Metrics、Prometheus、File Log 同时运行的真实 Runtime 测试。
### M4:安全 OTLP 接收和 Gateway 模式
状态:**已完成**`323f34b83`,许可证制品修复 `4eab2fa3e`
交付内容:
- Agent 模式默认只监听 `127.0.0.1:4317/4318`
- HTTP/gRPC 三信号、Protobuf/JSON 协议覆盖;
- 单请求大小、并发、队列和超时有界;
- Gateway 模式必须显式开启,非 loopback 监听必须配置 TLS
- Gateway 模式加入官方 Bearer Token Auth,可选 mTLS
- 认证 secret 只通过环境或本地 secret 文件传递;
- 认证失败、非法 payload、超限分别返回明确协议错误;
- 入站流量与主动采集共享总资源预算,不能互相饿死。
完成门禁:
- OTLP HTTP JSON/Protobuf 和 gRPC 三信号真实客户端测试;
- 无鉴权、错误 Token、过期/轮换 Token、TLS 和 mTLS 测试;
- 4 MiB 边界、并发拒绝和停止接收时的优雅排空测试;
- 默认配置确认没有公网监听。
### M5:随包 Java 自动埋点
状态:**进行中**
交付内容:
- 固定并审核官方 OpenTelemetry Java Agent 版本;
- Agent 作为独立可选制品放入安装包,不进入 Native Java Collector 可达闭包;
- 提供生成器输出 `-javaagent`、OTLP endpoint、protocol、resource attributes 和日志关联参数;
- Agent 默认发送到本机 Hybrid Collector,不直接绕过 Collector
- 支持用户显式启用/禁用 instrumentation
- 不自动修改用户启动脚本,不 attach 任意运行中 JVM
- 提供 Spring Boot、普通 JAR 和容器三种可复制启动方式;
- File Log 与 Agent 注入的 trace/span 字段能够关联;
- Agent 缺失、版本不兼容或端口不可用时给出明确诊断。
完成门禁:
- 一个真实 Java 示例应用产生 HTTP、数据库、异常和异步 Span;
- 同一应用产生 JVM 指标、关联日志和 Trace;
- HertzBeat Server 可查询三信号并按 traceId 跳转;
- Agent 关闭时应用行为不受 Collector 影响;
- Agent LICENSE、NOTICE、SBOM 和漏洞扫描通过。
### M6:运行状态、内部遥测和故障诊断
状态:**待实现**
交付内容:
- Java 心跳报告 desired revision、active revision、进程状态、PID、重启次数和最后错误;
- Go Runtime internal telemetry 直接上报 HertzBeat Server
- 展示 accepted、refused、sent、failed、queue size/capacity 和 file consumer 状态;
- 区分配置错误、端口冲突、后端不可用、认证失败、队列满、进程崩溃;
- 后端不可用只触发 exporter retry/queue,不重启 Runtime
- Java Agentless 采集在 Go Runtime 降级或失败时继续工作;
- 日志中不打印 Token、Authorization、完整用户日志正文或证书内容。
完成门禁:
- 每类故障的状态收敛测试;
- 心跳负载和 Server 查询不会携带遥测正文;
- Runtime crash/restart circuit 和人工恢复测试;
- 缺数据、不可用和真实零值能够区分。
### M7:容量、背压和长期故障恢复
状态:**待实现**
交付内容:
- 三信号共享内存上限和独立 pipeline 可观测性;
- exporter 磁盘队列固定容量、owner-only 权限和明确满盘行为;
- HertzBeat/Greptime 暂停后不丢已确认进入持久队列的数据;
- Runtime 被强杀、Java 被重启、机器重启后均能恢复队列和日志 offset;
- 无无限重试线程、无限队列或后台泄漏;
- 停止压力后自动恢复,不要求重启 Greptime 或 Collector
- 记录真实 CPU、RSS、磁盘、吞吐和延迟,不伪造容量结论。
完成门禁:
- Metrics、Logs、Traces 独立和混合负载;
- 后端断开、恢复、慢响应、429、503 和连接重置;
- 队列满、磁盘满和损坏存储;
- 24 小时 soak
- 停压恢复与无残留长任务证明。
### M8Java Native、多平台发行和真实端到端验收
状态:**待最终复验;基础打包已实现**
交付内容:
- Java 25 + Spring Boot 4 Collector
- macOS/Linux arm64/amd64、Windows amd64 Native Java 制品;
- 对应平台的 no-CGO Go Runtime
- Java Agent 作为可选独立文件随包发布;
- 单服务启动、停止、信号转发、子进程清理和崩溃恢复;
- Docker Native 镜像非 root 运行且无 JVM
- 组件 inventory、LICENSE、NOTICE、SBOM、漏洞报告、校验和与可复现构建;
- 真实 HertzBeat Server + GreptimeDB 完成用户闭环。
最终浏览器/API 闭环:
1. 注册一个 Collector
2. 配置主机指标、Prometheus 和日志 profile
3. 生成 Java Agent 启动参数并启动真实应用;
4. 查询主动采集指标和日志;
5. 查询应用 Trace、Span 和事件;
6. 日志按 traceId 跳转 Trace
7. Trace 查看同时间窗口日志和指标;
8. 断开 HertzBeat/Greptime 后恢复,积压自动发送;
9. 强杀 Go RuntimeJava Agentless 不停且 Go 自动恢复;
10. 重启整个 Collectoroffset、队列、配置 revision 和身份保持正确。
最终发布门禁:
- 全部聚焦单测、集成测试和真实 Runtime 测试;
- Java 25 Checkstyle、Spring Boot 4 AOT、Native Image
- 五个平台布局与启动合同;
- OCB inventory、license、SBOM、`govulncheck`、校验和和可复现构建;
- HertzBeat/Greptime 三信号真实写入和查询;
- 24 小时 soak 和故障恢复;
- `git diff --check`、无硬编码 CJK、无本地产物进入提交;
- 人工代码评审确认没有重复抽象、自研替代官方组件或未使用代码。
## 6. 实施顺序和停止规则
固定顺序为 `M1 → M2 → M3 → M4 → M5 → M6 → M7 → M8`,不得跳过失败门禁并继续。
每个里程碑执行循环:
1. 写最小失败合同;
2. 做满足合同的最小实现;
3. 运行聚焦验证;
4. 修复全部失败;
5. 运行该里程碑的真实 Runtime 或端到端证明;
6. 更新本文件状态和 `progress.md`
7. 只提交本里程碑相关源码、测试和文档;
8. 进入下一里程碑。
只有以下情况可以暂停并询问维护者:
- 需要改变本文件的固定交付范围;
- 需要新增官方组件或自研组件;
- 出现许可证、安全、跨平台或后端契约阻塞;
- 需要外部 CI、签名或发布权限;
- 真实数据证明原验收指标不可实现,需要维护者决定取舍。
## 7. 完成定义
以下条件必须同时成立:
- 主动采集主机指标、Prometheus 指标和批准的文件日志;
- 随包 Java Agent 从真实应用采集 Trace、JVM 指标并关联日志;
- 安全接收并转发外部 OTLP 三信号;
- 三信号拥有一致资源身份和 HertzBeat Collector 归属;
- Server Desired Config 能安全下发、验证、切换和回滚;
- 后端中断、进程崩溃和机器重启后能够恢复;
- Java Agentless 采集不依赖 Go Runtime 健康;
- JVM 和 Native Collector 均可运行,发布包覆盖目标平台;
- 真实 HertzBeat + Greptime 完成三信号查询与跨信号跳转;
- 所有 M1 到 M8 门禁通过;
- 代码通过人工可维护性评审,无未使用实现和临时兼容层。
未同时满足上述条件时,不得使用“Hybrid Collector 已完成”或“可正常替代 OTel Collector”作为结论。
-46
View File
@@ -1,46 +0,0 @@
# Application Instrumentation API
HertzBeat exposes one public application-instrumentation route family. The
paths are unversioned; internal implementation package names are not alternate
API surfaces. Every response uses the ordinary HertzBeat `Message<T>` envelope.
## Public endpoints
| Method | Path | Purpose |
| --- | --- | --- |
| `GET` | `/api/instrumentation/catalog` | Discover source groups, sources, and selectable recipes |
| `GET` | `/api/instrumentation/intake-profiles` | Discover non-secret OTLP intake destinations and transport requirements |
| `POST` | `/api/instrumentation/render` | Render structured onboarding blocks for a recipe and intake profile |
| `POST` | `/api/instrumentation/detect` | Detect scoped Metrics, Logs, and Traces reception |
There are no public path-version aliases. Clients must not prepend a version
segment or construct paths from implementation class or package names.
## Authorization
Catalog and intake-profile discovery are available to `admin`, `user`, and
`guest` roles. Rendering and detection require `admin` or `user`. Shipped
Sureness configurations enumerate the four paths explicitly so authorization
does not imply a second route family.
## Wire compatibility
The existing `schemaVersion` field remains part of the request and response
payloads. It is a payload compatibility guard, not a URL version. This contract
does not add, remove, or rename any wire field or enum value.
- Catalog data contains `schemaVersion`, `groups`, `sources`, and `recipes`.
- Intake-profile discovery returns explicit non-secret destinations,
transports, authentication requirements, and availability state.
- Render requests select a source or recipe, environment, platform, intake
profile, and service identity. Responses contain structured blocks and
declared secret placeholders; no token value is returned.
- Detection requests carry the same selection and identity scope plus
`startedAt`. Responses distinguish `waiting`, `received`, `unsupported`,
`unavailable`, and `error`, and return bounded polling and typed query-jump
context.
Clients should consume server-provided catalog choices and intake profiles
rather than inventing endpoints, package coordinates, health states, or signal
support. Unknown or unavailable data must remain distinguishable from healthy
or zero-valued data.
-49
View File
@@ -1,49 +0,0 @@
# Observability Query Context v1
This document freezes the optional query context shared by the Metrics, Logs, and Traces operator
APIs. It is separate from the application instrumentation catalog, render, and detection contract;
that contract remains unchanged.
## Context hierarchy
Queries narrow data in this order:
`Collector -> service.name -> service.namespace -> deployment.environment.name -> instance -> endpoint`
Each supplied dimension is an exact additional constraint. A row, series, log record, or span that
does not carry a supplied dimension does not match. An implementation must return an honest empty
result instead of falling back to the broader service context.
## Frozen fields
| Query field | Meaning | Metrics | Logs | Traces |
| --- | --- | --- | --- | --- |
| `instance` | OTel Resource `service.instance.id` | data-point label `service_instance_id` | Resource `service.instance.id` | Resource `service.instance.id` |
| `endpoint` | Low-cardinality HTTP route template | data-point label `http_route` | LogRecord attribute `http.route` | Span attribute `http.route` |
`endpoint` is a route template such as `/checkout`. It excludes the HTTP method, query string,
fragment, and concrete URL. Values such as `POST /checkout` and `/checkout?order=42` are invalid.
RPC method, messaging destination, and database operation meanings are not overloaded into this
field. Supporting those protocols requires new versioned fields.
The dedicated fields cannot compete with the same key in a free-form resource, attribute, or metric
filter. Such a request is rejected instead of silently applying ambiguous constraints.
## API coverage
- Metrics console request and returned context.
- Historical log list, surrounding context, overview, trace coverage, trend, and group-by queries.
- Live log SSE filtering.
- Trace list, overview, group-by, detail, and span queries. Detail and span requests retain the
exact trace identifier while also applying the supplied discovery context; an optional `spanId`
further narrows the row set.
Storage access remains behind the existing query services and storage adapters. Controllers only
bind the HTTP parameters and construct the storage-neutral context; they contain no database query
language or storage-specific SQL.
## Compatibility
These fields are optional additions to the existing query APIs. Omitting both retains the prior
behavior. Changing either field's semantic mapping, accepting an HTTP method in `endpoint`, or
reusing `endpoint` for a non-HTTP protocol requires a new contract version.
-215
View File
@@ -1,215 +0,0 @@
# Product Design User Validation Loop
This runbook makes Product Design part of the HertzBeat 2.0 user-layer
validation loop. It is not a screenshot-only audit pass and it is not a visual
polish layer. Use it to close product experience loops around real operator
workflows, honest state feedback, data lifecycle clarity, and repeatable
actions.
## Product Context
Start every Product Design workflow from saved context:
- Product Design state: `$CODEX_HOME/state/plugins/product-design/user-context.md`
- Local screenshot/reference assets:
`$CODEX_HOME/state/plugins/product-design/assets/`
- Repo guidance: `AGENTS.md` and the latest active slice in `progress.md`
- Frontend implementation: `web-app/src/app`, `web-app/src/layout`,
`web-app/src/features`, and `web-app/src/shared`
- Theme and UI constraints: `web-app/src/app/theme`, feature CSS modules,
shared Ant Design primitives, source-backed icon packages, and runtime i18n
catalogs
Saved context should include the HertzBeat product position, operator audience,
competitive references, design preferences, code paths, screenshots, design
system constraints, and mainstream observability console expectations. Future
audits, ideation, prototypes, and implementation work must prefer that context
unless the user explicitly asks for a different direction.
## Required Workflow Selection
Use the smallest workflow that matches the problem.
Small issue:
```text
audit -> small fix -> focused test -> Browser verification -> progress.md
```
Use this for a concrete user-visible problem in one flow, such as confusing
error copy, an unclear button, an empty state that hides the data lifecycle, a
broken cancel path, or a refresh/status mismatch.
Systemic issue:
```text
user-context/get-context -> research -> audit -> ideate -> prototype -> design-qa -> small implementation slice -> progress.md
```
Use this when the issue exposes a broader product pattern, such as monitor
authoring confusion, entity lifecycle ambiguity, alert rule authoring friction,
or settings forms that repeatedly fail to explain required state and side
effects.
Do not skip `audit` or `research` before broad UI changes. Do not use Product
Design to decorate an existing screen while leaving the real workflow,
state feedback, error explanation, or data lifecycle unresolved.
## Design Brief Gate
Before any design judgment, experience redesign, prototype exploration, or UI
implementation, confirm a brief through Product Design `get-context`.
The brief must identify:
- Target user and operating context.
- Workflow problem being solved.
- Existing route, screenshot, design system, or component reference.
- Required interaction fidelity.
- Backend/data lifecycle states that must stay honest.
- Acceptance evidence expected before implementation is considered done.
If the brief is missing or under-specified, stop and ask for only the missing
context. Do not edit UI from an unconfirmed brief.
## Research Gate
For systemic problems, perform product and user research before designing a
direction. Cover:
- Operator mental model and repeated-use pressure.
- Comparable observability platform patterns, especially Grafana, Datadog,
SigNoz, and OpenTelemetry ecosystem consoles where relevant.
- State, error, loading, empty, and recovery language.
- Help affordance density and progressive disclosure.
- Safe authoring, save/cancel, detect/test, refresh, and status convergence.
Prioritize monitor, entity, alert, and settings workflows because these are
high-frequency operator surfaces.
## Real Flow Audit
Audits must use current product evidence:
- Production frontend build.
- Real HertzBeat backend, using H2/Greptime/warehouse data honestly.
- In-app Browser screenshots for the current route state.
- Step notes tied to every screenshot and finding.
- Theme preflight before accepting screenshots: record `html[data-theme]`,
`body[data-theme]`, `--background`, `--card`, and `--ops-background`.
HertzBeat's default audit baseline is `dark-ops` (`--background: 220 11%
5%`, `--ops-background: #0b0c0e`). If an existing Browser origin has
persisted `light-ops`, do not use those screenshots as dark-baseline
evidence; use an isolated localhost port/origin or explicitly document that
the slice is auditing light theme.
Audit at least the relevant parts of:
- Login and guarded deep-link return.
- Navigation and retained query/context parameters.
- Create, edit, save, cancel, detect/test, refresh, and route revisit.
- Status convergence without browser reload when the UI promises refresh.
- Empty, loading, unavailable, failed, and recovered states.
- Help question marks, button labels, destructive confirmations, and action
meaning.
- Card hierarchy, table density, form grouping, text overflow, responsive
behavior, keyboard/focus semantics, and visible accessibility risks.
Findings must cite screenshot names and step numbers. Do not evaluate from
memory.
## Ideation Gate
When audit and research show a systemic UX problem, do not immediately rewrite
product code. Generate two or three different directions first. Useful default
directions for HertzBeat include:
- Dense operations console: fastest scanning and repeat actions.
- Diagnosis-first evidence chain: status, raw backend reason, metric evidence,
history availability, and recovery steps.
- Guided authoring: lower misoperation risk for complex create/edit/test flows.
Choose a direction before prototyping or implementing.
## Prototype Gate
For high-impact flows, build a local interactive prototype before production
implementation. Prototype only the smallest slice needed to validate interaction
and information architecture. Good candidates include:
- Monitor create, detect, detail refresh, and history chart availability.
- Alert rule authoring, preview/test, save/cancel, and template selection.
- Entity create/edit/detail and definition import/edit handoff.
- Settings forms with required fields, disabled save, cancel, and no-write
guarantees.
The prototype is evidence for product decisions. It does not replace
production code, tests, or Browser verification.
## Implementation Gate
Use Product Design `image-to-code` or production UI implementation only after
there is a confirmed brief plus a concrete visual or interaction target:
screenshot, selected visual direction, prototype, Figma frame, or mockup.
Implementation must preserve:
- Existing HertzBeat route/controller/view-model patterns.
- Shared `@hertzbeat/ui` primitives and source-backed icons.
- Dense operational layouts and stable dimensions.
- Clear data lifecycle and backend-state honesty.
- Runtime i18n rules and no hardcoded localized UI copy outside approved
locale resources.
Do not introduce marketing-page composition, decorative chrome, fake zero
states, fake healthy states, broad refactors, or untested UI rewrites.
## Design QA Gate
After implementation, run design QA against the target evidence:
- Original audit screenshot(s).
- Selected direction or prototype.
- Actual production page rendered through the in-app Browser.
- Focused automated tests and the smallest relevant Browser proof.
Check layout, hierarchy, density, text overflow, responsive behavior, control
states, focus/keyboard semantics, empty/error/loading states, and visible
accessibility risks. P0, P1, and P2 design issues must be fixed before the
slice is treated as passing.
## Artifact And Progress Policy
Persist Product Design evidence locally. Use clear names that can be understood
without opening the file.
Recommended local artifact layout:
```text
.tmp/product-design/
<slice-id>/
screenshots/
audit.md
research.md
directions.md
prototype-notes.md
design-qa.md
```
Do not commit `.tmp/`, screenshots, reports, runtime logs, local proof data, or
generated artifacts unless maintainers explicitly ask. The active slice in
`progress.md` must record:
- Saved Product Design context used.
- Research sources or research scope.
- Audit route, backend, screenshots, and steps.
- Theme preflight evidence and whether the screenshot baseline is `dark-ops`
or an intentional alternate theme.
- Findings by severity and user impact.
- Selected direction and prototype target, when applicable.
- Implementation files changed.
- Focused tests and Browser verification.
- Remaining risks and next slice.
The release gate is not complete until Product Design evidence supports the
actual operator workflow, not just a single screenshot or isolated component.
-184
View File
@@ -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.
-->
# Lightweight React Frontend Architecture
## Status And Reference Baselines
This document is the implementation contract for the maintainable React
single-page application that replaces the deprecated Next.js frontend.
- Implementation baseline: `2.0.0` at `2de48d2caa43fa58fd93e71b0e22e7d4f431c9d5`.
- Angular behavior and release reference: `apache/master` at
`9bd2796119b558cff2834dd35e3d21f2eb4a53a4`.
- Transitional signal reference: `feature/angular-three-signals` at
`5d523ddce322ad82ab1bcb65b3dbe516b0a1b97d`.
The branches have materially diverged. `apache/master` is a behavior and
packaging reference, not a merge source. Relevant fixes are selected and
reimplemented; the branch is never merged wholesale into `2.0.0`.
## Sources Of Truth
Use the following precedence for implementation decisions:
1. `apache/master` defines established operator workflows, stable URLs,
monitor hierarchy behavior, translations, and the external `web-app/dist`
release layout.
2. `2.0.0` defines entity, topology, OTLP signals, workspace and token scope,
GreptimeDB integration, and the current HttpOnly UI session boundary.
3. The Angular three-signal transition defines the simple onboarding and
query workflow that does not require a user to understand storage internals.
4. The removed Next.js implementation supplied only proven DTOs and pure
algorithms. Its page structure, private UI framework, BFF, compatibility
scaffolding, and implementation-detail tests were not migration sources.
## Scope Allowlist
### Established Workflows
- Session, lock, logout, permissions, locale, theme, shell notifications, and
account actions.
- Dashboard and Overview as aliases for one implementation, Bulletin, and the
public status page.
- Monitor list, create, edit, copy, detect, bulk operations, detail, realtime,
history, and favorite metrics.
- Alert center, thresholds, groups, inhibition, silence, notice, and integration.
- Collector, monitor definitions, labels, plugins, status, system configuration,
and message server settings.
- Dynamic monitor categories loaded from `/apps/hierarchy`.
### Approved 2.0 Extensions
- Entity list, create, edit, detail, import, discovery, and definition.
- Topology query, graph inspection, table drilldown, and detail interaction.
- Unified OTLP integration plus Metrics, Logs, and Traces workbenches.
- Object store and token settings required by observability workflows.
- Workspace, token scope, GreptimeDB, performance protection, and cross-signal
navigation context.
### Excluded From The New Frontend
- Actions, Incidents, Explorer, the standalone Events experiment, AI Chat UI,
MCP settings UI, and the signal dashboard draft/composition editor.
- The production UI Lab, parity framework, source-string ownership assertions,
generated demo states, in-memory fallbacks, and permanent compatibility pages.
- Next.js route handlers, the standalone Node server, the dedicated Next image,
and the Next-specific gateway split.
Deleted routes use the common not-found page. Compatibility URLs are explicit
redirects in the route registry and never duplicate a page implementation.
## Target Stack And Production Shape
- React 19 with Vite and React Router declarative routing.
- TanStack Query as the only server-state lifecycle.
- Ant Design React as the primitive component system. HertzBeat only owns
domain composites such as the time toolbar, signal workbench, data state,
attribute table, and topology canvas.
- i18next with locale JSON resources based on the five `apache/master` locales.
- CSS Modules and Ant Design theme tokens; no Tailwind utility layer.
- ECharts, G6, CodeMirror, and the log virtualizer load only in their owning
features.
- Vitest, React Testing Library, MSW, and Playwright for behavior-level tests.
- Node 22 and pnpm 10 for development and CI. Release assemblies run only the
JVM and never ship a Node runtime or a separate frontend service.
Vite emits `web-app/dist`. Release assemblies copy that directory to the
external distribution `dist/`, matching `apache/master`. Spring serves the SPA
and `/api` from one origin. The frontend is not embedded into the startup jar,
and no Maven frontend plugin is introduced.
## Source Ownership
```text
web-app/src/
app/ bootstrap, providers, route registry, startup
core/ API client, session, errors, i18n, permissions
layout/ basic, passport, and blank layouts
features/ domain-owned routes, API, model, hooks, pages, components
shared/ domain-neutral composites, forms, time, and query context
assets/i18n/ locale JSON resources
```
Each feature owns its route, API, types, hooks, pages, components, and tests.
`core` and `shared` cannot depend on a feature. Features cannot import another
feature's internals. Cross-feature behavior is exposed through a small explicit
feature entry.
Pages orchestrate route state and feature hooks. API modules perform requests
and response mapping. Hooks own TanStack Query lifecycles. Models contain domain
types and pure transformations. Components render visible behavior. A
controller or view model is introduced only when it removes real complexity;
parameter forwarding is not an abstraction.
The typed route registry is the only static source for routes, navigation,
breadcrumbs, permissions, canonical URLs, and redirects. Dynamic monitor
categories are appended after startup and are not duplicated in static source.
## Session And Routing Boundaries
The React migration must not copy the Angular local-storage token boundary.
Spring exposes UI session create, read, refresh, and delete endpoints backed by
the existing account service. Access and refresh tokens remain HttpOnly
cookies. A filter before Sureness adapts the UI access cookie to an internal
Bearer header only when the request has no explicit `Authorization` header.
Existing API tokens, OTLP Bearer authentication, workspace claims, and token
scope remain unchanged.
Cookie-authenticated mutations require same-origin and CSRF validation. Query
requests may refresh through one single-flight request and retry once.
Mutations are never replayed automatically. Same-origin SSE uses the same
session filter and reconnects with bounded backoff.
SPA fallback accepts only `GET` or `HEAD` HTML navigation for registered UI
prefixes. API, actuator, Swagger, H2, OTLP, SSE, and missing static resources
must retain their own non-HTML errors.
## Maintainability Gates
- Page orchestration files: at most 300 lines.
- Ordinary components, hooks, API modules, and models: at most 400 lines.
- Every production file: at most 600 lines.
- Feature public entries: at most 100 lines.
- Functions: at most 100 lines and cyclomatic complexity at most 12.
- No page or component calls `fetch` directly.
- No production mock, in-memory fallback, permanent `compat`, `legacy`, or
`deprecated` implementation directory.
- No tests that read production source and assert source strings.
- Knip and an explicit dependency-boundary check run in CI.
- Production frontend target: no more than 80,000 lines; hard limit 100,000.
- The final migration must delete at least 40 percent of the previous
production frontend source and must not retain the deprecated Next.js tree.
- Initial shell JavaScript: at most 450 KiB gzip. ECharts, G6, and CodeMirror
are forbidden from the shell chunk.
The budgets are architecture limits, not encouragement to split cohesive code
into one-line wrappers. Any temporary exception must name an owner, reason, and
expiry milestone and must be removed before cutover.
## Delivery Gates
Implementation proceeds in vertical milestones: foundation; session and
release; established shell and dashboard; monitors, alerts, and settings;
entities and topology; OTLP signals; then cutover and deletion. Each milestone
must pass focused tests, production build, and its real-backend browser flow
before the next milestone starts.
The final release must contain one JVM runtime, external hashed SPA assets,
working deep links, JSON API 404 responses, authenticated SSE, explicit Bearer
precedence, and no Node runtime or Next-specific gateway.
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "ftp",
"instance": "127.0.0.1",
"host": "127.0.0.1",
"name": "{{.param.monitorFTP}}"
},
"params": [
+1 -6
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "api",
"instance": "127.0.0.1",
"host": "127.0.0.1",
"name": "{{.param.monitorHTTP}}"
},
"params": [
@@ -73,11 +73,6 @@
"field": "successCode",
"type": 4,
"paramValue": "200, 201"
},
{
"field": "enableUrlEncoding",
"type": 1,
"paramValue": true
}
]
}
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "ping",
"instance": "127.0.0.1",
"host": "127.0.0.1",
"name": "{{.param.monitorPing}}"
},
"params": [
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "port",
"instance": "127.0.0.1",
"host": "127.0.0.1",
"name": "{{.param.monitorPort}}"
},
"params": [
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "fullsite",
"instance": "127.0.0.1",
"host": "127.0.0.1",
"name": "{{.param.monitorSiteMap}}"
},
"params": [
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "ssl_cert",
"instance": "127.0.0.1",
"host": "127.0.0.1",
"name": "{{.param.monitorSSL}}"
},
"params": [
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "udp_port",
"instance": "127.0.0.1",
"host": "127.0.0.1",
"name": "{{.param.monitorUDP}}"
},
"params": [
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "website",
"instance": "127.0.0.1",
"host": "127.0.0.1",
"name": "{{.param.monitorWebsite}}"
},
"params": [
-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",
"instance": "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}}
-126
View File
@@ -1,126 +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 http://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-gateway</artifactId>
<name>${project.artifactId}</name>
<description>HertzBeat Agent Gateway shell and read-only runtime boundary.</description>
<properties>
<spring-ai.version>2.0.0</spring-ai.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-common-spring</artifactId>
</dependency>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-common-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-base</artifactId>
</dependency>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-manager</artifactId>
</dependency>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-alerter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-warehouse</artifactId>
</dependency>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-observability</artifactId>
</dependency>
<dependency>
<groupId>com.usthe.sureness</groupId>
<artifactId>spring-boot3-starter-sureness</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-model</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</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,254 +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.gateway.alert;
import jakarta.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.service.AlertAnalysisPolicyService;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.InvokeCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ReplyMode;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommandRouter;
import org.apache.hertzbeat.ai.gateway.channel.core.ChannelId;
import org.apache.hertzbeat.ai.gateway.contract.AgentAlertIncidentContext;
import org.apache.hertzbeat.ai.gateway.contract.AgentResponseLanguage;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetRef;
import org.apache.hertzbeat.ai.gateway.contract.GatewayEnvelope;
import org.apache.hertzbeat.ai.gateway.contract.UserInput;
import org.apache.hertzbeat.ai.gateway.identity.AgentActor;
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeEntryType;
import org.apache.hertzbeat.ai.gateway.text.GatewayText;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.AlertAnalysisPolicy;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
/**
* Groups persisted single-alert facts and routes qualifying incidents through the standard Gateway command path.
*/
@Slf4j
@Component
public class AgentAlertAnalysisEventHandler {
private static final String ALERT_ANALYSIS_PROMPT_TEMPLATE = """
Analyze this HertzBeat alert incident. Correlate the alerts, gather relevant monitoring evidence with \
tools, identify likely causes, and provide prioritized checks. Incident context:
%s""";
private final AlertAnalysisPolicyService policyService;
private final GatewayCommandRouter commandRouter;
private final Map<AnalysisWindowKey, AnalysisWindow> windows = new ConcurrentHashMap<>();
private final ExecutorService executor = new ThreadPoolExecutor(2, 2, 0, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(256), Thread.ofPlatform().name("agent-alert-analysis-", 0).factory(),
new ThreadPoolExecutor.AbortPolicy());
public AgentAlertAnalysisEventHandler(AlertAnalysisPolicyService policyService,
GatewayCommandRouter commandRouter) {
this.policyService = policyService;
this.commandRouter = commandRouter;
}
@EventListener
public void onSingleAlertCreated(SingleAlert.CreatedEvent event) {
SingleAlert alert = event.alert();
if (alert == null || !CommonConstants.ALERT_STATUS_FIRING.equals(alert.getStatus())) {
return;
}
if (alert.getId() == null || alert.getWorkspaceId() == null || alert.getWorkspaceId().isBlank()) {
return;
}
try {
executor.execute(() -> process(alert.clone()));
} catch (RuntimeException exception) {
log.warn("Agent alert analysis input was rejected for alert {}", alert.getFingerprint(), exception);
}
}
private void process(SingleAlert alert) {
for (AlertAnalysisPolicy policy : policyService.findEnabled(alert.getWorkspaceId())) {
if (alert.getWorkspaceId().equals(policy.getWorkspaceId()) && matches(policy, alert)) {
accept(policy, alert);
}
}
}
private void accept(AlertAnalysisPolicy policy, SingleAlert alert) {
String group = groupKey(policy.getGroupByLabels(), alert.getLabels());
if (group == null) {
return;
}
long now = System.currentTimeMillis();
AnalysisWindowKey windowKey = new AnalysisWindowKey(alert.getWorkspaceId(), policy.getId(), group);
AnalysisTrigger trigger;
synchronized (windows.computeIfAbsent(windowKey, ignored -> new AnalysisWindow(now))) {
AnalysisWindow window = windows.get(windowKey);
if (now - window.firstSeenAt > TimeUnit.SECONDS.toMillis(policy.getWindowSeconds())) {
window.reset(now);
}
window.alerts.put(alertKey(alert), alert.clone());
if (now - window.lastTriggeredAt < TimeUnit.SECONDS.toMillis(policy.getCooldownSeconds())
|| window.alerts.size() < policy.getMinimumAlertCount()) {
return;
}
window.lastTriggeredAt = now;
trigger = new AnalysisTrigger(alert.getWorkspaceId(), policy, group, window.firstSeenAt,
List.copyOf(window.alerts.values()), alert);
window.alerts.clear();
window.firstSeenAt = now;
}
invoke(trigger);
}
private void invoke(AnalysisTrigger trigger) {
String contextHash = GatewayText.sha256(trigger.workspaceId() + ":" + trigger.policy().getId()
+ ":" + trigger.groupKey() + ":" + trigger.firstSeenAt());
String conversationId = "alert-analysis:" + contextHash;
String commandId = "alert_" + GatewayText.sha256(conversationId + ":"
+ alertKey(trigger.triggerAlert())).substring(0, 32);
List<Long> alertIds = trigger.alerts().stream()
.map(SingleAlert::getId)
.filter(java.util.Objects::nonNull)
.distinct()
.limit(256)
.toList();
long now = System.currentTimeMillis();
UserInput userInput = UserInput.builder()
.messageId(commandId)
.conversationId(conversationId)
.target(AgentTargetRef.builder().alertId(trigger.triggerAlert().getId()).build())
.alertIncident(AgentAlertIncidentContext.builder()
.analysisPolicyId(trigger.policy().getId())
.triggerAlertId(trigger.triggerAlert().getId())
.alertIds(alertIds)
.alertCount(trigger.alerts().size())
.windowStartedAt(trigger.firstSeenAt())
.build())
.message(UserInput.Message.builder().text(prompt(trigger)).build())
.build();
InvokeCommand command = InvokeCommand.builder()
.envelope(GatewayEnvelope.builder()
.channelId(ChannelId.SYSTEM.id())
.receivedAt(now)
.actor(AgentActor.alertAnalysisActor())
.preferredLanguage(AgentResponseLanguage.systemDefault())
.workspaceId(trigger.workspaceId())
.build())
.replyMode(ReplyMode.FINAL_ONLY)
.commandId(commandId)
.userInput(userInput)
.entryType(AgentRuntimeEntryType.ALERT_TRIGGER)
.build();
try {
commandRouter.handle(command);
} catch (RuntimeException exception) {
log.error("Automatic alert analysis failed for policy {} and conversation {}",
trigger.policy().getId(), conversationId, exception);
}
}
private String prompt(AnalysisTrigger trigger) {
List<Map<String, Object>> alerts = new ArrayList<>(trigger.alerts().size());
for (SingleAlert alert : trigger.alerts()) {
Map<String, Object> item = new LinkedHashMap<>();
item.put("id", alert.getId());
item.put("status", alert.getStatus());
item.put("labels", alert.getLabels());
item.put("annotations", alert.getAnnotations());
item.put("content", alert.getContent());
item.put("startAt", alert.getStartAt());
item.put("activeAt", alert.getActiveAt());
item.put("triggerTimes", alert.getTriggerTimes());
alerts.add(item);
}
Map<String, Object> context = new LinkedHashMap<>();
context.put("analysisPolicyId", trigger.policy().getId());
context.put("analysisPolicyName", trigger.policy().getName());
context.put("groupByLabels", trigger.policy().getGroupByLabels());
context.put("groupKey", trigger.groupKey());
context.put("windowStartedAt", trigger.firstSeenAt());
context.put("alerts", alerts);
return ALERT_ANALYSIS_PROMPT_TEMPLATE.formatted(JsonUtil.toJson(context));
}
private boolean matches(AlertAnalysisPolicy policy, SingleAlert alert) {
Map<String, String> labels = alert.getLabels();
if (labels == null) {
return false;
}
return policy.getMatchLabels().entrySet().stream()
.allMatch(entry -> entry.getValue().equals(labels.get(entry.getKey())));
}
private String groupKey(List<String> groupByLabels, Map<String, String> labels) {
if (labels == null) {
return null;
}
List<String> components = new ArrayList<>(groupByLabels.size());
for (String label : groupByLabels) {
String value = labels.get(label);
if (value == null) {
return null;
}
components.add(label + "=" + value);
}
components.sort(Comparator.naturalOrder());
return String.join(",", components);
}
private String alertKey(SingleAlert alert) {
return alert.getId() == null ? alert.getFingerprint() : String.valueOf(alert.getId());
}
@PreDestroy
public void destroy() {
executor.shutdownNow();
}
private static final class AnalysisWindow {
private final Map<String, SingleAlert> alerts = new LinkedHashMap<>();
private long firstSeenAt;
private long lastTriggeredAt;
private AnalysisWindow(long firstSeenAt) {
this.firstSeenAt = firstSeenAt;
}
private void reset(long now) {
alerts.clear();
firstSeenAt = now;
}
}
private record AnalysisWindowKey(String workspaceId, Long policyId, String groupKey) {
}
private record AnalysisTrigger(String workspaceId, AlertAnalysisPolicy policy, String groupKey, long firstSeenAt,
List<SingleAlert> alerts, SingleAlert triggerAlert) {
}
}
@@ -1,479 +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.gateway.application;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.InvokeCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ReplyMode;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.ErrorPayload;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.GatewayEventType;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.RunStatusPayload;
import org.apache.hertzbeat.ai.gateway.application.GatewayResponse.GatewaySingleResponse;
import org.apache.hertzbeat.ai.gateway.application.GatewayResponse.GatewayStreamResponse;
import org.apache.hertzbeat.ai.gateway.application.GatewayResponse.Meta;
import org.apache.hertzbeat.ai.gateway.contract.UserInput;
import org.apache.hertzbeat.ai.gateway.conversation.AgentRunSnapshot;
import org.apache.hertzbeat.ai.gateway.conversation.AgentRunSnapshotService;
import org.apache.hertzbeat.ai.gateway.conversation.AgentRunService;
import org.apache.hertzbeat.ai.gateway.conversation.AgentRunStatus;
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeEvent;
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeEvent.EventStatus;
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeEventType;
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeItemKind;
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeRequest;
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeService;
import org.apache.hertzbeat.common.entity.agent.AgentRun;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.SignalType;
/**
* Agent command execution service. Only agent-message commands enter runtime.
*/
@Slf4j
@Service
public class AgentCommandService {
private final AgentRunAdmissionService admissionService;
private final AgentRunService runService;
private final AgentRunSnapshotService snapshotService;
private final AgentRuntimeService runtimeService;
private final GatewayRuntimeEventProjector runtimeEventProjector;
public AgentCommandService(AgentRunAdmissionService admissionService, AgentRunService runService,
AgentRunSnapshotService snapshotService,
AgentRuntimeService runtimeService,
GatewayRuntimeEventProjector runtimeEventProjector) {
this.admissionService = admissionService;
this.runService = runService;
this.snapshotService = snapshotService;
this.runtimeService = runtimeService;
this.runtimeEventProjector = runtimeEventProjector;
}
public GatewayResponse handle(InvokeCommand command) {
return invoke(command, command.userInput());
}
GatewaySingleResponse invokeFinal(GatewayCommand command, UserInput userInput) {
AgentRunAdmission admission = admit(command);
if (admission.decision() != AgentRunAdmission.Decision.EXECUTE_NEW) {
return replayFinal(command, userInput, admission);
}
AgentRuntimeRequest request = runtimeRequest((InvokeCommand) command, admission);
String conversationId = userInput.getConversationId();
AtomicReference<String> reliableFinalResult = new AtomicReference<>();
List<GatewayEvent> events = gatewayEvents(command, request, conversationId, reliableFinalResult)
.collectList()
.block();
GatewayEvent terminalEvent = terminalEvent(events);
boolean failed = terminalEvent.type() == GatewayEventType.ERROR;
return GatewaySingleResponse.builder()
.meta(Meta.builder()
.commandId(command.commandId())
.conversationId(conversationId)
.sessionUid(request.getSession().getSessionUid())
.runUid(request.getRun().getRunUid())
.terminal(true)
.message(failed ? "error" : "completed")
.build())
.body(body(finalMessage(terminalEvent, reliableFinalResult.get()), terminalStatus(terminalEvent)))
.events(events)
.build();
}
GatewayStreamResponse invokeStream(GatewayCommand command, UserInput userInput) {
AgentRunAdmission admission = admit(command);
if (admission.decision() != AgentRunAdmission.Decision.EXECUTE_NEW) {
return replayStream(command, userInput, admission);
}
AgentRuntimeRequest request = runtimeRequest((InvokeCommand) command, admission);
String conversationId = userInput.getConversationId();
return GatewayStreamResponse.builder()
.meta(Meta.builder()
.commandId(command.commandId())
.conversationId(conversationId)
.sessionUid(request.getSession().getSessionUid())
.runUid(request.getRun().getRunUid())
.terminal(false)
.message("streaming")
.build())
.events(gatewayEvents(command, request, conversationId, new AtomicReference<>()))
.build();
}
AgentRuntimeRequest prepare(GatewayCommand command, UserInput userInput) {
AgentRunAdmission admission = admit(command);
if (admission.decision() != AgentRunAdmission.Decision.EXECUTE_NEW) {
throw new IllegalStateException("Agent run admission did not authorize execution");
}
return runtimeRequest((InvokeCommand) command, admission);
}
private AgentRuntimeRequest runtimeRequest(InvokeCommand command, AgentRunAdmission admission) {
return AgentRuntimeRequest.builder()
.entryType(command.entryType())
.approvalHandling(admission.approvalHandling())
.envelope(command.envelope())
.userInput(command.userInput())
.session(admission.session())
.run(admission.run())
.chatHistory(admission.chatHistory())
.build();
}
private AgentRunAdmission admit(GatewayCommand command) {
AgentRunAdmission admission = admissionService.admit((InvokeCommand) command);
if (admission.decision() == AgentRunAdmission.Decision.REJECT_MISMATCH) {
throw new IllegalArgumentException("Agent message identity conflicts with the durable run");
}
return admission;
}
/**
* Shared runtime entry after channel-specific commands have been normalized to UserInput while preserving
* command metadata and runtime entry type for response IDs and tool exposure.
*/
private GatewayResponse invoke(GatewayCommand command, UserInput userInput) {
if (command.replyMode() == ReplyMode.STREAM) {
return invokeStream(command, userInput);
}
return invokeFinal(command, userInput);
}
private Flux<GatewayEvent> gatewayEvents(GatewayCommand command, AgentRuntimeRequest request,
String conversationId, AtomicReference<String> reliableFinalResult) {
AtomicBoolean completed = new AtomicBoolean();
AssistantCompletionTracker assistantCompletion = new AssistantCompletionTracker();
AgentGatewayLifecycleRelay relay = new AgentGatewayLifecycleRelay(request.getRun().getRunUid());
Flux<GatewayEvent> lifecycle = Flux.defer(() -> runtimeService.streamInvoke(request))
.map(event -> {
GatewayEvent gatewayEvent = runtimeEventProjector.project(event, conversationId,
request.getSession().getSessionUid(), request.getRun().getRunUid());
assistantCompletion.observe(event);
try {
completeInvocationOnTerminalEvent(request.getRun(), event, completed,
assistantCompletion, reliableFinalResult);
} catch (Error error) {
if (isFatalJvmError(error)) {
completed.set(true);
relay.fail(error);
}
throw error;
}
return gatewayEvent;
})
.onErrorResume(exception -> {
if (exception instanceof Error error && isFatalJvmError(error)) {
completed.set(true);
return Flux.error(error);
}
if (exception instanceof RecoveryRequiredPersistenceException recoveryFailure) {
return Flux.just(recoveryRequiredErrorEvent(
command, request, conversationId, recoveryFailure.operatorMessage()));
}
log.debug("Agent Gateway runtime failed for run {}", request.getRun().getRunUid(), exception);
failInvocationIfIncomplete(request.getRun(), completed, "Agent Gateway runtime failed.");
return Flux.just(errorEvent(command, request, conversationId,
"Agent Gateway runtime failed."));
})
.concatWith(Flux.defer(() -> {
if (completed.get()) {
return Flux.empty();
}
String message = "Agent Gateway runtime stream completed without a terminal event.";
failInvocationIfIncomplete(request.getRun(), completed, message);
return Flux.just(errorEvent(command, request, conversationId, message));
}))
.doFinally(signalType -> completeInvocationIfStreamFinishedWithoutTerminal(request.getRun(),
signalType, completed));
return relay.connect(lifecycle);
}
private void completeInvocationOnTerminalEvent(AgentRun run, AgentRuntimeEvent event,
AtomicBoolean completed, AssistantCompletionTracker assistantCompletion,
AtomicReference<String> reliableFinalResult) {
if (completed.get()) {
return;
}
if (event.getType() != AgentRuntimeEventType.RUN_COMPLETED
&& event.getType() != AgentRuntimeEventType.ERROR) {
return;
}
if (!completed.compareAndSet(false, true)) {
return;
}
String terminalMessage = StringUtils.hasText(event.getErrorMessage())
? event.getErrorMessage()
: "Agent Gateway runtime failed.";
try {
if (event.getType() == AgentRuntimeEventType.RUN_COMPLETED) {
if (!assistantCompletion.latestAssistantCompleted()) {
throw new IllegalStateException("Agent runtime completed without an assistant message completion");
}
if (!StringUtils.hasText(event.getResult())) {
throw new IllegalStateException("Agent runtime completed without a durable result");
}
AgentRun succeededRun = runService.markSucceeded(run, event.getResult());
if (succeededRun == null || !StringUtils.hasText(succeededRun.getResultSummary())) {
throw new IllegalStateException("Succeeded agent run must expose its persisted result");
}
reliableFinalResult.compareAndSet(null, succeededRun.getResultSummary());
return;
}
if (event.getStatus() == EventStatus.RECOVERY_REQUIRED) {
runService.markRecoveryRequired(run, terminalMessage);
} else {
runService.markFailed(run, terminalMessage);
}
} catch (RuntimeException exception) {
if (event.getStatus() == EventStatus.RECOVERY_REQUIRED) {
throw new RecoveryRequiredPersistenceException(terminalMessage, exception);
}
completed.set(false);
throw exception;
} catch (Error error) {
if (event.getStatus() == EventStatus.RECOVERY_REQUIRED && !isFatalJvmError(error)) {
throw new RecoveryRequiredPersistenceException(terminalMessage, error);
}
completed.set(false);
throw error;
}
}
private boolean isFatalJvmError(Error error) {
return error instanceof VirtualMachineError
|| error instanceof ThreadDeath
|| error instanceof LinkageError;
}
private void failInvocationIfIncomplete(AgentRun run, AtomicBoolean completed, String message) {
if (!completed.compareAndSet(false, true)) {
return;
}
try {
runService.markFailed(run, message);
} catch (RuntimeException exception) {
completed.set(false);
throw exception;
}
}
private void completeInvocationIfStreamFinishedWithoutTerminal(AgentRun run, SignalType signalType,
AtomicBoolean completed) {
if (!completed.compareAndSet(false, true)) {
return;
}
if (signalType == SignalType.CANCEL) {
runService.markCancelled(run, "Runtime stream client disconnected.");
return;
}
if (signalType == SignalType.ON_ERROR) {
runService.markFailed(run, "Agent Gateway runtime failed.");
return;
}
runService.markFailed(run, "Agent Gateway runtime stream completed without a terminal event.");
}
private GatewayEvent terminalEvent(List<GatewayEvent> events) {
for (int index = events.size() - 1; index >= 0; index--) {
GatewayEvent event = events.get(index);
if (isTerminal(event)) {
return event;
}
}
throw new IllegalStateException("Mapped runtime events must contain a terminal event");
}
private String finalMessage(GatewayEvent terminalEvent, String reliableFinalResult) {
if (terminalEvent.type() == GatewayEventType.ERROR) {
if (terminalEvent.payload() instanceof ErrorPayload payload
&& StringUtils.hasText(payload.errorMessage())) {
return payload.errorMessage();
}
return "Agent Gateway runtime failed.";
}
if (!StringUtils.hasText(reliableFinalResult)) {
throw new IllegalStateException("Completed agent run must expose its reliable final result");
}
return reliableFinalResult;
}
private String terminalStatus(GatewayEvent terminalEvent) {
if (terminalEvent.type() != GatewayEventType.ERROR) {
return AgentRunStatus.SUCCEEDED.name();
}
if (terminalEvent.payload() instanceof ErrorPayload payload
&& EventStatus.RECOVERY_REQUIRED.externalName().equals(payload.status())) {
return AgentRunStatus.RECOVERY_REQUIRED.name();
}
return AgentRunStatus.FAILED.name();
}
private boolean isTerminal(GatewayEvent event) {
return event.type() == GatewayEventType.RUN_COMPLETED
|| event.type() == GatewayEventType.RUN_STATUS
|| event.type() == GatewayEventType.ERROR;
}
private GatewayStreamResponse replayStream(GatewayCommand command, UserInput userInput,
AgentRunAdmission admission) {
AgentRunSnapshot snapshot = snapshotService.snapshot(admission.session(), admission.run());
boolean terminal = admission.decision() == AgentRunAdmission.Decision.REPLAY_TERMINAL;
return GatewayStreamResponse.builder()
.meta(replayMeta(command, userInput, snapshot, terminal))
.events(Flux.just(runStatusEvent(command, userInput, snapshot)))
.build();
}
private GatewaySingleResponse replayFinal(GatewayCommand command, UserInput userInput,
AgentRunAdmission admission) {
AgentRunSnapshot snapshot = snapshotService.snapshot(admission.session(), admission.run());
boolean terminal = admission.decision() == AgentRunAdmission.Decision.REPLAY_TERMINAL;
GatewayEvent event = runStatusEvent(command, userInput, snapshot);
String message = StringUtils.hasText(snapshot.result()) ? snapshot.result() : snapshot.errorMessage();
return GatewaySingleResponse.builder()
.meta(replayMeta(command, userInput, snapshot, terminal))
.body(body(message, snapshot.status()))
.events(List.of(event))
.build();
}
private Meta replayMeta(GatewayCommand command, UserInput userInput, AgentRunSnapshot snapshot,
boolean terminal) {
return Meta.builder()
.commandId(command.commandId())
.conversationId(userInput.getConversationId())
.sessionUid(snapshot.sessionUid())
.runUid(snapshot.runUid())
.terminal(terminal)
.message(terminal ? "replayed" : "running")
.build();
}
private GatewayEvent runStatusEvent(GatewayCommand command, UserInput userInput, AgentRunSnapshot snapshot) {
return GatewayEvent.builder()
.type(GatewayEventType.RUN_STATUS)
.eventId(snapshot.runUid() + ":status:" + snapshot.status().toLowerCase(java.util.Locale.ROOT))
.conversationId(userInput.getConversationId())
.sessionUid(snapshot.sessionUid())
.runUid(snapshot.runUid())
.payload(RunStatusPayload.builder()
.status(snapshot.status())
.result(snapshot.result())
.errorMessage(snapshot.errorMessage())
.replayAvailable(snapshot.replayAvailable())
.build())
.timestamp(System.currentTimeMillis())
.build();
}
private GatewayEvent errorEvent(GatewayCommand command, AgentRuntimeRequest request, String conversationId,
String message) {
return GatewayEvent.builder()
.type(GatewayEventType.ERROR)
.eventId(command.commandId() + ":error")
.conversationId(conversationId)
.sessionUid(request.getSession().getSessionUid())
.runUid(request.getRun().getRunUid())
.payload(ErrorPayload.builder()
.errorMessage(message)
.build())
.timestamp(System.currentTimeMillis())
.build();
}
private GatewayEvent recoveryRequiredErrorEvent(GatewayCommand command, AgentRuntimeRequest request,
String conversationId, String message) {
return GatewayEvent.builder()
.type(GatewayEventType.ERROR)
.eventId(command.commandId() + ":recovery-required")
.conversationId(conversationId)
.sessionUid(request.getSession().getSessionUid())
.runUid(request.getRun().getRunUid())
.payload(ErrorPayload.builder()
.errorMessage(message)
.status(EventStatus.RECOVERY_REQUIRED.externalName())
.build())
.timestamp(System.currentTimeMillis())
.build();
}
private static final class RecoveryRequiredPersistenceException extends RuntimeException {
private final String operatorMessage;
private RecoveryRequiredPersistenceException(String operatorMessage, Throwable cause) {
super("Agent recovery-required state could not be persisted", cause);
this.operatorMessage = operatorMessage;
}
private String operatorMessage() {
return operatorMessage;
}
}
private static final class AssistantCompletionTracker {
private String latestAssistantItemId;
private String completedAssistantItemId;
private void observe(AgentRuntimeEvent event) {
if (event.getItemKind() == AgentRuntimeItemKind.TOOL_CALL) {
invalidate();
return;
}
if (event.getItemKind() != AgentRuntimeItemKind.ASSISTANT_MESSAGE) {
return;
}
if (event.getType() == AgentRuntimeEventType.ITEM_STARTED) {
latestAssistantItemId = event.getItemId();
completedAssistantItemId = null;
} else if (event.getType() == AgentRuntimeEventType.ITEM_DELTA
&& !Objects.equals(latestAssistantItemId, event.getItemId())) {
invalidate();
} else if (event.getType() == AgentRuntimeEventType.ITEM_COMPLETED) {
completedAssistantItemId = Objects.equals(latestAssistantItemId, event.getItemId())
? event.getItemId()
: null;
}
}
private void invalidate() {
latestAssistantItemId = null;
completedAssistantItemId = null;
}
private boolean latestAssistantCompleted() {
return StringUtils.hasText(latestAssistantItemId)
&& Objects.equals(latestAssistantItemId, completedAssistantItemId);
}
}
private Map<String, Object> body(String message, String status) {
return Map.of("message", StringUtils.hasText(message) ? message : "",
"status", StringUtils.hasText(status) ? status : "");
}
}
@@ -1,168 +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.gateway.application;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetAuthority;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetRef;
import org.apache.hertzbeat.common.entity.manager.ObserveEntity;
import org.apache.hertzbeat.manager.service.entity.EntityWorkspaceQueryService;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
/** Resolves and rechecks persisted authority for an exact workspace-owned Entity. */
@Service
public class AgentEntityTargetAuthorityService {
public static final String TARGET_VERSION_PREFIX = "entity.";
public static final String AUTHORITY_VERSION_PREFIX = "entity-authority.";
public static final String TARGET_VERSION = "entity.v1";
public static final String AUTHORITY_VERSION = "entity-authority.v1";
private final EntityWorkspaceQueryService entityWorkspaceQueryService;
public AgentEntityTargetAuthorityService(EntityWorkspaceQueryService entityWorkspaceQueryService) {
this.entityWorkspaceQueryService = entityWorkspaceQueryService;
}
public AgentTargetRef canonicalize(String workspaceId, long entityId) {
if (!StringUtils.hasText(workspaceId) || entityId <= 0) {
throw unavailable();
}
ObserveEntity entity;
try {
entity = entityWorkspaceQueryService.findEntityById(workspaceId, entityId)
.filter(candidate -> Objects.equals(workspaceId, candidate.getWorkspaceId()))
.filter(candidate -> Objects.equals(entityId, candidate.getId()))
.orElseThrow(this::unavailable);
} catch (UnavailableException failure) {
throw failure;
} catch (RuntimeException ignored) {
throw unavailable();
}
return AgentTargetRef.builder()
.version(TARGET_VERSION)
.entityId(entityId)
.authority(AgentTargetAuthority.builder()
.bindingId(entityId)
.version(AUTHORITY_VERSION)
.hash(authorityHash(workspaceId, entity))
.build())
.build();
}
public boolean verify(String workspaceId, AgentTargetRef target) {
if (!isCanonicalTarget(target)) {
return false;
}
try {
return Objects.equals(target, canonicalize(workspaceId, target.getEntityId()));
} catch (UnavailableException ignored) {
return false;
}
}
public boolean isCanonicalTarget(AgentTargetRef target) {
AgentTargetAuthority authority = target == null ? null : target.getAuthority();
return target != null
&& TARGET_VERSION.equals(target.getVersion())
&& target.getEntityId() != null && target.getEntityId() > 0
&& authority != null
&& Objects.equals(target.getEntityId(), authority.getBindingId())
&& AUTHORITY_VERSION.equals(authority.getVersion())
&& StringUtils.hasText(authority.getHash())
&& target.getMonitorId() == null && target.getAlertId() == null
&& target.getAlertType() == null && target.getCollector() == null
&& target.getSignal() == null && target.getTopology() == null
&& target.getTrace() == null && target.getLog() == null && target.getService() == null;
}
private String authorityHash(String workspaceId, ObserveEntity entity) {
StringBuilder material = new StringBuilder("entity-authority.v1;");
append(material, "workspace", workspaceId);
append(material, "id", entity.getId());
append(material, "type", entity.getType());
append(material, "name", entity.getName());
append(material, "displayName", entity.getDisplayName());
append(material, "subtype", entity.getSubtype());
append(material, "namespace", entity.getNamespace());
append(material, "environment", entity.getEnvironment());
append(material, "status", entity.getStatus());
append(material, "criticality", entity.getCriticality());
append(material, "owner", entity.getOwner());
append(material, "lifecycle", entity.getLifecycle());
append(material, "tier", entity.getTier());
append(material, "system", entity.getSystem());
append(material, "source", entity.getSource());
append(material, "description", entity.getDescription());
appendMap(material, "labels", entity.getLabels());
appendList(material, "tags", entity.getTags());
append(material, "updatedAt", entity.getGmtUpdate());
try {
return "sha256:" + HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
.digest(material.toString().getBytes(StandardCharsets.UTF_8)));
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException("SHA-256 is required for entity target authority", exception);
}
}
private void appendMap(StringBuilder material, String field, Map<String, String> values) {
append(material, field + "Size", values == null ? null : values.size());
if (values != null) {
new TreeMap<>(values).forEach((key, value) -> {
append(material, field + "Key", key);
append(material, field + "Value", value);
});
}
}
private void appendList(StringBuilder material, String field, List<String> values) {
append(material, field + "Size", values == null ? null : values.size());
if (values != null) {
values.forEach(value -> append(material, field + "Value", value));
}
}
private void append(StringBuilder material, String field, Object value) {
String text = value == null ? null : String.valueOf(value);
material.append(field).append(':').append(text == null ? -1 : text.length()).append(':');
if (text != null) {
material.append(text);
}
material.append(';');
}
private UnavailableException unavailable() {
return new UnavailableException();
}
/** Cause-free failure used at the channel boundary. */
public static final class UnavailableException extends IllegalArgumentException {
UnavailableException() {
super("Entity target is unavailable");
}
}
}
@@ -1,139 +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.gateway.application;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Sinks;
import reactor.util.concurrent.Queues;
/**
* Separates the durable runtime subscription from optional client demand.
*/
@Slf4j
final class AgentGatewayLifecycleRelay {
private static final int LIVE_BUFFER_SIZE = Queues.SMALL_BUFFER_SIZE;
private static final String LIFECYCLE_FAILURE = "Agent Gateway lifecycle failed";
private final String runUid;
private final Sinks.One<GatewayEvent> started = Sinks.one();
private final Sinks.Many<GatewayEvent> live = Sinks.many().multicast()
.onBackpressureBuffer(LIVE_BUFFER_SIZE, false);
private final Sinks.One<GatewayEvent> terminal = Sinks.one();
private final AtomicBoolean primaryClient = new AtomicBoolean();
private final AtomicBoolean drainStarted = new AtomicBoolean();
private final AtomicBoolean startSeen = new AtomicBoolean();
private final AtomicBoolean terminalSeen = new AtomicBoolean();
private final AtomicLong droppedLiveEvents = new AtomicLong();
AgentGatewayLifecycleRelay(String runUid) {
this.runUid = runUid;
}
Flux<GatewayEvent> connect(Flux<GatewayEvent> lifecycle) {
return Flux.defer(() -> {
Flux<GatewayEvent> clientEvents = primaryClient.compareAndSet(false, true)
? Flux.concat(started.asMono(), live.asFlux(), terminal.asMono())
: terminal.asMono().flux();
return clientEvents.doOnSubscribe(ignored -> startDrain(lifecycle));
});
}
void fail(Throwable failure) {
completeStartIfMissing();
completeLive();
Throwable clientFailure = isFatal(failure)
? failure
: new IllegalStateException(LIFECYCLE_FAILURE);
emitCritical("terminal failure", terminal.tryEmitError(clientFailure));
}
private void startDrain(Flux<GatewayEvent> lifecycle) {
if (!drainStarted.compareAndSet(false, true)) {
return;
}
try {
lifecycle.subscribe(this::emit, this::fail, this::complete);
} catch (RuntimeException failure) {
fail(failure);
throw failure;
} catch (Error failure) {
fail(failure);
throw failure;
}
}
private void emit(GatewayEvent event) {
if (event.type() == GatewayEvent.GatewayEventType.RUN_STARTED) {
startSeen.set(true);
emitCritical("run start", started.tryEmitValue(event));
return;
}
if (event.type() == GatewayEvent.GatewayEventType.RUN_COMPLETED
|| event.type() == GatewayEvent.GatewayEventType.ERROR) {
terminalSeen.set(true);
emitCritical("terminal event", terminal.tryEmitValue(event));
return;
}
Sinks.EmitResult result = live.tryEmitNext(event);
if (result.isFailure()) {
long dropped = droppedLiveEvents.incrementAndGet();
if (dropped == 1) {
log.warn("Agent Gateway live event buffer is full for run {}; dropping nonterminal events", runUid);
}
}
}
private void complete() {
completeStartIfMissing();
completeLive();
if (!terminalSeen.get()) {
emitCritical("missing terminal event", terminal.tryEmitError(
new IllegalStateException(LIFECYCLE_FAILURE)));
}
long dropped = droppedLiveEvents.get();
if (dropped > 1) {
log.warn("Agent Gateway dropped {} nonterminal live events for run {}", dropped, runUid);
}
}
private void completeStartIfMissing() {
if (!startSeen.get()) {
emitCritical("empty run start", started.tryEmitEmpty());
}
}
private void completeLive() {
emitCritical("live completion", live.tryEmitComplete());
}
private void emitCritical(String signal, Sinks.EmitResult result) {
if (result.isFailure()) {
log.warn("Agent Gateway could not emit {} for run {}: {}", signal, runUid, result);
}
}
private boolean isFatal(Throwable failure) {
return failure instanceof VirtualMachineError
|| failure instanceof ThreadDeath
|| failure instanceof LinkageError;
}
}
@@ -1,228 +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.gateway.application;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Pattern;
import org.apache.hertzbeat.ai.gateway.contract.AgentLogRef;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetAuthority;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetRef;
import org.apache.hertzbeat.ai.gateway.text.GatewayText;
import org.apache.hertzbeat.common.entity.log.LogEntry;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.hertzbeat.observability.logs.service.LogQueryService;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
/** Resolves one exact non-live Log Explore page through the trusted-workspace query boundary. */
@Service
public class AgentLogTargetAuthorityService {
public static final String TARGET_VERSION_PREFIX = "log-page.";
public static final String AUTHORITY_VERSION_PREFIX = "log-page-authority.";
public static final String TARGET_VERSION = "log-page.v1";
public static final String AUTHORITY_VERSION = "log-page-authority.v1";
private static final long MAX_RANGE_MILLIS = Duration.ofDays(7).toMillis();
private static final Pattern SAFE_ID = Pattern.compile("[A-Za-z0-9][A-Za-z0-9._:-]{0,127}");
private static final Set<String> SEVERITIES = Set.of("TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL");
private final LogQueryService logQueryService;
public AgentLogTargetAuthorityService(LogQueryService logQueryService) {
this.logQueryService = logQueryService;
}
public AgentTargetRef canonicalize(String workspaceId, AgentLogRef source) {
AgentLogRef log = normalizeSource(source);
Page<LogEntry> page;
try {
page = query(workspaceId, log);
} catch (RuntimeException ignored) {
throw unavailable();
}
if (page == null || page.isEmpty() || page.getNumber() != log.getPageIndex()
|| page.getSize() != log.getPageSize()) {
throw unavailable();
}
return AgentTargetRef.builder()
.version(TARGET_VERSION)
.log(log)
.authority(AgentTargetAuthority.builder()
.version(AUTHORITY_VERSION)
.hash("sha256:" + GatewayText.sha256(authorityMaterial(workspaceId, log, page)))
.build())
.build();
}
public boolean verify(String workspaceId, AgentTargetRef target) {
if (!isCanonicalTarget(target)) {
return false;
}
try {
return Objects.equals(target, canonicalize(workspaceId, target.getLog()));
} catch (UnavailableException ignored) {
return false;
}
}
public boolean isCanonicalTarget(AgentTargetRef target) {
AgentTargetAuthority authority = target == null ? null : target.getAuthority();
if (target == null || !TARGET_VERSION.equals(target.getVersion()) || target.getLog() == null
|| authority == null || authority.getBindingId() != null
|| !AUTHORITY_VERSION.equals(authority.getVersion())
|| authority.getHash() == null || !authority.getHash().matches("sha256:[0-9a-f]{64}")
|| target.getMonitorId() != null || target.getAlertId() != null || target.getAlertType() != null
|| target.getEntityId() != null || target.getCollector() != null || target.getSignal() != null
|| target.getTopology() != null || target.getTrace() != null || target.getService() != null) {
return false;
}
try {
return Objects.equals(target.getLog(), normalizeSource(target.getLog()));
} catch (UnavailableException ignored) {
return false;
}
}
public AgentLogRef normalizeSource(AgentLogRef source) {
if (source == null || source.getStart() == null || source.getEnd() == null || source.getStart() <= 0
|| source.getEnd() <= source.getStart() || source.getEnd() - source.getStart() > MAX_RANGE_MILLIS
|| !optionalSafeId(source.getTraceId()) || !optionalSafeId(source.getSpanId())
|| source.getSeverityNumber() != null
&& (source.getSeverityNumber() < 1 || source.getSeverityNumber() > 24)
|| source.getHideInternal() == null || source.getHideNoise() == null
|| source.getPageIndex() == null || source.getPageIndex() < 0 || source.getPageIndex() > 10_000
|| source.getPageSize() == null || source.getPageSize() < 1 || source.getPageSize() > 100) {
throw unavailable();
}
String severity = text(source.getSeverityText(), 16);
if (severity != null) {
severity = severity.toUpperCase(Locale.ROOT);
if (!SEVERITIES.contains(severity)) {
throw unavailable();
}
}
return AgentLogRef.builder()
.start(source.getStart()).end(source.getEnd())
.traceId(source.getTraceId()).spanId(source.getSpanId())
.severityNumber(source.getSeverityNumber()).severityText(severity)
.search(text(source.getSearch(), 256))
.serviceName(text(source.getServiceName(), 512))
.serviceNamespace(text(source.getServiceNamespace(), 512))
.environment(text(source.getEnvironment(), 512))
.resourceFilter(text(source.getResourceFilter(), 2048))
.attributeFilter(text(source.getAttributeFilter(), 2048))
.hideInternal(source.getHideInternal()).hideNoise(source.getHideNoise())
.pageIndex(source.getPageIndex()).pageSize(source.getPageSize())
.build();
}
private Page<LogEntry> query(String workspaceId, AgentLogRef log) {
return logQueryService.list(workspaceId, null, log.getStart(), log.getEnd(), log.getTraceId(), log.getSpanId(),
log.getSeverityNumber(), log.getSeverityText(), log.getSearch(), log.getServiceName(),
log.getServiceNamespace(), log.getEnvironment(), log.getResourceFilter(), log.getAttributeFilter(),
log.getPageIndex(), log.getPageSize(), log.getHideInternal(), log.getHideNoise());
}
private String authorityMaterial(String workspaceId, AgentLogRef log, Page<LogEntry> page) {
StringBuilder material = new StringBuilder("log-page-authority.v1;");
append(material, "workspaceId", workspaceId);
append(material, "scope", JsonUtil.toJson(log));
append(material, "number", page.getNumber());
append(material, "size", page.getSize());
append(material, "totalElements", page.getTotalElements());
append(material, "totalPages", page.getTotalPages());
for (LogEntry row : page.getContent()) {
append(material, "timeUnixNano", row == null ? null : row.getTimeUnixNano());
append(material, "observedTimeUnixNano", row == null ? null : row.getObservedTimeUnixNano());
append(material, "severityNumber", row == null ? null : row.getSeverityNumber());
append(material, "severityText", row == null ? null : row.getSeverityText());
append(material, "body", row == null ? null : canonical(row.getBody()));
append(material, "attributes", row == null ? null : canonical(row.getAttributes()));
append(material, "droppedAttributesCount", row == null ? null : row.getDroppedAttributesCount());
append(material, "traceId", row == null ? null : row.getTraceId());
append(material, "spanId", row == null ? null : row.getSpanId());
append(material, "traceFlags", row == null ? null : row.getTraceFlags());
append(material, "resource", row == null ? null : canonical(row.getResource()));
append(material, "resourceSchemaUrl", row == null ? null : row.getResourceSchemaUrl());
append(material, "instrumentationScope", row == null ? null : canonical(row.getInstrumentationScope()));
append(material, "scopeSchemaUrl", row == null ? null : row.getScopeSchemaUrl());
}
return material.toString();
}
private Object canonical(Object value) {
if (value instanceof Map<?, ?> map) {
Map<String, Object> sorted = new TreeMap<>();
map.forEach((key, nested) -> sorted.put(String.valueOf(key), canonical(nested)));
return sorted;
}
if (value instanceof Iterable<?> iterable) {
List<Object> values = new ArrayList<>();
iterable.forEach(nested -> values.add(canonical(nested)));
return values;
}
return value;
}
private String text(String value, int maximumLength) {
if (value == null) {
return null;
}
String normalized = value.trim();
if (!StringUtils.hasText(normalized) || normalized.length() > maximumLength
|| normalized.codePoints().anyMatch(code -> code < 32 || code == 127)
|| !Objects.equals(normalized, GatewayText.redactSecrets(normalized))) {
throw unavailable();
}
return normalized;
}
private boolean optionalSafeId(String value) {
return value == null || SAFE_ID.matcher(value).matches();
}
private void append(StringBuilder material, String field, Object value) {
String text = value == null ? null : value instanceof String ? (String) value : JsonUtil.toJson(value);
material.append(field).append(':').append(text == null ? -1 : text.length()).append(':');
if (text != null) {
material.append(text);
}
material.append(';');
}
private UnavailableException unavailable() {
return new UnavailableException();
}
/** Cause-free failure used at the channel boundary. */
public static final class UnavailableException extends IllegalArgumentException {
UnavailableException() {
super("Log target is unavailable");
}
}
}
@@ -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.gateway.application;
import java.util.Objects;
import org.apache.hertzbeat.ai.gateway.application.AgentTargetCanonicalizationService.FailureKind;
import org.apache.hertzbeat.ai.gateway.application.AgentTargetCanonicalizationService.TargetCanonicalizationException;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetAuthority;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetRef;
import org.apache.hertzbeat.ai.gateway.contract.UserInput;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
/** Isolates strict source/canonical adaptation for one exact non-live Log Explore page. */
@Service
public class AgentLogTargetCanonicalizationAdapter {
private final AgentLogTargetAuthorityService authorityService;
public AgentLogTargetCanonicalizationAdapter(AgentLogTargetAuthorityService authorityService) {
this.authorityService = authorityService;
}
public boolean isIntent(AgentTargetRef target) {
if (target == null) {
return false;
}
AgentTargetAuthority authority = target.getAuthority();
return target.getLog() != null
|| hasVersion(target.getVersion(), AgentLogTargetAuthorityService.TARGET_VERSION_PREFIX)
|| authority != null && hasVersion(
authority.getVersion(), AgentLogTargetAuthorityService.AUTHORITY_VERSION_PREFIX);
}
public GatewayCommand.InvokeCommand canonicalize(GatewayCommand.InvokeCommand command) {
AgentTargetRef source = requireSourceIntent(command.userInput().getTarget());
try {
AgentTargetRef target = authorityService.canonicalize(
command.envelope().getWorkspaceId(), source.getLog());
if (!authorityService.isCanonicalTarget(target)
|| !Objects.equals(authorityService.normalizeSource(source.getLog()), target.getLog())) {
throw unavailable();
}
return withTarget(command, target);
} catch (AgentLogTargetAuthorityService.UnavailableException failure) {
throw unavailable();
} catch (TargetCanonicalizationException failure) {
throw failure;
} catch (RuntimeException ignored) {
throw unavailable();
}
}
public GatewayCommand.InvokeCommand replayCommand(
GatewayCommand.InvokeCommand command, AgentTargetRef persistedTarget) {
AgentTargetRef source = requireSourceIntent(command.userInput().getTarget());
try {
if (!authorityService.isCanonicalTarget(persistedTarget)
|| !Objects.equals(authorityService.normalizeSource(source.getLog()), persistedTarget.getLog())) {
throw mismatch();
}
return withTarget(command, persistedTarget);
} catch (TargetCanonicalizationException failure) {
throw failure;
} catch (RuntimeException ignored) {
throw mismatch();
}
}
public static AgentTargetRef sourceIntent(AgentTargetRef target) {
return AgentTargetRef.builder().log(target.getLog()).build();
}
private AgentTargetRef requireSourceIntent(AgentTargetRef target) {
if (target == null || target.getLog() == null || target.getVersion() != null || target.getAuthority() != null
|| target.getMonitorId() != null || target.getAlertId() != null || target.getAlertType() != null
|| target.getEntityId() != null || target.getCollector() != null || target.getSignal() != null
|| target.getTopology() != null || target.getTrace() != null || target.getService() != null) {
throw unavailable();
}
return target;
}
private GatewayCommand.InvokeCommand withTarget(GatewayCommand.InvokeCommand command, AgentTargetRef target) {
UserInput userInput = command.userInput().toBuilder().target(target).build();
return new GatewayCommand.InvokeCommand(command.envelope(), command.replyMode(), command.commandId(), userInput,
command.entryType());
}
private boolean hasVersion(String version, String prefix) {
return StringUtils.hasText(version) && version.startsWith(prefix);
}
private TargetCanonicalizationException mismatch() {
return new TargetCanonicalizationException(FailureKind.MISMATCH);
}
private TargetCanonicalizationException unavailable() {
return new TargetCanonicalizationException(FailureKind.UNAVAILABLE);
}
}
@@ -1,52 +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.gateway.application;
import java.util.List;
import java.util.Objects;
import org.apache.hertzbeat.ai.gateway.runtime.AgentApprovalHandling;
import org.apache.hertzbeat.ai.gateway.runtime.TranscriptMessage;
import org.apache.hertzbeat.common.entity.agent.AgentRun;
import org.apache.hertzbeat.common.entity.agent.AgentSession;
/**
* Linearized outcome of admitting one durable Agent Gateway message.
*/
public record AgentRunAdmission(
Decision decision,
AgentSession session,
AgentRun run,
AgentApprovalHandling approvalHandling,
List<TranscriptMessage> chatHistory) {
public AgentRunAdmission {
Objects.requireNonNull(decision, "Agent run admission decision is required");
Objects.requireNonNull(session, "Agent run admission session is required");
Objects.requireNonNull(run, "Agent run admission run is required");
Objects.requireNonNull(approvalHandling, "Agent run admission approval handling is required");
chatHistory = chatHistory == null ? List.of() : List.copyOf(chatHistory);
}
/** Durable admission decisions for one normalized message identity. */
public enum Decision {
EXECUTE_NEW,
REPLAY_ACTIVE,
REPLAY_TERMINAL,
REJECT_MISMATCH
}
}
@@ -1,90 +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.gateway.application;
import java.util.Objects;
import java.util.Optional;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.InvokeCommand;
import org.apache.hertzbeat.ai.gateway.conversation.persistence.AgentRunDao;
import org.apache.hertzbeat.ai.gateway.conversation.AgentSessionService;
import org.apache.hertzbeat.common.entity.agent.AgentSession;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
/** Resolves the canonical session before entering the serialized admission transaction. */
@Service
public class AgentRunAdmissionService {
private final AgentSessionService sessionService;
private final AgentRunAdmissionTransaction admissionTransaction;
private final AgentRunDao runDao;
private final AgentTargetCanonicalizationService targetCanonicalizationService;
public AgentRunAdmissionService(AgentSessionService sessionService,
AgentRunAdmissionTransaction admissionTransaction, AgentRunDao runDao,
AgentTargetCanonicalizationService targetCanonicalizationService) {
this.sessionService = sessionService;
this.admissionTransaction = admissionTransaction;
this.runDao = runDao;
this.targetCanonicalizationService = targetCanonicalizationService;
}
public AgentRunAdmission admit(InvokeCommand command) {
validateCommandIdentity(command);
if (!targetCanonicalizationService.requiresCanonicalization(command)) {
return admitResolved(command, command);
}
Optional<AgentSession> existingSession = sessionService.findSession(
command.envelope(), command.userInput().getConversationId());
if (hasExistingRun(existingSession, command)) {
return admissionTransaction.admit(existingSession.orElseThrow().getId(), command, null);
}
try {
InvokeCommand canonicalCommand = targetCanonicalizationService.canonicalize(command);
return admitResolved(command, canonicalCommand);
} catch (RuntimeException failure) {
Optional<AgentSession> winnerSession = sessionService.findSession(
command.envelope(), command.userInput().getConversationId());
if (hasExistingRun(winnerSession, command)) {
return admissionTransaction.admit(winnerSession.orElseThrow().getId(), command, null);
}
throw failure;
}
}
private AgentRunAdmission admitResolved(InvokeCommand sourceCommand, InvokeCommand executionCommand) {
AgentSession session = sessionService.findOrCreateSession(
sourceCommand.envelope(), sourceCommand.userInput(), sourceCommand.entryType());
if (!Objects.equals(session.getWorkspaceId(), sourceCommand.envelope().getWorkspaceId())) {
throw new IllegalArgumentException("Agent run session workspace does not match the command");
}
return admissionTransaction.admit(session.getId(), sourceCommand, executionCommand);
}
private boolean hasExistingRun(Optional<AgentSession> session, InvokeCommand command) {
return session.isPresent() && runDao.findBySessionIdAndMessageId(
session.get().getId(), command.userInput().getMessageId()).isPresent();
}
private void validateCommandIdentity(InvokeCommand command) {
if (!StringUtils.hasText(command.userInput().getMessageId())
|| !Objects.equals(command.commandId(), command.userInput().getMessageId())) {
throw new IllegalArgumentException("Agent command and message identity must match");
}
}
}
@@ -1,200 +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.gateway.application;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import org.apache.hertzbeat.ai.gateway.application.AgentRunAdmission.Decision;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.InvokeCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ReplyMode;
import org.apache.hertzbeat.ai.gateway.conversation.AgentRunService;
import org.apache.hertzbeat.ai.gateway.conversation.AgentRunStatus;
import org.apache.hertzbeat.ai.gateway.conversation.AgentTranscriptRecorder;
import org.apache.hertzbeat.ai.gateway.conversation.persistence.AgentRunDao;
import org.apache.hertzbeat.ai.gateway.conversation.persistence.AgentSessionDao;
import org.apache.hertzbeat.ai.gateway.identity.AgentActor;
import org.apache.hertzbeat.ai.gateway.runtime.AgentApprovalHandling;
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeEntryType;
import org.apache.hertzbeat.ai.gateway.runtime.TranscriptMessage;
import org.apache.hertzbeat.ai.gateway.contract.AgentRunRequestSnapshot;
import org.apache.hertzbeat.common.entity.agent.AgentRun;
import org.apache.hertzbeat.common.entity.agent.AgentSession;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
/** Serializes run admission after the canonical session has been resolved. */
@Service
public class AgentRunAdmissionTransaction {
private final AgentSessionDao sessionDao;
private final AgentRunDao runDao;
private final AgentRunService runService;
private final AgentTranscriptRecorder transcriptRecorder;
private final AgentTargetCanonicalizationService targetCanonicalizationService;
public AgentRunAdmissionTransaction(AgentSessionDao sessionDao, AgentRunDao runDao,
AgentRunService runService, AgentTranscriptRecorder transcriptRecorder,
AgentTargetCanonicalizationService targetCanonicalizationService) {
this.sessionDao = sessionDao;
this.runDao = runDao;
this.runService = runService;
this.transcriptRecorder = transcriptRecorder;
this.targetCanonicalizationService = targetCanonicalizationService;
}
@Transactional
public AgentRunAdmission admit(Long sessionId, InvokeCommand command) {
return admit(sessionId, command, command);
}
@Transactional
public AgentRunAdmission admit(Long sessionId, InvokeCommand sourceCommand, InvokeCommand executionCommand) {
AgentSession session = sessionDao.findFirstById(sessionId)
.orElseThrow(() -> new IllegalStateException("Agent run session disappeared during admission"));
validateSessionIdentity(session, sourceCommand);
AgentApprovalHandling approvalHandling = approvalHandling(sourceCommand.replyMode());
Optional<AgentRun> existing = runDao.findBySessionIdAndMessageId(
session.getId(), sourceCommand.userInput().getMessageId());
if (existing.isPresent()) {
InvokeCommand replayCommand = replayCommand(sourceCommand, existing.get(), executionCommand);
if (replayCommand == null) {
return admission(Decision.REJECT_MISMATCH, session, existing.get(), approvalHandling);
}
String fingerprint = AgentRunRequestFingerprint.from(replayCommand, approvalHandling);
return admitExisting(session, existing.get(), replayCommand, approvalHandling, fingerprint);
}
if (executionCommand == null) {
throw new IllegalStateException("Canonical target is required for a new Agent run");
}
AgentRunRequestSnapshot requestSnapshot = AgentRunRequestFingerprint.snapshot(
executionCommand, approvalHandling);
String fingerprint = AgentRunRequestFingerprint.from(requestSnapshot);
return createAndAdmit(session, executionCommand, approvalHandling, fingerprint, requestSnapshot);
}
private InvokeCommand replayCommand(InvokeCommand sourceCommand, AgentRun run, InvokeCommand executionCommand) {
if (!targetCanonicalizationService.requiresCanonicalization(sourceCommand)) {
return executionCommand == null ? sourceCommand : executionCommand;
}
try {
return targetCanonicalizationService.replayCommand(sourceCommand, AgentRunService.targetFromRun(run));
} catch (IllegalArgumentException ignored) {
return null;
}
}
private AgentRunAdmission createAndAdmit(AgentSession session, InvokeCommand command,
AgentApprovalHandling approvalHandling, String fingerprint,
AgentRunRequestSnapshot requestSnapshot) {
List<TranscriptMessage> history = transcriptRecorder.chatHistory(session.getId());
AgentRun run = runService.createOrResumeRun(session, command.userInput(), command.entryType());
transcriptRecorder.recordUserTranscriptEntry(session, run, command.userInput(),
AgentRunRequestFingerprint.VERSION, fingerprint, requestSnapshot);
AgentRun running = runService.markRunning(run);
return new AgentRunAdmission(Decision.EXECUTE_NEW, session, running, approvalHandling, history);
}
private AgentRunAdmission admitExisting(AgentSession session, AgentRun run, InvokeCommand command,
AgentApprovalHandling approvalHandling, String fingerprint) {
if (!sameRunIdentity(session, run, command)) {
return admission(Decision.REJECT_MISMATCH, session, run, approvalHandling);
}
Optional<TranscriptMessage> requestMessage = transcriptRecorder.findRunRequestMessage(run.getId());
if (requestMessage.isEmpty()) {
if (isScheduleReservation(run, command)) {
return admitScheduleReservation(session, run, command, approvalHandling, fingerprint);
}
return admission(Decision.REJECT_MISMATCH, session, run, approvalHandling);
}
if (!matchesFingerprint(requestMessage.get(), fingerprint)) {
return admission(Decision.REJECT_MISMATCH, session, run, approvalHandling);
}
AgentRunStatus status = status(run);
if (status == AgentRunStatus.RUNNING) {
return admission(Decision.REPLAY_ACTIVE, session, run, approvalHandling);
}
if (status == AgentRunStatus.SUCCEEDED
|| status == AgentRunStatus.FAILED
|| status == AgentRunStatus.CANCELLED
|| status == AgentRunStatus.RECOVERY_REQUIRED) {
return admission(Decision.REPLAY_TERMINAL, session, run, approvalHandling);
}
return admission(Decision.REJECT_MISMATCH, session, run, approvalHandling);
}
private AgentRunAdmission admitScheduleReservation(AgentSession session, AgentRun run, InvokeCommand command,
AgentApprovalHandling approvalHandling, String fingerprint) {
List<TranscriptMessage> history = transcriptRecorder.chatHistory(session.getId());
transcriptRecorder.recordUserTranscriptEntry(session, run, command.userInput(),
AgentRunRequestFingerprint.VERSION, fingerprint);
AgentRun running = runService.markRunning(run);
return new AgentRunAdmission(Decision.EXECUTE_NEW, session, running, approvalHandling, history);
}
private boolean matchesFingerprint(TranscriptMessage requestMessage, String fingerprint) {
return Objects.equals(AgentRunRequestFingerprint.VERSION, requestMessage.getRequestFingerprintVersion())
&& Objects.equals(fingerprint, requestMessage.getRequestFingerprint());
}
private boolean sameRunIdentity(AgentSession session, AgentRun run, InvokeCommand command) {
return Objects.equals(session.getId(), run.getSessionId())
&& Objects.equals(command.userInput().getMessageId(), run.getMessageId())
&& Objects.equals(command.entryType().name(), run.getEntryType());
}
private boolean isScheduleReservation(AgentRun run, InvokeCommand command) {
return status(run) == AgentRunStatus.CREATED
&& command.entryType() == AgentRuntimeEntryType.SCHEDULE_TRIGGER
&& Objects.equals(AgentRuntimeEntryType.SCHEDULE_TRIGGER.name(), run.getEntryType())
&& Objects.equals(AgentRunService.targetFromRun(run), command.userInput().getTarget());
}
private AgentRunAdmission admission(Decision decision, AgentSession session, AgentRun run,
AgentApprovalHandling approvalHandling) {
return new AgentRunAdmission(decision, session, run, approvalHandling, List.of());
}
private AgentRunStatus status(AgentRun run) {
if (!StringUtils.hasText(run.getStatus())) {
throw new IllegalStateException("Agent run status is required during admission");
}
return AgentRunStatus.valueOf(run.getStatus());
}
private AgentApprovalHandling approvalHandling(ReplyMode replyMode) {
return replyMode == ReplyMode.STREAM
? AgentApprovalHandling.WAIT_FOR_DECISION
: AgentApprovalHandling.DENY;
}
private void validateSessionIdentity(AgentSession session, InvokeCommand command) {
AgentActor actor = command.envelope().getActor();
boolean matches = actor != null
&& Objects.equals(session.getWorkspaceId(), command.envelope().getWorkspaceId())
&& Objects.equals(session.getChannel(), command.envelope().getChannelId())
&& Objects.equals(session.getActorType(), actor.getType())
&& Objects.equals(session.getActorId(), actor.getId())
&& Objects.equals(session.getOriginEntryType(), command.entryType().name())
&& Objects.equals(session.getConversationId(), command.userInput().getConversationId());
if (!matches) {
throw new IllegalArgumentException("Agent run session identity does not match the command");
}
}
}
@@ -1,89 +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.gateway.application;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
import java.util.List;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.InvokeCommand;
import org.apache.hertzbeat.ai.gateway.contract.AgentRunRequestSnapshot;
import org.apache.hertzbeat.ai.gateway.runtime.AgentApprovalHandling;
import org.apache.hertzbeat.ai.gateway.text.GatewayText;
import org.apache.hertzbeat.common.util.JsonUtil;
/**
* Stable hash of fields that can change one runtime invocation's behavior.
*/
public final class AgentRunRequestFingerprint {
public static final String VERSION = "1";
private AgentRunRequestFingerprint() {
}
static String from(InvokeCommand command, AgentApprovalHandling approvalHandling) {
return from(snapshot(command, approvalHandling));
}
public static String from(AgentRunRequestSnapshot request) {
FingerprintMaterial material = new FingerprintMaterial(
request.entryType(), request.target(), request.alertIncident(), request.message(),
List.copyOf(request.attachments()), GatewayText.normalize(request.preferredLanguage()),
request.approvalHandling(), request.replyMode());
String canonical = JsonUtil.toJson(material);
if (!org.springframework.util.StringUtils.hasText(canonical)) {
throw new IllegalStateException("Agent run request fingerprint material cannot be serialized");
}
try {
byte[] digest = MessageDigest.getInstance("SHA-256")
.digest(canonical.getBytes(StandardCharsets.UTF_8));
return "sha256:" + HexFormat.of().formatHex(digest);
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException("SHA-256 is required for Agent run request identity", exception);
}
}
static AgentRunRequestSnapshot snapshot(InvokeCommand command, AgentApprovalHandling approvalHandling) {
return AgentRunRequestSnapshot.builder()
.version(AgentRunRequestSnapshot.VERSION)
.conversationId(command.userInput().getConversationId())
.messageId(command.userInput().getMessageId())
.entryType(command.entryType().name())
.target(command.userInput().getTarget())
.alertIncident(command.userInput().getAlertIncident())
.message(command.userInput().getMessage().getText())
.attachments(command.userInput().getMessage().getAttachments())
.preferredLanguage(GatewayText.normalize(command.envelope().getPreferredLanguage()))
.approvalHandling(approvalHandling.name())
.replyMode(command.replyMode().name())
.build();
}
private record FingerprintMaterial(
String entryType,
Object target,
Object alertIncident,
String message,
List<String> attachments,
String preferredLanguage,
String approvalHandling,
String replyMode) {
}
}
@@ -1,159 +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.gateway.application;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetAuthority;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetRef;
import org.apache.hertzbeat.alert.service.AlertService;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
/** Resolves and rechecks the persisted authority for an exact workspace-owned SingleAlert. */
@Service
public class AgentSingleAlertTargetAuthorityService {
public static final String TARGET_VERSION_PREFIX = "single-alert.";
public static final String AUTHORITY_VERSION_PREFIX = "single-alert-authority.";
public static final String TARGET_VERSION = "single-alert.v1";
public static final String AUTHORITY_VERSION = "single-alert-authority.v1";
public static final String ALERT_TYPE = "single";
private final AlertService alertService;
public AgentSingleAlertTargetAuthorityService(AlertService alertService) {
this.alertService = alertService;
}
public AgentTargetRef canonicalize(String workspaceId, long alertId) {
if (!StringUtils.hasText(workspaceId) || alertId <= 0) {
throw unavailable();
}
SingleAlert alert;
try {
alert = alertService.findSingleAlert(workspaceId, alertId)
.filter(candidate -> Objects.equals(workspaceId, candidate.getWorkspaceId()))
.filter(candidate -> Objects.equals(alertId, candidate.getId()))
.orElseThrow(this::unavailable);
} catch (UnavailableException failure) {
throw failure;
} catch (RuntimeException ignored) {
throw unavailable();
}
return AgentTargetRef.builder()
.version(TARGET_VERSION)
.alertId(alertId)
.alertType(ALERT_TYPE)
.authority(AgentTargetAuthority.builder()
.bindingId(alertId)
.version(AUTHORITY_VERSION)
.hash(authorityHash(workspaceId, alert))
.build())
.build();
}
public boolean verify(String workspaceId, AgentTargetRef target) {
if (!isCanonicalTarget(target)) {
return false;
}
try {
return matches(target, canonicalize(workspaceId, target.getAlertId()));
} catch (UnavailableException ignored) {
return false;
}
}
public boolean isCanonicalTarget(AgentTargetRef target) {
AgentTargetAuthority authority = target == null ? null : target.getAuthority();
return target != null
&& TARGET_VERSION.equals(target.getVersion())
&& target.getAlertId() != null && target.getAlertId() > 0
&& ALERT_TYPE.equals(target.getAlertType())
&& authority != null
&& Objects.equals(target.getAlertId(), authority.getBindingId())
&& AUTHORITY_VERSION.equals(authority.getVersion())
&& StringUtils.hasText(authority.getHash())
&& target.getMonitorId() == null && target.getEntityId() == null
&& target.getCollector() == null && target.getSignal() == null
&& target.getTopology() == null && target.getTrace() == null && target.getLog() == null
&& target.getService() == null;
}
public boolean matches(AgentTargetRef expected, AgentTargetRef actual) {
return isCanonicalTarget(expected) && Objects.equals(expected, actual);
}
private String authorityHash(String workspaceId, SingleAlert alert) {
StringBuilder material = new StringBuilder("single-alert-authority.v1;");
append(material, "workspace", workspaceId);
append(material, "id", alert.getId());
append(material, "fingerprint", alert.getFingerprint());
append(material, "status", alert.getStatus());
append(material, "content", alert.getContent());
append(material, "triggerTimes", alert.getTriggerTimes());
append(material, "startAt", alert.getStartAt());
append(material, "activeAt", alert.getActiveAt());
append(material, "endAt", alert.getEndAt());
appendMap(material, "labels", alert.getLabels());
appendMap(material, "annotations", alert.getAnnotations());
try {
return "sha256:" + HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
.digest(material.toString().getBytes(StandardCharsets.UTF_8)));
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException("SHA-256 is required for alert target authority", exception);
}
}
private void appendMap(StringBuilder material, String field, Map<String, String> values) {
append(material, field + "Size", values == null ? null : values.size());
if (values != null) {
new TreeMap<>(values).forEach((key, value) -> {
append(material, field + "Key", key);
append(material, field + "Value", value);
});
}
}
private void append(StringBuilder material, String field, Object value) {
String text = value == null ? null : String.valueOf(value);
material.append(field).append(':').append(text == null ? -1 : text.length()).append(':');
if (text != null) {
material.append(text);
}
material.append(';');
}
private UnavailableException unavailable() {
return new UnavailableException();
}
/** Cause-free failure used at the channel boundary. */
public static final class UnavailableException extends IllegalArgumentException {
UnavailableException() {
super("Single alert target is unavailable");
}
}
}
@@ -1,506 +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.gateway.application;
import java.util.Objects;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.InvokeCommand;
import org.apache.hertzbeat.ai.gateway.contract.AgentServiceRef;
import org.apache.hertzbeat.ai.gateway.contract.AgentSignalRef;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetAuthority;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetRef;
import org.apache.hertzbeat.ai.gateway.contract.UserInput;
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeEntryType;
import org.apache.hertzbeat.ai.gateway.text.GatewayText;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.hertzbeat.manager.service.entity.EntityMonitorMetricTargetCanonicalizer;
import org.apache.hertzbeat.manager.service.metric.MonitorMetricQueryContract;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
/** Adapts untrusted Gateway monitor-metric intent to the manager-authoritative target contract. */
@Service
public class AgentTargetCanonicalizationService {
private final EntityMonitorMetricTargetCanonicalizer canonicalizer;
private final AgentSingleAlertTargetAuthorityService alertAuthorityService;
private final AgentEntityTargetAuthorityService entityAuthorityService;
private final AgentTopologyTargetAuthorityService topologyAuthorityService;
private final AgentTraceTargetCanonicalizationAdapter traceAdapter;
private final AgentLogTargetCanonicalizationAdapter logAdapter;
public AgentTargetCanonicalizationService(EntityMonitorMetricTargetCanonicalizer canonicalizer,
AgentSingleAlertTargetAuthorityService alertAuthorityService,
AgentEntityTargetAuthorityService entityAuthorityService,
AgentTopologyTargetAuthorityService topologyAuthorityService) {
this(canonicalizer, alertAuthorityService, entityAuthorityService, topologyAuthorityService, null, null);
}
public AgentTargetCanonicalizationService(EntityMonitorMetricTargetCanonicalizer canonicalizer,
AgentSingleAlertTargetAuthorityService alertAuthorityService,
AgentEntityTargetAuthorityService entityAuthorityService,
AgentTopologyTargetAuthorityService topologyAuthorityService,
AgentTraceTargetCanonicalizationAdapter traceAdapter) {
this(canonicalizer, alertAuthorityService, entityAuthorityService, topologyAuthorityService,
traceAdapter, null);
}
@Autowired
public AgentTargetCanonicalizationService(EntityMonitorMetricTargetCanonicalizer canonicalizer,
AgentSingleAlertTargetAuthorityService alertAuthorityService,
AgentEntityTargetAuthorityService entityAuthorityService,
AgentTopologyTargetAuthorityService topologyAuthorityService,
AgentTraceTargetCanonicalizationAdapter traceAdapter,
AgentLogTargetCanonicalizationAdapter logAdapter) {
this.canonicalizer = canonicalizer;
this.alertAuthorityService = alertAuthorityService;
this.entityAuthorityService = entityAuthorityService;
this.topologyAuthorityService = topologyAuthorityService;
this.traceAdapter = traceAdapter;
this.logAdapter = logAdapter;
}
public boolean requiresCanonicalization(InvokeCommand command) {
AgentTargetRef target = command.userInput().getTarget();
AgentSignalRef signal = target == null ? null : target.getSignal();
return command.entryType() == AgentRuntimeEntryType.USER_INPUT
&& target != null && (hasCanonicalMarker(target) || isAlertIntent(target) || isTopologyIntent(target)
|| isEntityIntent(target) || traceAdapter != null && traceAdapter.isIntent(target)
|| logAdapter != null && logAdapter.isIntent(target)
|| target.getMonitorId() != null && signal != null && "metrics".equals(signal.getType()));
}
public InvokeCommand canonicalize(InvokeCommand sourceCommand) {
if (isAlertIntent(sourceCommand.userInput().getTarget())) {
return canonicalizeAlert(sourceCommand);
}
if (isTopologyIntent(sourceCommand.userInput().getTarget())) {
return canonicalizeTopology(sourceCommand);
}
if (traceAdapter != null && traceAdapter.isIntent(sourceCommand.userInput().getTarget())) {
return traceAdapter.canonicalize(sourceCommand);
}
if (logAdapter != null && logAdapter.isIntent(sourceCommand.userInput().getTarget())) {
return logAdapter.canonicalize(sourceCommand);
}
if (isEntityIntent(sourceCommand.userInput().getTarget())) {
return canonicalizeEntity(sourceCommand);
}
AgentTargetRef source = requireSourceIntent(sourceCommand.userInput().getTarget());
AgentSignalRef signal = source.getSignal();
EntityMonitorMetricTargetCanonicalizer.CanonicalTarget canonical;
try {
canonical = canonicalizer.canonicalize(sourceCommand.envelope().getWorkspaceId(),
new EntityMonitorMetricTargetCanonicalizer.SourceIntent(source.getMonitorId(), signal.getType(),
signal.getQuery(), signal.getStart(), signal.getEnd(), signal.getTimezone()));
} catch (EntityMonitorMetricTargetCanonicalizer.CanonicalizationException failure) {
throw new TargetCanonicalizationException(
failure.kind() == EntityMonitorMetricTargetCanonicalizer.FailureKind.MISMATCH
? FailureKind.MISMATCH : FailureKind.UNAVAILABLE);
} catch (RuntimeException ignored) {
throw unavailable();
}
if (canonical == null || canonical.service() == null || canonical.signal() == null
|| canonical.authority() == null) {
throw unavailable();
}
AgentTargetRef target = AgentTargetRef.builder()
.version(canonical.version())
.entityId(canonical.entityId())
.monitorId(canonical.monitorId())
.service(AgentServiceRef.builder()
.name(canonical.service().name())
.namespace(canonical.service().namespace())
.environment(canonical.service().environment())
.build())
.signal(AgentSignalRef.builder()
.type(canonical.signal().type())
.query(canonical.signal().query())
.start(canonical.signal().start())
.end(canonical.signal().end())
.timezone(canonical.signal().timezone())
.build())
.authority(AgentTargetAuthority.builder()
.bindingId(canonical.authority().bindingId())
.version(canonical.authority().version())
.hash(canonical.authority().hash())
.build())
.build();
requireSafeCanonicalTarget(target);
return withTarget(sourceCommand, target);
}
public InvokeCommand replayCommand(InvokeCommand sourceCommand, AgentTargetRef persistedTarget) {
if (isAlertIntent(sourceCommand.userInput().getTarget())) {
AgentTargetRef source = requireAlertSourceIntent(sourceCommand.userInput().getTarget());
if (!isCanonicalAlertTarget(persistedTarget)
|| !Objects.equals(alertSourceIntent(source), alertSourceIntent(persistedTarget))) {
throw mismatch();
}
return withTarget(sourceCommand, persistedTarget);
}
if (isTopologyIntent(sourceCommand.userInput().getTarget())) {
AgentTargetRef source = requireTopologySourceIntent(sourceCommand.userInput().getTarget());
if (!isCanonicalTopologyTarget(persistedTarget)
|| !Objects.equals(normalizedTopologySourceIntent(source), topologySourceIntent(persistedTarget))) {
throw mismatch();
}
return withTarget(sourceCommand, persistedTarget);
}
if (traceAdapter != null && traceAdapter.isIntent(sourceCommand.userInput().getTarget())) {
return traceAdapter.replayCommand(sourceCommand, persistedTarget);
}
if (logAdapter != null && logAdapter.isIntent(sourceCommand.userInput().getTarget())) {
return logAdapter.replayCommand(sourceCommand, persistedTarget);
}
if (isEntityIntent(sourceCommand.userInput().getTarget())) {
AgentTargetRef source = requireEntitySourceIntent(sourceCommand.userInput().getTarget());
if (!isCanonicalEntityTarget(persistedTarget)
|| !Objects.equals(entitySourceIntent(source), entitySourceIntent(persistedTarget))) {
throw mismatch();
}
return withTarget(sourceCommand, persistedTarget);
}
AgentTargetRef source = requireSourceIntent(sourceCommand.userInput().getTarget());
if (!isCanonicalTarget(persistedTarget)
|| !Objects.equals(sourceIntent(source), sourceIntent(persistedTarget))) {
throw mismatch();
}
return withTarget(sourceCommand, persistedTarget);
}
public static AgentTargetRef retrySourceIntent(AgentTargetRef target) {
if (isCanonicalAlertTarget(target)) {
return alertSourceIntent(target);
}
if (isCanonicalEntityTarget(target)) {
return entitySourceIntent(target);
}
if (isCanonicalTopologyTarget(target)) {
return topologySourceIntent(target);
}
if (AgentTraceTargetAuthorityService.TARGET_VERSION.equals(target == null ? null : target.getVersion())) {
return AgentTraceTargetCanonicalizationAdapter.sourceIntent(target);
}
if (AgentLogTargetAuthorityService.TARGET_VERSION.equals(target == null ? null : target.getVersion())) {
return AgentLogTargetCanonicalizationAdapter.sourceIntent(target);
}
return isCanonicalTarget(target) ? sourceIntent(target) : target;
}
public static boolean isCanonicalTarget(AgentTargetRef target) {
return target != null
&& EntityMonitorMetricTargetCanonicalizer.TARGET_VERSION.equals(target.getVersion())
&& target.getEntityId() != null && target.getMonitorId() != null
&& target.getService() != null && target.getAuthority() != null
&& target.getSignal() != null && "metrics".equals(target.getSignal().getType())
&& target.getAlertId() == null && target.getAlertType() == null
&& target.getCollector() == null && target.getTopology() == null && target.getTrace() == null
&& target.getLog() == null;
}
public static boolean isCanonicalAlertTarget(AgentTargetRef target) {
AgentTargetAuthority authority = target == null ? null : target.getAuthority();
return target != null
&& AgentSingleAlertTargetAuthorityService.TARGET_VERSION.equals(target.getVersion())
&& target.getAlertId() != null && target.getAlertId() > 0
&& AgentSingleAlertTargetAuthorityService.ALERT_TYPE.equals(target.getAlertType())
&& authority != null
&& Objects.equals(target.getAlertId(), authority.getBindingId())
&& AgentSingleAlertTargetAuthorityService.AUTHORITY_VERSION.equals(authority.getVersion())
&& StringUtils.hasText(authority.getHash())
&& target.getMonitorId() == null && target.getEntityId() == null
&& target.getCollector() == null && target.getSignal() == null
&& target.getTopology() == null && target.getTrace() == null && target.getLog() == null
&& target.getService() == null;
}
public static boolean isCanonicalEntityTarget(AgentTargetRef target) {
AgentTargetAuthority authority = target == null ? null : target.getAuthority();
return target != null
&& AgentEntityTargetAuthorityService.TARGET_VERSION.equals(target.getVersion())
&& target.getEntityId() != null && target.getEntityId() > 0
&& authority != null
&& Objects.equals(target.getEntityId(), authority.getBindingId())
&& AgentEntityTargetAuthorityService.AUTHORITY_VERSION.equals(authority.getVersion())
&& StringUtils.hasText(authority.getHash())
&& target.getMonitorId() == null && target.getAlertId() == null
&& target.getAlertType() == null && target.getCollector() == null
&& target.getSignal() == null && target.getTopology() == null && target.getTrace() == null
&& target.getLog() == null
&& target.getService() == null;
}
public static boolean isCanonicalTopologyTarget(AgentTargetRef target) {
AgentTargetAuthority authority = target == null ? null : target.getAuthority();
return target != null
&& AgentTopologyTargetAuthorityService.TARGET_VERSION.equals(target.getVersion())
&& target.getEntityId() != null && target.getEntityId() > 0
&& target.getTopology() != null
&& Objects.equals(target.getEntityId(), target.getTopology().getRootEntityId())
&& authority != null
&& Objects.equals(target.getEntityId(), authority.getBindingId())
&& AgentTopologyTargetAuthorityService.AUTHORITY_VERSION.equals(authority.getVersion())
&& StringUtils.hasText(authority.getHash())
&& target.getMonitorId() == null && target.getAlertId() == null && target.getAlertType() == null
&& target.getCollector() == null && target.getSignal() == null && target.getTrace() == null
&& target.getLog() == null
&& target.getService() == null;
}
private AgentTargetRef requireSourceIntent(AgentTargetRef target) {
if (target == null || target.getMonitorId() == null || target.getSignal() == null
|| target.getVersion() != null || target.getEntityId() != null || target.getService() != null
|| target.getAuthority() != null || target.getAlertId() != null || target.getAlertType() != null
|| target.getCollector() != null
|| target.getTopology() != null || target.getTrace() != null || target.getLog() != null
|| target.getSignal().getTimeRange() != null
|| !MonitorMetricQueryContract.isExactWindowAllowed(
target.getSignal().getStart(), target.getSignal().getEnd())) {
throw mismatch();
}
return target;
}
private static AgentTargetRef sourceIntent(AgentTargetRef target) {
AgentSignalRef signal = target.getSignal();
return AgentTargetRef.builder()
.monitorId(target.getMonitorId())
.signal(AgentSignalRef.builder()
.type(signal.getType())
.query(signal.getQuery())
.start(signal.getStart())
.end(signal.getEnd())
.timezone(signal.getTimezone())
.build())
.build();
}
private InvokeCommand canonicalizeAlert(InvokeCommand command) {
AgentTargetRef source = requireAlertSourceIntent(command.userInput().getTarget());
if (alertAuthorityService == null) {
throw unavailable();
}
try {
AgentTargetRef target = alertAuthorityService.canonicalize(
command.envelope().getWorkspaceId(), source.getAlertId());
requireSafeCanonicalTarget(target);
return withTarget(command, target);
} catch (AgentSingleAlertTargetAuthorityService.UnavailableException failure) {
throw unavailable();
} catch (RuntimeException ignored) {
throw unavailable();
}
}
private AgentTargetRef requireAlertSourceIntent(AgentTargetRef target) {
if (target == null || target.getAlertId() == null || target.getAlertId() <= 0
|| !AgentSingleAlertTargetAuthorityService.ALERT_TYPE.equals(target.getAlertType())
|| target.getVersion() != null || target.getAuthority() != null
|| target.getMonitorId() != null || target.getEntityId() != null
|| target.getCollector() != null || target.getSignal() != null
|| target.getTopology() != null || target.getTrace() != null || target.getLog() != null
|| target.getService() != null) {
throw unavailable();
}
return target;
}
private static AgentTargetRef alertSourceIntent(AgentTargetRef target) {
return AgentTargetRef.builder()
.alertId(target.getAlertId())
.alertType(AgentSingleAlertTargetAuthorityService.ALERT_TYPE)
.build();
}
private InvokeCommand canonicalizeEntity(InvokeCommand command) {
AgentTargetRef source = requireEntitySourceIntent(command.userInput().getTarget());
if (entityAuthorityService == null) {
throw unavailable();
}
try {
AgentTargetRef target = entityAuthorityService.canonicalize(
command.envelope().getWorkspaceId(), source.getEntityId());
requireSafeCanonicalTarget(target);
return withTarget(command, target);
} catch (AgentEntityTargetAuthorityService.UnavailableException failure) {
throw unavailable();
} catch (RuntimeException ignored) {
throw unavailable();
}
}
private AgentTargetRef requireEntitySourceIntent(AgentTargetRef target) {
if (target == null || target.getEntityId() == null || target.getEntityId() <= 0
|| target.getVersion() != null || target.getAuthority() != null
|| target.getMonitorId() != null || target.getAlertId() != null || target.getAlertType() != null
|| target.getCollector() != null || target.getSignal() != null
|| target.getTopology() != null || target.getTrace() != null || target.getLog() != null
|| target.getService() != null) {
throw unavailable();
}
return target;
}
private static AgentTargetRef entitySourceIntent(AgentTargetRef target) {
return AgentTargetRef.builder().entityId(target.getEntityId()).build();
}
private InvokeCommand canonicalizeTopology(InvokeCommand command) {
AgentTargetRef source = requireTopologySourceIntent(command.userInput().getTarget());
if (topologyAuthorityService == null) {
throw unavailable();
}
try {
AgentTargetRef target = topologyAuthorityService.canonicalize(
command.envelope().getWorkspaceId(), source.getTopology());
requireSafeCanonicalTarget(target);
return withTarget(command, target);
} catch (AgentTopologyTargetAuthorityService.UnavailableException failure) {
throw unavailable();
} catch (RuntimeException ignored) {
throw unavailable();
}
}
private AgentTargetRef requireTopologySourceIntent(AgentTargetRef target) {
if (target == null || target.getTopology() == null
|| target.getTopology().getRootEntityId() == null || target.getTopology().getRootEntityId() <= 0
|| target.getTopology().getDepth() == null || target.getTopology().getDepth() < 1
|| target.getTopology().getDepth() > 2
|| target.getTopology().getNodeId() != null && target.getTopology().getEdgeId() != null
|| target.getVersion() != null || target.getAuthority() != null || target.getEntityId() != null
|| target.getMonitorId() != null || target.getAlertId() != null || target.getAlertType() != null
|| target.getCollector() != null || target.getSignal() != null || target.getTrace() != null
|| target.getLog() != null
|| target.getService() != null) {
throw unavailable();
}
return target;
}
private static AgentTargetRef topologySourceIntent(AgentTargetRef target) {
return AgentTargetRef.builder().topology(target.getTopology()).build();
}
private AgentTargetRef normalizedTopologySourceIntent(AgentTargetRef target) {
if (topologyAuthorityService == null) {
throw mismatch();
}
try {
return AgentTargetRef.builder()
.topology(topologyAuthorityService.normalizeSource(target.getTopology()))
.build();
} catch (RuntimeException ignored) {
throw mismatch();
}
}
private boolean isAlertIntent(AgentTargetRef target) {
if (target == null) {
return false;
}
AgentTargetAuthority authority = target.getAuthority();
return target.getAlertId() != null || target.getAlertType() != null
|| hasSingleAlertVersion(target.getVersion(), AgentSingleAlertTargetAuthorityService.TARGET_VERSION_PREFIX)
|| authority != null && hasSingleAlertVersion(
authority.getVersion(), AgentSingleAlertTargetAuthorityService.AUTHORITY_VERSION_PREFIX);
}
private boolean isEntityIntent(AgentTargetRef target) {
if (target == null) {
return false;
}
AgentTargetAuthority authority = target.getAuthority();
return target.getTopology() == null && target.getTrace() == null && target.getLog() == null
&& target.getEntityId() != null && (target.getMonitorId() == null || target.getSignal() == null)
|| hasEntityVersion(target.getVersion(), AgentEntityTargetAuthorityService.TARGET_VERSION_PREFIX)
|| authority != null && hasEntityVersion(
authority.getVersion(), AgentEntityTargetAuthorityService.AUTHORITY_VERSION_PREFIX);
}
private boolean isTopologyIntent(AgentTargetRef target) {
if (target == null) {
return false;
}
AgentTargetAuthority authority = target.getAuthority();
return target.getTopology() != null
|| hasTopologyVersion(target.getVersion(), AgentTopologyTargetAuthorityService.TARGET_VERSION_PREFIX)
|| authority != null && hasTopologyVersion(
authority.getVersion(), AgentTopologyTargetAuthorityService.AUTHORITY_VERSION_PREFIX);
}
private boolean hasCanonicalMarker(AgentTargetRef target) {
return target.getVersion() != null || target.getAuthority() != null;
}
private boolean hasSingleAlertVersion(String version, String prefix) {
return StringUtils.hasText(version) && version.startsWith(prefix);
}
private boolean hasEntityVersion(String version, String prefix) {
return StringUtils.hasText(version) && version.startsWith(prefix);
}
private boolean hasTopologyVersion(String version, String prefix) {
return StringUtils.hasText(version) && version.startsWith(prefix);
}
private InvokeCommand withTarget(InvokeCommand command, AgentTargetRef target) {
UserInput userInput = command.userInput().toBuilder().target(target).build();
return new InvokeCommand(command.envelope(), command.replyMode(), command.commandId(), userInput,
command.entryType());
}
private void requireSafeCanonicalTarget(AgentTargetRef target) {
String json = JsonUtil.toJson(target);
if (!StringUtils.hasText(json) || !Objects.equals(json, GatewayText.redactSecrets(json))) {
throw unavailable();
}
}
private TargetCanonicalizationException mismatch() {
return new TargetCanonicalizationException(FailureKind.MISMATCH);
}
private TargetCanonicalizationException unavailable() {
return new TargetCanonicalizationException(FailureKind.UNAVAILABLE);
}
/** Stable cause-free failure categories for channel presentation. */
public enum FailureKind {
MISMATCH,
UNAVAILABLE
}
/** Cause-free exception used at the channel boundary. */
public static final class TargetCanonicalizationException extends IllegalArgumentException {
private final FailureKind kind;
public TargetCanonicalizationException(FailureKind kind) {
super(kind == FailureKind.MISMATCH ? "Investigation target does not match"
: "Investigation target is unavailable");
this.kind = kind;
}
public FailureKind kind() {
return kind;
}
}
}
@@ -1,214 +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.gateway.application;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.util.HexFormat;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetAuthority;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetRef;
import org.apache.hertzbeat.ai.gateway.contract.AgentTopologyRef;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
/** Resolves a focused Topology scope through its workspace-owned root Entity. */
@Service
public class AgentTopologyTargetAuthorityService {
public static final String TARGET_VERSION_PREFIX = "topology.";
public static final String AUTHORITY_VERSION_PREFIX = "topology-authority.";
public static final String TARGET_VERSION = "topology.v1";
public static final String AUTHORITY_VERSION = "topology-authority.v1";
private static final long MAX_RANGE_MILLIS = Duration.ofDays(7).toMillis();
private static final Set<String> SOURCE_KINDS = Set.of(
"all", "alert-impact", "entity-relation", "monitor-bind", "monitor-ownership",
"otlp-trace-call", "k8s-workload", "cmdb-manual-label", "database-middleware-connection",
"template-dependency");
private final AgentEntityTargetAuthorityService entityAuthorityService;
public AgentTopologyTargetAuthorityService(AgentEntityTargetAuthorityService entityAuthorityService) {
this.entityAuthorityService = entityAuthorityService;
}
public AgentTargetRef canonicalize(String workspaceId, AgentTopologyRef source) {
AgentTopologyRef topology = normalizeSource(source);
AgentTargetRef entityTarget;
try {
entityTarget = entityAuthorityService.canonicalize(workspaceId, topology.getRootEntityId());
} catch (RuntimeException ignored) {
throw unavailable();
}
if (!entityAuthorityService.isCanonicalTarget(entityTarget)
|| !Objects.equals(topology.getRootEntityId(), entityTarget.getEntityId())) {
throw unavailable();
}
return AgentTargetRef.builder()
.version(TARGET_VERSION)
.entityId(topology.getRootEntityId())
.topology(topology)
.authority(AgentTargetAuthority.builder()
.bindingId(topology.getRootEntityId())
.version(AUTHORITY_VERSION)
.hash(authorityHash(entityTarget.getAuthority().getHash(), topology))
.build())
.build();
}
public boolean verify(String workspaceId, AgentTargetRef target) {
if (!isCanonicalTarget(target)) {
return false;
}
try {
return Objects.equals(target, canonicalize(workspaceId, target.getTopology()));
} catch (UnavailableException ignored) {
return false;
}
}
public boolean isCanonicalTarget(AgentTargetRef target) {
AgentTargetAuthority authority = target == null ? null : target.getAuthority();
if (target == null || !TARGET_VERSION.equals(target.getVersion())
|| target.getEntityId() == null || target.getEntityId() <= 0
|| target.getTopology() == null || authority == null
|| !Objects.equals(target.getEntityId(), target.getTopology().getRootEntityId())
|| !Objects.equals(target.getEntityId(), authority.getBindingId())
|| !AUTHORITY_VERSION.equals(authority.getVersion())
|| authority.getHash() == null || !authority.getHash().matches("sha256:[0-9a-f]{64}")
|| target.getMonitorId() != null || target.getAlertId() != null || target.getAlertType() != null
|| target.getCollector() != null || target.getSignal() != null || target.getTrace() != null
|| target.getLog() != null
|| target.getService() != null) {
return false;
}
try {
return Objects.equals(target.getTopology(), normalizeSource(target.getTopology()));
} catch (UnavailableException ignored) {
return false;
}
}
public AgentTopologyRef normalizeSource(AgentTopologyRef source) {
if (source == null || source.getRootEntityId() == null || source.getRootEntityId() <= 0
|| source.getDepth() == null || source.getDepth() < 1 || source.getDepth() > 2
|| source.getNodeId() != null && source.getEdgeId() != null) {
throw unavailable();
}
String nodeId = text(source.getNodeId(), 512);
String edgeId = text(source.getEdgeId(), 512);
String environment = text(source.getEnvironment(), 128);
String relationType = text(source.getRelationType(), 128);
String sourceKind = source.getSourceKind() == null
? "entity-relation" : text(source.getSourceKind(), 64);
if (sourceKind == null || !SOURCE_KINDS.contains(sourceKind.toLowerCase(Locale.ROOT))) {
throw unavailable();
}
sourceKind = sourceKind.toLowerCase(Locale.ROOT);
validateRange(source.getStart(), source.getEnd());
int pageIndex = source.getPageIndex() == null ? 0 : source.getPageIndex();
int pageSize = source.getPageSize() == null ? 50 : source.getPageSize();
if (pageIndex < 0 || pageIndex > 10_000 || pageSize < 1 || pageSize > 100) {
throw unavailable();
}
return AgentTopologyRef.builder()
.rootEntityId(source.getRootEntityId())
.nodeId(nodeId)
.edgeId(edgeId)
.depth(source.getDepth())
.environment(environment)
.sourceKind(sourceKind)
.start(source.getStart())
.end(source.getEnd())
.relationType(relationType)
.hideInternal(Boolean.TRUE.equals(source.getHideInternal()))
.pageIndex(pageIndex)
.pageSize(pageSize)
.build();
}
private void validateRange(Long start, Long end) {
if (start == null && end == null) {
return;
}
if (start == null || end == null || start <= 0 || end <= start || end - start > MAX_RANGE_MILLIS) {
throw unavailable();
}
}
private String text(String value, int maximumLength) {
if (value == null) {
return null;
}
String normalized = value.trim();
if (!StringUtils.hasText(normalized) || normalized.length() > maximumLength
|| normalized.codePoints().anyMatch(code -> code < 32 || code == 127)) {
throw unavailable();
}
return normalized;
}
private String authorityHash(String entityAuthorityHash, AgentTopologyRef topology) {
StringBuilder material = new StringBuilder("topology-authority.v1;");
append(material, "entityAuthority", entityAuthorityHash);
append(material, "rootEntityId", topology.getRootEntityId());
append(material, "nodeId", topology.getNodeId());
append(material, "edgeId", topology.getEdgeId());
append(material, "depth", topology.getDepth());
append(material, "environment", topology.getEnvironment());
append(material, "sourceKind", topology.getSourceKind());
append(material, "start", topology.getStart());
append(material, "end", topology.getEnd());
append(material, "relationType", topology.getRelationType());
append(material, "hideInternal", topology.getHideInternal());
append(material, "pageIndex", topology.getPageIndex());
append(material, "pageSize", topology.getPageSize());
try {
return "sha256:" + HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
.digest(material.toString().getBytes(StandardCharsets.UTF_8)));
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException("SHA-256 is required for topology target authority", exception);
}
}
private void append(StringBuilder material, String field, Object value) {
String text = value == null ? null : String.valueOf(value);
material.append(field).append(':').append(text == null ? -1 : text.length()).append(':');
if (text != null) {
material.append(text);
}
material.append(';');
}
private UnavailableException unavailable() {
return new UnavailableException();
}
/** Cause-free failure used at the channel boundary. */
public static final class UnavailableException extends IllegalArgumentException {
UnavailableException() {
super("Topology target is unavailable");
}
}
}
@@ -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.ai.gateway.application;
import java.time.Duration;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import java.util.regex.Pattern;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetAuthority;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetRef;
import org.apache.hertzbeat.ai.gateway.contract.AgentTraceRef;
import org.apache.hertzbeat.ai.gateway.text.GatewayText;
import org.apache.hertzbeat.common.observability.dto.trace.TraceDetailDto;
import org.apache.hertzbeat.common.observability.dto.trace.TraceSpanNodeDto;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.hertzbeat.observability.traces.service.EntityTraceQueryService;
import org.apache.hertzbeat.observability.traces.service.EntityTraceQueryService.TraceDetailQuery;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
/** Resolves one exact Trace Explore detail scope through the trusted-workspace query boundary. */
@Service
public class AgentTraceTargetAuthorityService {
public static final String TARGET_VERSION_PREFIX = "trace-detail.";
public static final String AUTHORITY_VERSION_PREFIX = "trace-detail-authority.";
public static final String TARGET_VERSION = "trace-detail.v1";
public static final String AUTHORITY_VERSION = "trace-detail-authority.v1";
private static final long MAX_RANGE_MILLIS = Duration.ofDays(7).toMillis();
private static final Pattern SAFE_ID = Pattern.compile("[A-Za-z0-9][A-Za-z0-9._:-]{0,127}");
private final EntityTraceQueryService traceQueryService;
public AgentTraceTargetAuthorityService(EntityTraceQueryService traceQueryService) {
this.traceQueryService = traceQueryService;
}
public AgentTargetRef canonicalize(String workspaceId, AgentTraceRef source) {
AgentTraceRef trace = normalizeSource(source);
TraceDetailDto detail;
try {
detail = traceQueryService.getTraceDetail(workspaceId, detailQuery(trace));
} catch (RuntimeException ignored) {
throw unavailable();
}
if (detail == null || !Objects.equals(trace.getTraceId(), detail.getTraceId())) {
throw unavailable();
}
return AgentTargetRef.builder()
.version(TARGET_VERSION)
.trace(trace)
.authority(AgentTargetAuthority.builder()
.version(AUTHORITY_VERSION)
.hash("sha256:" + GatewayText.sha256(authorityMaterial(workspaceId, trace, detail)))
.build())
.build();
}
public boolean verify(String workspaceId, AgentTargetRef target) {
if (!isCanonicalTarget(target)) {
return false;
}
try {
return Objects.equals(target, canonicalize(workspaceId, target.getTrace()));
} catch (UnavailableException ignored) {
return false;
}
}
public boolean isCanonicalTarget(AgentTargetRef target) {
AgentTargetAuthority authority = target == null ? null : target.getAuthority();
if (target == null || !TARGET_VERSION.equals(target.getVersion()) || target.getTrace() == null
|| authority == null || authority.getBindingId() != null
|| !AUTHORITY_VERSION.equals(authority.getVersion())
|| authority.getHash() == null || !authority.getHash().matches("sha256:[0-9a-f]{64}")
|| target.getMonitorId() != null || target.getAlertId() != null || target.getAlertType() != null
|| target.getEntityId() != null || target.getCollector() != null || target.getSignal() != null
|| target.getTopology() != null || target.getLog() != null || target.getService() != null) {
return false;
}
try {
return Objects.equals(target.getTrace(), normalizeSource(target.getTrace()));
} catch (UnavailableException ignored) {
return false;
}
}
public AgentTraceRef normalizeSource(AgentTraceRef source) {
if (source == null || !safeId(source.getTraceId()) || source.getSpanId() != null && !safeId(source.getSpanId())
|| source.getStart() == null || source.getEnd() == null || source.getStart() <= 0
|| source.getEnd() <= source.getStart() || source.getEnd() - source.getStart() > MAX_RANGE_MILLIS
|| source.getMinDurationMs() != null && source.getMinDurationMs() < 0
|| source.getMaxDurationMs() != null && source.getMaxDurationMs() < 0
|| source.getMinDurationMs() != null && source.getMaxDurationMs() != null
&& source.getMinDurationMs() > source.getMaxDurationMs()) {
throw unavailable();
}
return AgentTraceRef.builder()
.traceId(source.getTraceId())
.spanId(source.getSpanId())
.start(source.getStart())
.end(source.getEnd())
.serviceName(text(source.getServiceName(), 512))
.serviceNamespace(text(source.getServiceNamespace(), 512))
.environment(text(source.getEnvironment(), 512))
.resourceFilter(text(source.getResourceFilter(), 2048))
.attributeFilter(text(source.getAttributeFilter(), 2048))
.minDurationMs(source.getMinDurationMs())
.maxDurationMs(source.getMaxDurationMs())
.build();
}
private TraceDetailQuery detailQuery(AgentTraceRef trace) {
return new TraceDetailQuery(null, trace.getTraceId(), trace.getSpanId(), trace.getStart(), trace.getEnd(),
trace.getServiceName(), trace.getServiceNamespace(), trace.getEnvironment(), trace.getResourceFilter(),
trace.getAttributeFilter(), trace.getMinDurationMs(), trace.getMaxDurationMs());
}
private String authorityMaterial(String workspaceId, AgentTraceRef trace, TraceDetailDto detail) {
StringBuilder material = new StringBuilder("trace-detail-authority.v1;");
append(material, "workspaceId", workspaceId);
append(material, "scope", JsonUtil.toJson(trace));
append(material, "traceId", detail.getTraceId());
append(material, "rootSpanId", detail.getRootSpanId());
append(material, "serviceName", detail.getServiceName());
append(material, "serviceNamespace", detail.getServiceNamespace());
append(material, "rootSpanName", detail.getRootSpanName());
append(material, "durationNanos", detail.getDurationNanos());
append(material, "status", detail.getStatus());
append(material, "startTime", detail.getStartTime());
append(material, "errorSpanCount", detail.getErrorSpanCount());
append(material, "resourceAttributes", sorted(detail.getResourceAttributes()));
if (detail.getSpans() != null) {
for (TraceSpanNodeDto span : detail.getSpans()) {
appendSpan(material, span);
}
}
return material.toString();
}
private void appendSpan(StringBuilder material, TraceSpanNodeDto span) {
append(material, "span.traceId", span == null ? null : span.getTraceId());
append(material, "span.spanId", span == null ? null : span.getSpanId());
append(material, "span.parentSpanId", span == null ? null : span.getParentSpanId());
append(material, "span.name", span == null ? null : span.getSpanName());
append(material, "span.service", span == null ? null : span.getServiceName());
append(material, "span.status", span == null ? null : span.getStatus());
append(material, "span.kind", span == null ? null : span.getSpanKind());
append(material, "span.duration", span == null ? null : span.getDurationNanos());
append(material, "span.start", span == null ? null : span.getStartTime());
append(material, "span.resource", span == null ? null : sorted(span.getResourceAttributes()));
append(material, "span.attributes", span == null ? null : sorted(span.getSpanAttributes()));
}
private Map<String, String> sorted(Map<String, String> values) {
return values == null ? Map.of() : new TreeMap<>(values);
}
private String text(String value, int maximumLength) {
if (value == null) {
return null;
}
String normalized = value.trim();
if (!StringUtils.hasText(normalized) || normalized.length() > maximumLength
|| normalized.codePoints().anyMatch(code -> code < 32 || code == 127)
|| !Objects.equals(normalized, GatewayText.redactSecrets(normalized))) {
throw unavailable();
}
return normalized;
}
private boolean safeId(String value) {
return value != null && SAFE_ID.matcher(value).matches();
}
private void append(StringBuilder material, String field, Object value) {
String text = value == null ? null : String.valueOf(value);
material.append(field).append(':').append(text == null ? -1 : text.length()).append(':');
if (text != null) {
material.append(text);
}
material.append(';');
}
private UnavailableException unavailable() {
return new UnavailableException();
}
/** Cause-free failure used at the channel boundary. */
public static final class UnavailableException extends IllegalArgumentException {
UnavailableException() {
super("Trace target is unavailable");
}
}
}
@@ -1,120 +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.gateway.application;
import java.util.Objects;
import org.apache.hertzbeat.ai.gateway.application.AgentTargetCanonicalizationService.FailureKind;
import org.apache.hertzbeat.ai.gateway.application.AgentTargetCanonicalizationService.TargetCanonicalizationException;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetAuthority;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetRef;
import org.apache.hertzbeat.ai.gateway.contract.UserInput;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
/** Isolates strict source/canonical adaptation for an exact Trace Explore detail target. */
@Service
public class AgentTraceTargetCanonicalizationAdapter {
private final AgentTraceTargetAuthorityService authorityService;
public AgentTraceTargetCanonicalizationAdapter(AgentTraceTargetAuthorityService authorityService) {
this.authorityService = authorityService;
}
public boolean isIntent(AgentTargetRef target) {
if (target == null) {
return false;
}
AgentTargetAuthority authority = target.getAuthority();
return target.getTrace() != null
|| hasVersion(target.getVersion(), AgentTraceTargetAuthorityService.TARGET_VERSION_PREFIX)
|| authority != null && hasVersion(
authority.getVersion(), AgentTraceTargetAuthorityService.AUTHORITY_VERSION_PREFIX);
}
public GatewayCommand.InvokeCommand canonicalize(GatewayCommand.InvokeCommand command) {
AgentTargetRef source = requireSourceIntent(command.userInput().getTarget());
try {
AgentTargetRef target = authorityService.canonicalize(
command.envelope().getWorkspaceId(), source.getTrace());
if (!authorityService.isCanonicalTarget(target)
|| !Objects.equals(authorityService.normalizeSource(source.getTrace()), target.getTrace())) {
throw unavailable();
}
return withTarget(command, target);
} catch (AgentTraceTargetAuthorityService.UnavailableException failure) {
throw unavailable();
} catch (TargetCanonicalizationException failure) {
throw failure;
} catch (RuntimeException ignored) {
throw unavailable();
}
}
public GatewayCommand.InvokeCommand replayCommand(
GatewayCommand.InvokeCommand command, AgentTargetRef persistedTarget) {
AgentTargetRef source = requireSourceIntent(command.userInput().getTarget());
try {
if (!authorityService.isCanonicalTarget(persistedTarget)
|| !Objects.equals(authorityService.normalizeSource(source.getTrace()), persistedTarget.getTrace())) {
throw mismatch();
}
return withTarget(command, persistedTarget);
} catch (TargetCanonicalizationException failure) {
throw failure;
} catch (RuntimeException ignored) {
throw mismatch();
}
}
public boolean isCanonicalTarget(AgentTargetRef target) {
return authorityService.isCanonicalTarget(target);
}
public static AgentTargetRef sourceIntent(AgentTargetRef target) {
return AgentTargetRef.builder().trace(target.getTrace()).build();
}
private AgentTargetRef requireSourceIntent(AgentTargetRef target) {
if (target == null || target.getTrace() == null || target.getVersion() != null || target.getAuthority() != null
|| target.getMonitorId() != null || target.getAlertId() != null || target.getAlertType() != null
|| target.getEntityId() != null || target.getCollector() != null || target.getSignal() != null
|| target.getTopology() != null || target.getLog() != null || target.getService() != null) {
throw unavailable();
}
return target;
}
private GatewayCommand.InvokeCommand withTarget(GatewayCommand.InvokeCommand command, AgentTargetRef target) {
UserInput userInput = command.userInput().toBuilder().target(target).build();
return new GatewayCommand.InvokeCommand(command.envelope(), command.replyMode(), command.commandId(), userInput,
command.entryType());
}
private boolean hasVersion(String version, String prefix) {
return StringUtils.hasText(version) && version.startsWith(prefix);
}
private TargetCanonicalizationException mismatch() {
return new TargetCanonicalizationException(FailureKind.MISMATCH);
}
private TargetCanonicalizationException unavailable() {
return new TargetCanonicalizationException(FailureKind.UNAVAILABLE);
}
}
@@ -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.gateway.application;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ApprovalDecisionCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.ErrorPayload;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.GatewayEventType;
import org.apache.hertzbeat.ai.gateway.application.GatewayResponse.Meta;
import org.apache.hertzbeat.ai.gateway.application.GatewayResponse.GatewaySingleResponse;
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeApprovalRegistry;
import org.apache.hertzbeat.ai.gateway.tool.core.AgentToolCallLedgerService;
import org.apache.hertzbeat.common.entity.agent.AgentToolCall;
import org.springframework.stereotype.Service;
/**
* Approval decision commands for active runtime loops.
*/
@Service
public class ApprovalCommandService {
private static final String STATUS_COMPLETED = "completed";
private static final String STATUS_FAILED = "failed";
private static final Duration DEFAULT_CONSUMPTION_TIMEOUT = Duration.ofSeconds(5);
private final AgentToolCallLedgerService toolCallLedgerService;
private final AgentRuntimeApprovalRegistry approvalRegistry;
private final Duration consumptionTimeout;
@org.springframework.beans.factory.annotation.Autowired
public ApprovalCommandService(AgentToolCallLedgerService toolCallLedgerService,
AgentRuntimeApprovalRegistry approvalRegistry) {
this(toolCallLedgerService, approvalRegistry, DEFAULT_CONSUMPTION_TIMEOUT);
}
ApprovalCommandService(AgentToolCallLedgerService toolCallLedgerService,
AgentRuntimeApprovalRegistry approvalRegistry,
Duration consumptionTimeout) {
this.toolCallLedgerService = toolCallLedgerService;
this.approvalRegistry = approvalRegistry;
this.consumptionTimeout = consumptionTimeout;
}
public GatewaySingleResponse decide(ApprovalDecisionCommand command) {
toolCallLedgerService.requireApprovalOwner(command.approvalId(), command.envelope(),
command.originEntryType());
var reservation = approvalRegistry.reserve(command.approvalId());
if (reservation.isEmpty()) {
return response(command, null, List.of(errorEvent(command, null,
"Agent approval runtime loop is no longer active.")));
}
AgentToolCall approval;
try {
approval = toolCallLedgerService.decideApproval(command.approvalId(), command.envelope(),
command.originEntryType(), command.decision());
} catch (RuntimeException | Error failure) {
reservation.orElseThrow().release();
throw failure;
}
var delivery = reservation.orElseThrow().deliver(command.decision());
if (!delivery.accepted() || !delivery.awaitConsumption(consumptionTimeout)) {
AgentToolCall terminal = toolCallLedgerService.terminalizeUnconsumedApproval(
command.approvalId(), command.envelope(), command.originEntryType(), command.decision());
return response(command, terminal, List.of(errorEvent(command, terminal,
"Agent approval runtime loop is no longer active.")));
}
return response(command, approval, List.of());
}
private GatewaySingleResponse response(ApprovalDecisionCommand command, AgentToolCall approval,
List<GatewayEvent> events) {
boolean success = events.isEmpty();
Meta meta = Meta.builder()
.commandId(command.commandId())
.sessionUid(approval == null ? null : approval.getSessionUid())
.runUid(approval == null ? null : approval.getRunUid())
.terminal(true)
.message(success ? "approval accepted" : "approval failed")
.build();
return GatewaySingleResponse.builder()
.meta(meta)
.body(Map.of("status", success ? STATUS_COMPLETED : STATUS_FAILED))
.events(events)
.build();
}
private GatewayEvent errorEvent(ApprovalDecisionCommand command, AgentToolCall approval, String message) {
return GatewayEvent.builder()
.type(GatewayEventType.ERROR)
.eventId(command.commandId() + ":error")
.sessionUid(approval == null ? null : approval.getSessionUid())
.runUid(approval == null ? null : approval.getRunUid())
.payload(ErrorPayload.builder()
.errorMessage(message)
.build())
.timestamp(System.currentTimeMillis())
.build();
}
}
@@ -1,419 +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.gateway.application;
import java.util.Objects;
import lombok.Builder;
import org.apache.hertzbeat.ai.gateway.contract.GatewayEnvelope;
import org.apache.hertzbeat.ai.gateway.contract.UserInput;
import org.apache.hertzbeat.ai.gateway.identity.ActorSupport;
import org.apache.hertzbeat.ai.gateway.tool.core.AgentApprovalDecision;
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeEntryType;
import org.apache.hertzbeat.common.entity.dto.ModelProviderConfig;
import org.springframework.util.StringUtils;
/**
* Strongly typed Gateway command.
*/
public sealed interface GatewayCommand permits
GatewayCommand.InvokeCommand,
GatewayCommand.ApprovalDecisionCommand,
GatewayCommand.CancelRunCommand,
GatewayCommand.GetSessionCommand,
GatewayCommand.GetRunCommand,
GatewayCommand.GetLatestSessionRunCommand,
GatewayCommand.ListSessionsCommand,
GatewayCommand.GetSessionTranscriptCommand,
GatewayCommand.ListModelProviderOptionsCommand,
GatewayCommand.ListModelProviderConfigurationsCommand,
GatewayCommand.CreateModelProviderConfigurationCommand,
GatewayCommand.UpdateModelProviderConfigurationCommand,
GatewayCommand.DeleteModelProviderConfigurationCommand,
GatewayCommand.SwitchModelProviderCommand {
GatewayEnvelope envelope();
ReplyMode replyMode();
String commandId();
/**
* Normalized runtime invocation shared by interactive and system-triggered entries.
*/
@Builder
record InvokeCommand(
GatewayEnvelope envelope,
ReplyMode replyMode,
String commandId,
UserInput userInput,
AgentRuntimeEntryType entryType) implements GatewayCommand {
public InvokeCommand {
envelope = Objects.requireNonNull(envelope, "envelope is required");
replyMode = Objects.requireNonNull(replyMode, "replyMode is required");
if (!StringUtils.hasText(commandId)) {
throw new IllegalArgumentException("commandId is required");
}
userInput = Objects.requireNonNull(userInput, "userInput is required");
entryType = Objects.requireNonNull(entryType, "entryType is required");
}
public InvokeCommand(GatewayEnvelope envelope, ReplyMode replyMode, String commandId, UserInput userInput) {
this(envelope, replyMode, commandId, userInput, AgentRuntimeEntryType.USER_INPUT);
}
}
/**
* Approval decision command.
*/
@Builder
record ApprovalDecisionCommand(
GatewayEnvelope envelope,
ReplyMode replyMode,
String commandId,
AgentRuntimeEntryType originEntryType,
String approvalId,
AgentApprovalDecision decision) implements GatewayCommand {
public ApprovalDecisionCommand {
envelope = Objects.requireNonNull(envelope, "envelope is required");
replyMode = Objects.requireNonNull(replyMode, "replyMode is required");
originEntryType = Objects.requireNonNull(originEntryType, "originEntryType is required");
decision = Objects.requireNonNull(decision, "approval decision is required");
if (!StringUtils.hasText(commandId) || !StringUtils.hasText(approvalId)) {
throw new IllegalArgumentException("commandId and approvalId are required");
}
}
}
/**
* Runtime cancellation command.
*/
@Builder
record CancelRunCommand(
GatewayEnvelope envelope,
ReplyMode replyMode,
String commandId,
AgentRuntimeEntryType originEntryType,
String runUid,
String reason) implements GatewayCommand {
public CancelRunCommand {
envelope = Objects.requireNonNull(envelope, "envelope is required");
replyMode = Objects.requireNonNull(replyMode, "replyMode is required");
originEntryType = Objects.requireNonNull(originEntryType, "originEntryType is required");
if (!StringUtils.hasText(commandId) || !StringUtils.hasText(runUid)) {
throw new IllegalArgumentException("commandId and runUid are required");
}
}
}
/**
* Session query command.
*/
@Builder
record GetSessionCommand(
GatewayEnvelope envelope,
ReplyMode replyMode,
String commandId,
AgentRuntimeEntryType originEntryType,
String sessionUid) implements GatewayCommand {
public GetSessionCommand {
envelope = Objects.requireNonNull(envelope, "envelope is required");
replyMode = Objects.requireNonNull(replyMode, "replyMode is required");
// The command must fully describe the session query scope.
originEntryType = Objects.requireNonNull(originEntryType, "originEntryType is required");
if (!StringUtils.hasText(commandId) || !StringUtils.hasText(sessionUid)) {
throw new IllegalArgumentException("commandId and sessionUid are required");
}
if (!ActorSupport.hasIdentity(envelope.getActor())) {
throw new IllegalArgumentException("Session query actor is required");
}
}
}
/** Owner-scoped durable run query. */
@Builder
record GetRunCommand(
GatewayEnvelope envelope,
ReplyMode replyMode,
String commandId,
AgentRuntimeEntryType originEntryType,
String runUid) implements GatewayCommand {
public GetRunCommand {
envelope = Objects.requireNonNull(envelope, "envelope is required");
replyMode = Objects.requireNonNull(replyMode, "replyMode is required");
originEntryType = Objects.requireNonNull(originEntryType, "originEntryType is required");
if (!StringUtils.hasText(commandId) || !StringUtils.hasText(runUid)) {
throw new IllegalArgumentException("commandId and runUid are required");
}
if (!ActorSupport.hasIdentity(envelope.getActor())) {
throw new IllegalArgumentException("Run query actor is required");
}
}
}
/** Owner-scoped latest durable run for one session. */
@Builder
record GetLatestSessionRunCommand(
GatewayEnvelope envelope,
ReplyMode replyMode,
String commandId,
AgentRuntimeEntryType originEntryType,
String sessionUid) implements GatewayCommand {
public GetLatestSessionRunCommand {
envelope = Objects.requireNonNull(envelope, "envelope is required");
replyMode = Objects.requireNonNull(replyMode, "replyMode is required");
originEntryType = Objects.requireNonNull(originEntryType, "originEntryType is required");
if (!StringUtils.hasText(commandId) || !StringUtils.hasText(sessionUid)) {
throw new IllegalArgumentException("commandId and sessionUid are required");
}
if (!ActorSupport.hasIdentity(envelope.getActor())) {
throw new IllegalArgumentException("Latest run query actor is required");
}
}
}
/**
* Current actor session list query.
*/
@Builder
record ListSessionsCommand(
GatewayEnvelope envelope,
ReplyMode replyMode,
String commandId,
AgentRuntimeEntryType originEntryType,
String title,
int pageIndex,
int pageSize) implements GatewayCommand {
public ListSessionsCommand {
envelope = Objects.requireNonNull(envelope, "envelope is required");
replyMode = Objects.requireNonNull(replyMode, "replyMode is required");
// The command must fully describe the session query scope.
originEntryType = Objects.requireNonNull(originEntryType, "originEntryType is required");
if (!StringUtils.hasText(commandId)) {
throw new IllegalArgumentException("commandId is required");
}
if (!ActorSupport.hasIdentity(envelope.getActor())) {
throw new IllegalArgumentException("Session query actor is required");
}
if (pageIndex < 0 || pageSize < 1) {
throw new IllegalArgumentException("Session page index and size are invalid");
}
pageSize = Math.min(pageSize, 200);
}
}
/**
* Current actor session transcript query.
*/
@Builder
record GetSessionTranscriptCommand(
GatewayEnvelope envelope,
ReplyMode replyMode,
String commandId,
AgentRuntimeEntryType originEntryType,
String sessionUid,
int pageIndex,
int pageSize) implements GatewayCommand {
public GetSessionTranscriptCommand {
envelope = Objects.requireNonNull(envelope, "envelope is required");
replyMode = Objects.requireNonNull(replyMode, "replyMode is required");
// The command must fully describe the session query scope.
originEntryType = Objects.requireNonNull(originEntryType, "originEntryType is required");
if (!StringUtils.hasText(commandId) || !StringUtils.hasText(sessionUid)) {
throw new IllegalArgumentException("commandId and sessionUid are required");
}
if (!ActorSupport.hasIdentity(envelope.getActor())) {
throw new IllegalArgumentException("Session query actor is required");
}
if (pageIndex < 0 || pageSize < 1) {
throw new IllegalArgumentException("Session page index and size are invalid");
}
pageSize = Math.min(pageSize, 200);
}
}
/**
* Registered model provider option query.
*/
@Builder
record ListModelProviderOptionsCommand(
GatewayEnvelope envelope,
ReplyMode replyMode,
String commandId) implements GatewayCommand {
public ListModelProviderOptionsCommand {
// Channel commands require trusted invocation metadata and an explicit delivery mode.
envelope = Objects.requireNonNull(envelope, "envelope is required");
replyMode = Objects.requireNonNull(replyMode, "replyMode is required");
if (!StringUtils.hasText(commandId)) {
throw new IllegalArgumentException("commandId is required");
}
if (replyMode != ReplyMode.FINAL_ONLY) {
throw new IllegalArgumentException("Model provider option queries require final-only replies");
}
if (!ActorSupport.hasIdentity(envelope.getActor())) {
throw new IllegalArgumentException("Model provider option query actor is required");
}
}
}
/**
* Saved model provider configuration query.
*/
@Builder
record ListModelProviderConfigurationsCommand(
GatewayEnvelope envelope,
ReplyMode replyMode,
String commandId) implements GatewayCommand {
public ListModelProviderConfigurationsCommand {
// Channel commands require trusted invocation metadata and an explicit delivery mode.
envelope = Objects.requireNonNull(envelope, "envelope is required");
replyMode = Objects.requireNonNull(replyMode, "replyMode is required");
if (!StringUtils.hasText(commandId)) {
throw new IllegalArgumentException("commandId is required");
}
if (replyMode != ReplyMode.FINAL_ONLY) {
throw new IllegalArgumentException("Model provider configuration queries require final-only replies");
}
if (!ActorSupport.hasIdentity(envelope.getActor())) {
throw new IllegalArgumentException("Model provider configuration query actor is required");
}
}
}
/**
* Saved model provider configuration creation.
*/
@Builder
record CreateModelProviderConfigurationCommand(
GatewayEnvelope envelope,
ReplyMode replyMode,
String commandId,
ModelProviderConfig config) implements GatewayCommand {
public CreateModelProviderConfigurationCommand {
// Channel commands require trusted invocation metadata and an explicit delivery mode.
envelope = Objects.requireNonNull(envelope, "envelope is required");
replyMode = Objects.requireNonNull(replyMode, "replyMode is required");
if (!StringUtils.hasText(commandId)) {
throw new IllegalArgumentException("commandId is required");
}
if (replyMode != ReplyMode.FINAL_ONLY) {
throw new IllegalArgumentException("Model provider configuration creation requires final-only replies");
}
if (!ActorSupport.hasIdentity(envelope.getActor())) {
throw new IllegalArgumentException("Model provider configuration creation actor is required");
}
// A create command owns the complete provider DTO so validation and normalization happen once downstream.
config = Objects.requireNonNull(config, "model provider config is required");
}
}
/**
* Saved model provider configuration update.
*/
@Builder
record UpdateModelProviderConfigurationCommand(
GatewayEnvelope envelope,
ReplyMode replyMode,
String commandId,
String providerUid,
ModelProviderConfig config) implements GatewayCommand {
public UpdateModelProviderConfigurationCommand {
envelope = Objects.requireNonNull(envelope, "envelope is required");
replyMode = Objects.requireNonNull(replyMode, "replyMode is required");
if (!StringUtils.hasText(commandId) || !StringUtils.hasText(providerUid)) {
throw new IllegalArgumentException("commandId and providerUid are required");
}
if (replyMode != ReplyMode.FINAL_ONLY) {
throw new IllegalArgumentException("Model provider configuration updates require final-only replies");
}
if (!ActorSupport.hasIdentity(envelope.getActor())) {
throw new IllegalArgumentException("Model provider configuration update actor is required");
}
config = Objects.requireNonNull(config, "model provider config is required");
}
}
/**
* Saved model provider configuration deletion.
*/
@Builder
record DeleteModelProviderConfigurationCommand(
GatewayEnvelope envelope,
ReplyMode replyMode,
String commandId,
String providerUid) implements GatewayCommand {
public DeleteModelProviderConfigurationCommand {
envelope = Objects.requireNonNull(envelope, "envelope is required");
replyMode = Objects.requireNonNull(replyMode, "replyMode is required");
if (!StringUtils.hasText(commandId) || !StringUtils.hasText(providerUid)) {
throw new IllegalArgumentException("commandId and providerUid are required");
}
if (replyMode != ReplyMode.FINAL_ONLY) {
throw new IllegalArgumentException("Model provider configuration deletions require final-only replies");
}
if (!ActorSupport.hasIdentity(envelope.getActor())) {
throw new IllegalArgumentException("Model provider configuration deletion actor is required");
}
}
}
/**
* Active model provider selection. A null UID selects the YAML default.
*/
@Builder
record SwitchModelProviderCommand(
GatewayEnvelope envelope,
ReplyMode replyMode,
String commandId,
String providerUid) implements GatewayCommand {
public SwitchModelProviderCommand {
envelope = Objects.requireNonNull(envelope, "envelope is required");
replyMode = Objects.requireNonNull(replyMode, "replyMode is required");
if (!StringUtils.hasText(commandId)) {
throw new IllegalArgumentException("commandId is required");
}
if (replyMode != ReplyMode.FINAL_ONLY) {
throw new IllegalArgumentException("Model provider switches require final-only replies");
}
if (!ActorSupport.hasIdentity(envelope.getActor())) {
throw new IllegalArgumentException("Model provider switch actor is required");
}
}
}
/**
* Response delivery mode requested by a channel.
*/
enum ReplyMode {
FINAL_ONLY,
STREAM
}
}
@@ -1,90 +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.gateway.application;
import java.util.Objects;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ApprovalDecisionCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.CancelRunCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.CreateModelProviderConfigurationCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.DeleteModelProviderConfigurationCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.GetLatestSessionRunCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.GetSessionCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.GetRunCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.GetSessionTranscriptCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.InvokeCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ListModelProviderConfigurationsCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ListModelProviderOptionsCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ListSessionsCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.SwitchModelProviderCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.UpdateModelProviderConfigurationCommand;
import org.springframework.stereotype.Service;
/**
* Routes Gateway commands to domain services.
*/
@Service
public class GatewayCommandRouter {
private final AgentCommandService agentCommandService;
private final ApprovalCommandService approvalCommandService;
private final RunCommandService runCommandService;
private final GatewayQueryService queryService;
private final ModelProviderCommandService modelProviderCommandService;
public GatewayCommandRouter(AgentCommandService agentCommandService,
ApprovalCommandService approvalCommandService,
RunCommandService runCommandService,
GatewayQueryService queryService,
ModelProviderCommandService modelProviderCommandService) {
this.agentCommandService = Objects.requireNonNull(agentCommandService, "agentCommandService is required");
this.approvalCommandService = Objects.requireNonNull(approvalCommandService,
"approvalCommandService is required");
this.runCommandService = Objects.requireNonNull(runCommandService, "runCommandService is required");
this.queryService = Objects.requireNonNull(queryService, "queryService is required");
// Provider commands cannot be exposed unless their application service is wired.
this.modelProviderCommandService = Objects.requireNonNull(
modelProviderCommandService, "modelProviderCommandService is required");
}
public GatewayResponse handle(GatewayCommand command) {
Objects.requireNonNull(command, "command is required");
return switch (command) {
case InvokeCommand invokeCommand -> agentCommandService.handle(invokeCommand);
case ApprovalDecisionCommand approvalCommand -> approvalCommandService.decide(approvalCommand);
case CancelRunCommand cancelCommand -> runCommandService.cancel(cancelCommand);
case GetSessionCommand getSessionCommand -> queryService.getSession(getSessionCommand);
case GetRunCommand getRunCommand -> queryService.getRun(getRunCommand);
case GetLatestSessionRunCommand latestRunCommand -> queryService.getLatestSessionRun(latestRunCommand);
case ListSessionsCommand listSessionsCommand -> queryService.listSessions(listSessionsCommand);
case GetSessionTranscriptCommand transcriptCommand ->
queryService.getSessionTranscript(transcriptCommand);
case ListModelProviderOptionsCommand optionsCommand ->
modelProviderCommandService.listOptions(optionsCommand);
case ListModelProviderConfigurationsCommand configurationsCommand ->
modelProviderCommandService.listConfigurations(configurationsCommand);
case CreateModelProviderConfigurationCommand createCommand ->
modelProviderCommandService.createConfiguration(createCommand);
case UpdateModelProviderConfigurationCommand updateCommand ->
modelProviderCommandService.updateConfiguration(updateCommand);
case DeleteModelProviderConfigurationCommand deleteCommand ->
modelProviderCommandService.deleteConfiguration(deleteCommand);
case SwitchModelProviderCommand switchCommand ->
modelProviderCommandService.switchConfiguration(switchCommand);
};
}
}
@@ -1,199 +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.gateway.application;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import lombok.Builder;
import org.springframework.util.StringUtils;
/**
* Channel-neutral event emitted by the Gateway layer.
*/
@Builder
public record GatewayEvent(
GatewayEventType type,
String eventId,
String conversationId,
String sessionUid,
String runUid,
String itemId,
GatewayEventPayload payload,
Long timestamp) {
public GatewayEvent {
// GatewayEvent is the external event boundary; clients need stable identity and a concrete schema.
Objects.requireNonNull(type, "type is required");
if (!StringUtils.hasText(eventId)) {
throw new IllegalArgumentException("eventId is required");
}
Objects.requireNonNull(payload, "payload is required");
Objects.requireNonNull(timestamp, "timestamp is required");
if (timestamp < 0) {
throw new IllegalArgumentException("timestamp must not be negative");
}
}
/**
* Event categories exposed to channels.
*/
public enum GatewayEventType {
RUN_STARTED,
MESSAGE_STARTED,
MESSAGE_DELTA,
MESSAGE_COMPLETED,
TOOL_STARTED,
TOOL_COMPLETED,
INPUT_REQUESTED,
INPUT_COMPLETED,
APPROVAL_REQUESTED,
APPROVAL_COMPLETED,
RUN_STATUS,
RUN_COMPLETED,
ERROR
}
/**
* Marker boundary for structured event payloads.
*/
public sealed interface GatewayEventPayload permits
RunStartedPayload,
MessageStartedPayload,
MessageDeltaPayload,
MessageCompletedPayload,
ToolStartedPayload,
ToolCompletedPayload,
InputRequestedPayload,
InputCompletedPayload,
ApprovalRequestedPayload,
ApprovalCompletedPayload,
RunStatusPayload,
RunCompletedPayload,
ErrorPayload {
}
/** Run start payload. */
@Builder
public record RunStartedPayload(String traceId) implements GatewayEventPayload {
}
/** Assistant message start payload. */
@Builder
public record MessageStartedPayload(String traceId) implements GatewayEventPayload {
}
/** Assistant message delta payload. */
@Builder
public record MessageDeltaPayload(
String traceId,
Integer deltaIndex,
String delta) implements GatewayEventPayload {
}
/** Assistant message completion payload. */
@Builder
public record MessageCompletedPayload(String traceId) implements GatewayEventPayload {
}
/** Tool execution start payload. */
@Builder
public record ToolStartedPayload(
String traceId,
String toolName,
String toolCallId,
Map<String, Object> arguments,
String status) implements GatewayEventPayload {
}
/** Tool execution completion payload. */
@Builder
public record ToolCompletedPayload(
String traceId,
String toolName,
String toolCallId,
String approvalId,
String policyDecision,
String errorMessage,
Long elapsedMs,
String status) implements GatewayEventPayload {
}
/** Structured user input requested by a tool. */
@Builder
public record InputRequestedPayload(
String traceId,
String interactionId,
String targetTool,
String title,
String description,
List<Map<String, Object>> fields,
String status) implements GatewayEventPayload {
}
/** Completion of a structured user input request. */
@Builder
public record InputCompletedPayload(
String traceId,
String interactionId,
String status,
String errorMessage) implements GatewayEventPayload {
}
/** Approval request payload. */
@Builder
public record ApprovalRequestedPayload(
String traceId,
String toolName,
String approvalId,
String toolCallId,
String status) implements GatewayEventPayload {
}
/** Approval completion payload. */
@Builder
public record ApprovalCompletedPayload(
String traceId,
String toolName,
String approvalId,
String toolCallId,
String status) implements GatewayEventPayload {
}
/** Durable run snapshot emitted instead of starting a duplicate runtime. */
@Builder
public record RunStatusPayload(
String status,
String result,
String errorMessage,
boolean replayAvailable) implements GatewayEventPayload {
}
/** Run completion payload. */
@Builder
public record RunCompletedPayload(String traceId) implements GatewayEventPayload {
}
/** Gateway error payload. */
@Builder
public record ErrorPayload(
String traceId,
String errorMessage,
String status) implements GatewayEventPayload {
}
}
@@ -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.ai.gateway.application;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.GetSessionCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.GetRunCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.GetLatestSessionRunCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.GetSessionTranscriptCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ListSessionsCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayResponse.Meta;
import org.apache.hertzbeat.ai.gateway.application.GatewayResponse.GatewaySingleResponse;
import org.apache.hertzbeat.ai.gateway.conversation.AgentSessionService;
import org.apache.hertzbeat.ai.gateway.conversation.AgentSessionListItem;
import org.apache.hertzbeat.ai.gateway.conversation.AgentRunListProjection;
import org.apache.hertzbeat.ai.gateway.conversation.AgentRunService;
import org.apache.hertzbeat.ai.gateway.conversation.AgentRunSnapshotService;
import org.apache.hertzbeat.common.entity.agent.AgentSession;
import org.apache.hertzbeat.common.entity.agent.AgentTranscriptEntry;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
/**
* Gateway query commands. Query commands never invoke runtime.
*/
@Service
public class GatewayQueryService {
private final AgentSessionService sessionService;
private final AgentRunService runService;
private final AgentRunSnapshotService snapshotService;
public GatewayQueryService(AgentSessionService sessionService, AgentRunService runService,
AgentRunSnapshotService snapshotService) {
this.sessionService = sessionService;
this.runService = runService;
this.snapshotService = snapshotService;
}
public GatewaySingleResponse listSessions(ListSessionsCommand command) {
Page<AgentSession> sessions = sessionService.findSessions(
command.envelope(),
command.originEntryType(),
command.title(),
PageRequest.of(command.pageIndex(), command.pageSize()));
List<Long> sessionIds = sessions.stream().map(AgentSession::getId).toList();
Map<Long, AgentRunListProjection> latestRuns = runService.findLatestRunProjections(sessionIds);
List<AgentSessionListItem> projectedContent = sessions.stream()
.map(session -> AgentSessionListItem.from(session, latestRuns.get(session.getId())))
.sorted(Comparator.comparing(
AgentSessionListItem::gmtUpdate,
Comparator.nullsLast(Comparator.reverseOrder())))
.toList();
Page<AgentSessionListItem> projectedSessions = new PageImpl<>(
projectedContent, sessions.getPageable(), sessions.getTotalElements());
return GatewaySingleResponse.builder()
.meta(Meta.builder()
.commandId(command.commandId())
.terminal(true)
.message("sessions")
.build())
.body(projectedSessions)
.events(List.of())
.build();
}
public GatewaySingleResponse getSession(GetSessionCommand command) {
return sessionService.findOwnedSession(
command.sessionUid(), command.envelope(), command.originEntryType())
.<GatewaySingleResponse>map(session -> GatewaySingleResponse.builder()
.meta(Meta.builder()
.commandId(command.commandId())
.sessionUid(session.getSessionUid())
.terminal(true)
.message("session")
.build())
.body(session)
.events(List.of())
.build())
.orElseGet(() -> GatewaySingleResponse.builder()
.meta(Meta.builder()
.commandId(command.commandId())
.sessionUid(command.sessionUid())
.terminal(true)
.message("Agent session not found")
.build())
.events(List.of())
.build());
}
public GatewaySingleResponse getRun(GetRunCommand command) {
return runService.findRun(command.runUid())
.filter(run -> command.originEntryType().name().equals(run.getEntryType()))
.flatMap(run -> sessionService.findOwnedSession(
String.valueOf(run.getSessionId()), command.envelope(), command.originEntryType())
.map(session -> snapshotService.snapshot(session, run)))
.<GatewaySingleResponse>map(snapshot -> GatewaySingleResponse.builder()
.meta(Meta.builder()
.commandId(command.commandId())
.sessionUid(snapshot.sessionUid())
.runUid(snapshot.runUid())
.terminal(true)
.message("run")
.build())
.body(snapshot)
.events(List.of())
.build())
.orElseGet(() -> GatewaySingleResponse.builder()
.meta(Meta.builder()
.commandId(command.commandId())
.runUid(command.runUid())
.terminal(true)
.message("Agent run not found")
.build())
.events(List.of())
.build());
}
public GatewaySingleResponse getLatestSessionRun(GetLatestSessionRunCommand command) {
return sessionService.findOwnedSession(
command.sessionUid(), command.envelope(), command.originEntryType())
.flatMap(session -> runService.findLatestRun(session.getId())
.filter(run -> command.originEntryType().name().equals(run.getEntryType()))
.map(run -> snapshotService.snapshot(session, run)))
.<GatewaySingleResponse>map(snapshot -> GatewaySingleResponse.builder()
.meta(Meta.builder()
.commandId(command.commandId())
.sessionUid(snapshot.sessionUid())
.runUid(snapshot.runUid())
.terminal(true)
.message("run")
.build())
.body(snapshot)
.events(List.of())
.build())
.orElseGet(() -> GatewaySingleResponse.builder()
.meta(Meta.builder()
.commandId(command.commandId())
.sessionUid(command.sessionUid())
.terminal(true)
.message("Agent run not found")
.build())
.events(List.of())
.build());
}
public GatewaySingleResponse getSessionTranscript(GetSessionTranscriptCommand command) {
PageRequest pageRequest = PageRequest.of(command.pageIndex(), command.pageSize());
AgentSession session = sessionService.findOwnedSession(
command.sessionUid(), command.envelope(), command.originEntryType()).orElse(null);
Page<AgentTranscriptEntry> transcript = session == null
? Page.empty(pageRequest)
: sessionService.findTranscriptEntries(session.getId(), pageRequest);
return GatewaySingleResponse.builder()
.meta(Meta.builder()
.commandId(command.commandId())
.sessionUid(session == null ? command.sessionUid() : session.getSessionUid())
.terminal(true)
.message(session == null ? "Agent session not found" : "session-transcript")
.build())
.body(transcript)
.events(List.of())
.build();
}
}
@@ -1,83 +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.gateway.application;
import java.util.List;
import java.util.Objects;
import lombok.Builder;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
/**
* Channel-neutral Gateway response.
*/
public sealed interface GatewayResponse permits
GatewayResponse.GatewaySingleResponse,
GatewayResponse.GatewayStreamResponse {
Meta meta();
/**
* Final-only Gateway response.
*/
@Builder
record GatewaySingleResponse(
Meta meta,
Object body,
List<GatewayEvent> events) implements GatewayResponse {
public GatewaySingleResponse {
Objects.requireNonNull(meta, "meta is required");
events = events == null ? List.of() : List.copyOf(events);
}
}
/**
* Streaming Gateway response.
*/
@Builder
record GatewayStreamResponse(
Meta meta,
Flux<GatewayEvent> events) implements GatewayResponse {
public GatewayStreamResponse {
Objects.requireNonNull(meta, "meta is required");
Objects.requireNonNull(events, "events is required");
}
}
/**
* Common Gateway response metadata.
*/
@Builder
record Meta(
String commandId,
String conversationId,
String sessionUid,
String runUid,
boolean terminal,
String message) {
public Meta {
// Every response is correlated to one normalized command and exposes a human-readable outcome label.
if (!StringUtils.hasText(commandId) || !StringUtils.hasText(message)) {
throw new IllegalArgumentException("Response command id and message are required");
}
}
}
}
@@ -1,282 +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.gateway.application;
import java.time.Instant;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.ApprovalCompletedPayload;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.ApprovalRequestedPayload;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.ErrorPayload;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.GatewayEventPayload;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.GatewayEventType;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.MessageCompletedPayload;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.InputRequestedPayload;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.InputCompletedPayload;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.MessageDeltaPayload;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.MessageStartedPayload;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.RunCompletedPayload;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.RunStartedPayload;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.ToolCompletedPayload;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.ToolStartedPayload;
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeEvent;
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeEvent.RequestKind;
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeEvent.EventStatus;
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeItemKind;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
/**
* Projects runtime facts into the external Gateway event contract.
*/
@Component
public class GatewayRuntimeEventProjector {
public GatewayEvent project(AgentRuntimeEvent event, String conversationId, String sessionUid, String runUid) {
// Runtime publishers only emit complete events; projection cannot represent a missing source event.
Objects.requireNonNull(event, "Runtime event is required");
return switch (event.getType()) {
case RUN_STARTED -> gatewayEvent(event, GatewayEventType.RUN_STARTED, conversationId, sessionUid, runUid,
RunStartedPayload.builder()
.traceId(event.getTraceId())
.build());
case ITEM_STARTED -> projectItemStarted(event, conversationId, sessionUid, runUid);
case ITEM_DELTA -> projectItemDelta(event, conversationId, sessionUid, runUid);
case ITEM_COMPLETED -> projectItemCompleted(event, conversationId, sessionUid, runUid);
case REQUESTED_ACTION -> projectRequestedAction(event, conversationId, sessionUid, runUid);
case REQUEST_COMPLETED -> projectRequestCompleted(event, conversationId, sessionUid, runUid);
case RUN_COMPLETED -> gatewayEvent(event, GatewayEventType.RUN_COMPLETED, conversationId, sessionUid,
runUid, RunCompletedPayload.builder()
.traceId(event.getTraceId())
.build());
case ERROR -> gatewayEvent(event, GatewayEventType.ERROR, conversationId, sessionUid, runUid,
errorPayload(event, event.getErrorMessage()));
};
}
private GatewayEvent projectItemStarted(AgentRuntimeEvent event, String conversationId, String sessionUid,
String runUid) {
if (isTool(event)) {
return gatewayEvent(event, GatewayEventType.TOOL_STARTED, conversationId, sessionUid, runUid,
ToolStartedPayload.builder()
.traceId(event.getTraceId())
.toolName(event.getToolName())
.toolCallId(event.getToolCallId())
.arguments(event.getToolArguments())
.status(event.getStatus() == null ? EventStatus.IN_PROGRESS.externalName()
: event.getStatus().externalName())
.build());
}
if (isAssistantMessage(event)) {
return gatewayEvent(event, GatewayEventType.MESSAGE_STARTED, conversationId, sessionUid, runUid,
MessageStartedPayload.builder()
.traceId(event.getTraceId())
.build());
}
return unsupported(event, conversationId, sessionUid, runUid,
"Runtime item start event has no supported item kind.");
}
private GatewayEvent projectItemDelta(AgentRuntimeEvent event, String conversationId, String sessionUid,
String runUid) {
// Legacy runtime delta events without itemKind are assistant message deltas.
if (event.getItemKind() == null || isAssistantMessage(event)) {
return gatewayEvent(event, GatewayEventType.MESSAGE_DELTA, conversationId, sessionUid, runUid,
MessageDeltaPayload.builder()
.traceId(event.getTraceId())
.deltaIndex(event.getDeltaIndex())
.delta(event.getDelta())
.build());
}
return unsupported(event, conversationId, sessionUid, runUid,
"Runtime item delta event has no supported item kind.");
}
private GatewayEvent projectItemCompleted(AgentRuntimeEvent event, String conversationId, String sessionUid,
String runUid) {
if (isTool(event)) {
return gatewayEvent(event, GatewayEventType.TOOL_COMPLETED, conversationId, sessionUid, runUid,
ToolCompletedPayload.builder()
.traceId(event.getTraceId())
.toolName(event.getToolName())
.toolCallId(event.getToolCallId())
.approvalId(event.getApprovalId())
.policyDecision(externalName(event.getPolicyDecision()))
.errorMessage(event.getErrorMessage())
.elapsedMs(event.getElapsedMs())
.status(toolCompletedStatus(event))
.build());
}
if (isAssistantMessage(event)) {
return gatewayEvent(event, GatewayEventType.MESSAGE_COMPLETED, conversationId, sessionUid, runUid,
MessageCompletedPayload.builder()
.traceId(event.getTraceId())
.build());
}
return unsupported(event, conversationId, sessionUid, runUid,
"Runtime item completion event has no supported item kind.");
}
private GatewayEvent projectRequestedAction(AgentRuntimeEvent event, String conversationId, String sessionUid,
String runUid) {
if (event.getRequestKind() == RequestKind.USER_INPUT) {
Map<String, Object> requestPayload = event.getRequestPayload() == null
? Map.of() : event.getRequestPayload();
return gatewayEvent(event, GatewayEventType.INPUT_REQUESTED, conversationId, sessionUid, runUid,
InputRequestedPayload.builder()
.traceId(event.getTraceId())
.interactionId(event.getRequestId())
.targetTool(stringValue(requestPayload.get("targetTool")))
.title(stringValue(requestPayload.get("title")))
.description(stringValue(requestPayload.get("description")))
.fields(inputFields(requestPayload.get("fields")))
.status(event.getStatus().externalName())
.build());
}
if (event.getRequestKind() != RequestKind.APPROVAL) {
return unsupported(event, conversationId, sessionUid, runUid,
"Runtime requested action event has no supported request kind.");
}
return gatewayEvent(event, GatewayEventType.APPROVAL_REQUESTED, conversationId, sessionUid, runUid,
ApprovalRequestedPayload.builder()
.traceId(event.getTraceId())
.toolName(event.getToolName())
.approvalId(event.getApprovalId())
.toolCallId(event.getToolCallId())
.status(event.getStatus() == null ? EventStatus.WAITING_APPROVAL.externalName()
: event.getStatus().externalName())
.build());
}
private GatewayEvent projectRequestCompleted(AgentRuntimeEvent event, String conversationId, String sessionUid,
String runUid) {
if (event.getRequestKind() == RequestKind.USER_INPUT) {
return gatewayEvent(event, GatewayEventType.INPUT_COMPLETED, conversationId, sessionUid, runUid,
InputCompletedPayload.builder()
.traceId(event.getTraceId())
.interactionId(event.getRequestId())
.status(event.getStatus().externalName())
.errorMessage(event.getErrorMessage())
.build());
}
if (event.getRequestKind() != RequestKind.APPROVAL) {
return unsupported(event, conversationId, sessionUid, runUid,
"Runtime completed request event has no supported request kind.");
}
return gatewayEvent(event, GatewayEventType.APPROVAL_COMPLETED, conversationId, sessionUid, runUid,
ApprovalCompletedPayload.builder()
.traceId(event.getTraceId())
.toolName(event.getToolName())
.approvalId(event.getApprovalId())
.toolCallId(event.getToolCallId())
.status(event.getStatus().externalName())
.build());
}
private GatewayEvent unsupported(AgentRuntimeEvent event, String conversationId, String sessionUid, String runUid,
String message) {
return gatewayEvent(event, GatewayEventType.ERROR, conversationId, sessionUid, runUid,
errorPayload(event, message));
}
private GatewayEvent gatewayEvent(AgentRuntimeEvent event, GatewayEventType type, String conversationId,
String sessionUid, String runUid, GatewayEventPayload payload) {
return GatewayEvent.builder()
.type(type)
.eventId(eventId(event, type, runUid))
.conversationId(conversationId)
.sessionUid(sessionUid)
.runUid(runUid)
.itemId(event.getItemId())
.payload(payload)
.timestamp(timestamp(event.getTimestamp()))
.build();
}
private ErrorPayload errorPayload(AgentRuntimeEvent event, String errorMessage) {
return ErrorPayload.builder()
.traceId(event.getTraceId())
.errorMessage(errorMessage)
.status(event.getStatus() == null ? null : event.getStatus().externalName())
.build();
}
private String eventId(AgentRuntimeEvent event, GatewayEventType type, String runUid) {
String eventRunUid = StringUtils.hasText(runUid) ? runUid : "run";
if (event.getEventSequence() != null) {
return eventRunUid + ":event:" + event.getEventSequence();
}
String stableId = Stream.of(event.getRequestId(), event.getApprovalId(), event.getItemId(),
event.getToolCallId(), event.getTraceId())
.filter(StringUtils::hasText)
.findFirst()
.orElse("event");
String eventType = type.name().toLowerCase(Locale.ROOT);
return eventRunUid + ":" + eventType + ":" + stableId;
}
private String toolCompletedStatus(AgentRuntimeEvent event) {
if (event.getStatus() != null) {
return event.getStatus().externalName();
}
if (StringUtils.hasText(event.getErrorMessage())) {
return EventStatus.FAILED.externalName();
}
if (org.apache.hertzbeat.ai.gateway.tool.core.AgentPolicyDecision.REQUIRE_APPROVAL
== event.getPolicyDecision()) {
return EventStatus.WAITING_APPROVAL.externalName();
}
return EventStatus.COMPLETED.externalName();
}
private boolean isAssistantMessage(AgentRuntimeEvent event) {
return event.getItemKind() == AgentRuntimeItemKind.ASSISTANT_MESSAGE;
}
private boolean isTool(AgentRuntimeEvent event) {
return event.getItemKind() == AgentRuntimeItemKind.TOOL_CALL;
}
private Long timestamp(Instant timestamp) {
return timestamp == null ? System.currentTimeMillis() : timestamp.toEpochMilli();
}
private String externalName(Enum<?> value) {
return value == null ? null : value.name();
}
private String stringValue(Object value) {
return value instanceof String text ? text : null;
}
@SuppressWarnings("unchecked")
private List<Map<String, Object>> inputFields(Object value) {
if (!(value instanceof List<?> values)) {
return List.of();
}
return values.stream()
.filter(Map.class::isInstance)
.map(item -> Map.copyOf((Map<String, Object>) item))
.toList();
}
}
@@ -1,153 +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.gateway.application;
import java.util.List;
import java.util.Objects;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.CreateModelProviderConfigurationCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.DeleteModelProviderConfigurationCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ListModelProviderConfigurationsCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ListModelProviderOptionsCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.SwitchModelProviderCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.UpdateModelProviderConfigurationCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayResponse.GatewaySingleResponse;
import org.apache.hertzbeat.ai.gateway.application.GatewayResponse.Meta;
import org.apache.hertzbeat.ai.gateway.application.ModelProviderConfigurationView.Provider;
import org.apache.hertzbeat.ai.gateway.runtime.ReloadableAgentRuntimeModelClient;
import org.apache.hertzbeat.ai.gateway.runtime.provider.AgentModelProviderRegistry;
import org.apache.hertzbeat.common.entity.dto.ModelProviderConfig;
import org.apache.hertzbeat.common.entity.dto.ModelProviderConfigState;
import org.apache.hertzbeat.manager.service.ModelProviderConfigurationService;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
/**
* Channel-neutral model provider configuration commands.
*/
@Service
public class ModelProviderCommandService {
private final AgentModelProviderRegistry providerRegistry;
private final ModelProviderConfigurationService configurationService;
private final ReloadableAgentRuntimeModelClient runtimeModelClient;
public ModelProviderCommandService(AgentModelProviderRegistry providerRegistry,
ModelProviderConfigurationService configurationService,
ReloadableAgentRuntimeModelClient runtimeModelClient) {
this.providerRegistry = providerRegistry;
this.configurationService = configurationService;
this.runtimeModelClient = runtimeModelClient;
}
public GatewaySingleResponse listOptions(ListModelProviderOptionsCommand command) {
return response(command.commandId(), "model-provider-options", providerRegistry.options());
}
public GatewaySingleResponse listConfigurations(ListModelProviderConfigurationsCommand command) {
return stateResponse(command.commandId(), "model-provider-configurations",
configurationService.getState());
}
public GatewaySingleResponse createConfiguration(CreateModelProviderConfigurationCommand command) {
ModelProviderConfig config = copyConfiguration(command.config());
config.setUid(null);
providerRegistry.createModel(config);
return stateResponse(command.commandId(), "model-provider-configuration-created",
configurationService.createConfiguration(config));
}
public GatewaySingleResponse updateConfiguration(UpdateModelProviderConfigurationCommand command) {
ModelProviderConfig existing = configurationService.getConfiguration(command.providerUid());
ModelProviderConfig config = copyConfiguration(command.config());
config.setUid(command.providerUid());
boolean sameProvider = Objects.equals(existing.getCode(), config.getCode())
&& Objects.equals(existing.getType(), config.getType());
if (!StringUtils.hasText(config.getApiKey())
&& sameProvider) {
config.setApiKey(existing.getApiKey());
}
providerRegistry.createModel(config);
ModelProviderConfigState state = configurationService.updateConfiguration(
command.providerUid(), config);
if (Objects.equals(command.providerUid(), state.getActiveProviderUid())) {
runtimeModelClient.refreshConfiguration();
}
return stateResponse(command.commandId(), "model-provider-configuration-updated", state);
}
public GatewaySingleResponse deleteConfiguration(DeleteModelProviderConfigurationCommand command) {
boolean deletingActiveConfiguration = Objects.equals(
command.providerUid(), configurationService.getState().getActiveProviderUid());
ModelProviderConfigState state = configurationService.deleteConfiguration(command.providerUid());
if (deletingActiveConfiguration) {
runtimeModelClient.refreshConfiguration();
}
return stateResponse(command.commandId(), "model-provider-configuration-deleted", state);
}
public GatewaySingleResponse switchConfiguration(SwitchModelProviderCommand command) {
String previousProviderUid = configurationService.getState().getActiveProviderUid();
if (StringUtils.hasText(command.providerUid())) {
ModelProviderConfig config = configurationService.getConfiguration(command.providerUid());
providerRegistry.createModel(config);
}
ModelProviderConfigState state = configurationService.switchActiveConfiguration(command.providerUid());
if (!Objects.equals(previousProviderUid, state.getActiveProviderUid())) {
runtimeModelClient.refreshConfiguration();
}
return stateResponse(command.commandId(), "model-provider-configuration-switched", state);
}
private GatewaySingleResponse stateResponse(String commandId, String message,
ModelProviderConfigState state) {
List<Provider> providers = state.getProviders().stream()
.map(config -> new Provider(
config.getUid(),
config.getType(),
config.getCode(),
config.getBaseUrl(),
config.getModel(),
StringUtils.hasText(config.getApiKey())))
.toList();
return response(commandId, message,
new ModelProviderConfigurationView(state.getActiveProviderUid(), providers));
}
private GatewaySingleResponse response(String commandId, String message, Object body) {
return GatewaySingleResponse.builder()
.meta(Meta.builder()
.commandId(commandId)
.terminal(true)
.message(message)
.build())
.body(body)
.events(List.of())
.build();
}
private ModelProviderConfig copyConfiguration(ModelProviderConfig source) {
ModelProviderConfig copy = new ModelProviderConfig();
copy.setUid(source.getUid());
copy.setType(source.getType());
copy.setCode(source.getCode());
copy.setBaseUrl(source.getBaseUrl());
copy.setModel(source.getModel());
copy.setApiKey(source.getApiKey());
return copy;
}
}
@@ -1,57 +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.gateway.application;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
/**
* Secret-free provider configuration state returned to Gateway channels.
*
* @param activeProviderUid active saved configuration UID; null selects the YAML default
* @param providers saved provider configurations without API keys
*/
@Schema(description = "Saved LLM provider configurations and active selection")
public record ModelProviderConfigurationView(
String activeProviderUid,
List<Provider> providers) {
public ModelProviderConfigurationView {
providers = List.copyOf(providers);
}
/**
* One secret-free saved provider configuration.
*
* @param uid stable saved configuration identifier
* @param type runtime provider implementation type
* @param code provider preset code
* @param baseUrl provider API endpoint
* @param model model identifier
* @param apiKeyConfigured whether a secret is persisted
*/
@Schema(description = "Saved LLM provider configuration without its API key")
public record Provider(
String uid,
String type,
String code,
String baseUrl,
String model,
boolean apiKeyConfigured) {
}
}
@@ -1,164 +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.gateway.application;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.CancelRunCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ReplyMode;
import org.apache.hertzbeat.ai.gateway.conversation.AgentRunStatus;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.ErrorPayload;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.GatewayEventType;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.RunCompletedPayload;
import org.apache.hertzbeat.ai.gateway.application.GatewayResponse.Meta;
import org.apache.hertzbeat.ai.gateway.application.GatewayResponse.GatewaySingleResponse;
import org.apache.hertzbeat.ai.gateway.application.GatewayResponse.GatewayStreamResponse;
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeControlRegistry;
import org.apache.hertzbeat.ai.gateway.conversation.AgentRunService;
import org.apache.hertzbeat.ai.gateway.conversation.AgentSessionService;
import org.apache.hertzbeat.ai.gateway.text.GatewayText;
import org.apache.hertzbeat.common.entity.agent.AgentRun;
import org.apache.hertzbeat.common.entity.agent.AgentSession;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
/**
* Run lifecycle commands. Cancellation never invokes runtime.
*/
@Service
public class RunCommandService {
private final AgentRunService runService;
private final AgentSessionService sessionService;
private final AgentRuntimeControlRegistry controlRegistry;
public RunCommandService(AgentRunService runService, AgentSessionService sessionService,
AgentRuntimeControlRegistry controlRegistry) {
this.runService = runService;
this.sessionService = sessionService;
this.controlRegistry = controlRegistry;
}
public GatewayResponse cancel(CancelRunCommand command) {
String runUid = requiredText(command.runUid(), "Agent run UID is required");
String reason = GatewayText.isBlank(command.reason())
? "Runtime cancellation requested."
: command.reason();
Optional<AgentRun> runOptional = runService.findRun(runUid);
if (runOptional.isEmpty()) {
GatewayEvent error = errorEvent(command.commandId(), null, runUid, "Agent run not found.");
return response(command, null, runUid, List.of(error));
}
AgentRun run = runOptional.get();
AgentSession session = session(run, command);
List<GatewayEvent> events;
if (session == null) {
events = List.of(errorEvent(command.commandId(), null, runUid, "Agent run not found."));
} else if (isTerminalRun(run)) {
events = List.of(errorEvent(command.commandId(), session, runUid,
"Agent run is already stopped."));
} else if (AgentRunStatus.RUNNING.name().equals(run.getStatus())) {
boolean cancelled = controlRegistry.cancel(runUid, reason);
events = cancelled ? List.of() : List.of(errorEvent(command.commandId(), session, runUid,
"Agent runtime run is not active or already stopped."));
} else {
AgentRun cancelledRun = runService.markCancelled(run, reason);
events = List.of(runCompletedEvent(command.commandId(), session, cancelledRun,
"Agent run cancelled."));
}
return response(command, session, runUid, events);
}
private GatewayResponse response(CancelRunCommand command, AgentSession session, String runUid,
List<GatewayEvent> events) {
if (command.replyMode() == ReplyMode.STREAM) {
return GatewayStreamResponse.builder()
.meta(Meta.builder()
.commandId(command.commandId())
.sessionUid(session == null ? null : session.getSessionUid())
.runUid(runUid)
.terminal(true)
.message("cancel")
.build())
.events(Flux.fromIterable(events))
.build();
}
return GatewaySingleResponse.builder()
.meta(Meta.builder()
.commandId(command.commandId())
.sessionUid(session == null ? null : session.getSessionUid())
.runUid(runUid)
.terminal(true)
.message("cancel")
.build())
.body(Map.of("runUid", runUid))
.events(events)
.build();
}
private GatewayEvent runCompletedEvent(String commandId, AgentSession session, AgentRun run, String message) {
return GatewayEvent.builder()
.type(GatewayEventType.RUN_COMPLETED)
.eventId(commandId + ":run-completed")
.sessionUid(session == null ? null : session.getSessionUid())
.runUid(run == null ? null : run.getRunUid())
.payload(RunCompletedPayload.builder().build())
.timestamp(System.currentTimeMillis())
.build();
}
private GatewayEvent errorEvent(String commandId, AgentSession session, String runUid, String message) {
return GatewayEvent.builder()
.type(GatewayEventType.ERROR)
.eventId(commandId + ":error")
.sessionUid(session == null ? null : session.getSessionUid())
.runUid(runUid)
.payload(ErrorPayload.builder()
.errorMessage(message)
.build())
.timestamp(System.currentTimeMillis())
.build();
}
private AgentSession session(AgentRun run, CancelRunCommand command) {
if (run == null || run.getSessionId() == null) {
return null;
}
return sessionService.findOwnedSession(String.valueOf(run.getSessionId()), command.envelope(),
command.originEntryType()).orElse(null);
}
private boolean isTerminalRun(AgentRun run) {
String status = run == null ? null : run.getStatus();
return AgentRunStatus.SUCCEEDED.name().equals(status)
|| AgentRunStatus.FAILED.name().equals(status)
|| AgentRunStatus.CANCELLED.name().equals(status)
|| AgentRunStatus.RECOVERY_REQUIRED.name().equals(status);
}
private String requiredText(String value, String message) {
// Command identifiers originate in channel requests and may contain surrounding whitespace before lookup.
String normalized = GatewayText.normalize(value);
if (normalized == null) {
throw new IllegalArgumentException(message);
}
return normalized;
}
}
@@ -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.ai.gateway.channel.core;
/** Built-in Agent Gateway ingress channel identifiers. */
public enum ChannelId {
WEB_UI("web-ui"),
SYSTEM("system");
private final String id;
ChannelId(String id) {
this.id = id;
}
public String id() {
return id;
}
}
@@ -1,174 +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.gateway.channel.webui;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.CreateModelProviderConfigurationCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.DeleteModelProviderConfigurationCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ListModelProviderConfigurationsCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ListModelProviderOptionsCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ReplyMode;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.SwitchModelProviderCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.UpdateModelProviderConfigurationCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommandRouter;
import org.apache.hertzbeat.ai.gateway.application.GatewayResponse.GatewaySingleResponse;
import org.apache.hertzbeat.ai.gateway.application.ModelProviderConfigurationView;
import org.apache.hertzbeat.ai.gateway.channel.core.ChannelId;
import org.apache.hertzbeat.ai.gateway.contract.GatewayEnvelope;
import org.apache.hertzbeat.ai.gateway.identity.ActorSupport;
import org.apache.hertzbeat.ai.gateway.runtime.provider.AgentModelProviderOption;
import org.apache.hertzbeat.common.entity.dto.Message;
import org.apache.hertzbeat.common.entity.dto.ModelProviderConfig;
import org.apache.hertzbeat.common.observability.gateway.AuthTokenRequestContext;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Agent model provider discovery and configuration API.
*/
@Tag(name = "Agent Model Provider API")
@RestController
@RequestMapping(path = "/api/agent/model-providers", produces = {APPLICATION_JSON_VALUE})
public class ModelProviderConfigController {
private final GatewayCommandRouter commandRouter;
public ModelProviderConfigController(GatewayCommandRouter commandRouter) {
this.commandRouter = commandRouter;
}
@GetMapping("/options")
@Operation(summary = "List registered Agent model provider configuration options")
public ResponseEntity<Message<List<AgentModelProviderOption>>> getOptions() {
GatewaySingleResponse response = (GatewaySingleResponse) commandRouter.handle(
ListModelProviderOptionsCommand.builder()
.envelope(webUiEnvelope())
.replyMode(ReplyMode.FINAL_ONLY)
.commandId("list-model-provider-options")
.build());
@SuppressWarnings("unchecked")
List<AgentModelProviderOption> options = (List<AgentModelProviderOption>) response.body();
return ResponseEntity.ok(Message.success(options));
}
@GetMapping("/configurations")
@Operation(summary = "List saved Agent model provider configurations")
public ResponseEntity<Message<ModelProviderConfigurationView>> getConfigurations() {
GatewaySingleResponse response = (GatewaySingleResponse) commandRouter.handle(
ListModelProviderConfigurationsCommand.builder()
.envelope(webUiEnvelope())
.replyMode(ReplyMode.FINAL_ONLY)
.commandId("list-model-provider-configurations")
.build());
return stateResponse(response);
}
@PostMapping("/configurations")
@Operation(summary = "Validate and create an Agent model provider configuration")
public ResponseEntity<Message<ModelProviderConfigurationView>> createConfiguration(
@RequestBody ModelProviderConfig config) {
GatewaySingleResponse response = (GatewaySingleResponse) commandRouter.handle(
CreateModelProviderConfigurationCommand.builder()
.envelope(webUiEnvelope())
.replyMode(ReplyMode.FINAL_ONLY)
.commandId("create-model-provider-configuration")
.config(config)
.build());
return stateResponse(response);
}
@PutMapping("/configurations/{providerUid}")
@Operation(summary = "Validate and update an Agent model provider configuration")
public ResponseEntity<Message<ModelProviderConfigurationView>> updateConfiguration(
@PathVariable String providerUid,
@RequestBody ModelProviderConfig config) {
GatewaySingleResponse response = (GatewaySingleResponse) commandRouter.handle(
UpdateModelProviderConfigurationCommand.builder()
.envelope(webUiEnvelope())
.replyMode(ReplyMode.FINAL_ONLY)
.commandId("update-model-provider-configuration")
.providerUid(providerUid)
.config(config)
.build());
return stateResponse(response);
}
@DeleteMapping("/configurations/{providerUid}")
@Operation(summary = "Delete an Agent model provider configuration")
public ResponseEntity<Message<ModelProviderConfigurationView>> deleteConfiguration(
@PathVariable String providerUid) {
GatewaySingleResponse response = (GatewaySingleResponse) commandRouter.handle(
DeleteModelProviderConfigurationCommand.builder()
.envelope(webUiEnvelope())
.replyMode(ReplyMode.FINAL_ONLY)
.commandId("delete-model-provider-configuration")
.providerUid(providerUid)
.build());
return stateResponse(response);
}
@PutMapping("/active/{providerUid}")
@Operation(summary = "Switch the active Agent model provider configuration")
public ResponseEntity<Message<ModelProviderConfigurationView>> switchConfiguration(
@PathVariable String providerUid) {
return switchConfiguration(providerUid, "switch-model-provider-configuration");
}
@DeleteMapping("/active")
@Operation(summary = "Switch the active Agent model provider to the YAML default")
public ResponseEntity<Message<ModelProviderConfigurationView>> switchToDefault() {
return switchConfiguration(null, "switch-model-provider-to-default");
}
private ResponseEntity<Message<ModelProviderConfigurationView>> switchConfiguration(
String providerUid, String commandId) {
GatewaySingleResponse response = (GatewaySingleResponse) commandRouter.handle(
SwitchModelProviderCommand.builder()
.envelope(webUiEnvelope())
.replyMode(ReplyMode.FINAL_ONLY)
.commandId(commandId)
.providerUid(providerUid)
.build());
return stateResponse(response);
}
private ResponseEntity<Message<ModelProviderConfigurationView>> stateResponse(
GatewaySingleResponse response) {
return ResponseEntity.ok(Message.success((ModelProviderConfigurationView) response.body()));
}
private GatewayEnvelope webUiEnvelope() {
return GatewayEnvelope.builder()
.channelId(ChannelId.WEB_UI.id())
.receivedAt(System.currentTimeMillis())
.actor(ActorSupport.requireCurrentAdminSurenessActor())
.workspaceId(AuthTokenRequestContext.currentWorkspaceId())
.build();
}
}
@@ -1,221 +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.gateway.channel.webui;
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 org.apache.hertzbeat.ai.gateway.channel.core.ChannelId;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommandRouter;
import org.apache.hertzbeat.ai.gateway.contract.GatewayEnvelope;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.GetSessionCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.GetRunCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.GetLatestSessionRunCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.GetSessionTranscriptCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ListSessionsCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ReplyMode;
import org.apache.hertzbeat.ai.gateway.application.GatewayResponse.GatewaySingleResponse;
import org.apache.hertzbeat.ai.gateway.identity.ActorSupport;
import org.apache.hertzbeat.ai.gateway.identity.AgentActor;
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeEntryType;
import org.apache.hertzbeat.ai.gateway.conversation.AgentRunSnapshot;
import org.apache.hertzbeat.ai.gateway.conversation.AgentSessionListItem;
import org.apache.hertzbeat.common.entity.agent.AgentTranscriptEntry;
import org.apache.hertzbeat.common.entity.dto.Message;
import org.apache.hertzbeat.common.observability.gateway.AuthTokenRequestContext;
import org.apache.hertzbeat.common.observability.gateway.AuthTokenScopes;
import org.springframework.data.domain.Page;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* Read-only Agent Gateway query API.
*/
@Tag(name = "Agent Query API")
@RestController
@RequestMapping(path = "/api/agent", produces = {APPLICATION_JSON_VALUE})
public class QueryController {
private final GatewayCommandRouter commandRouter;
public QueryController(GatewayCommandRouter commandRouter) {
this.commandRouter = commandRouter;
}
@GetMapping("/sessions/{sessionId}")
@Operation(summary = "Get Agent Gateway session")
public ResponseEntity<Message<GatewaySingleResponse>> getSession(
@Parameter(description = "Session ID or UID") @PathVariable String sessionId) {
return ResponseEntity.ok(Message.success((GatewaySingleResponse) commandRouter.handle(
GetSessionCommand.builder()
.envelope(webUiEnvelope())
.replyMode(ReplyMode.FINAL_ONLY)
.commandId("get-session:" + sessionId)
.originEntryType(AgentRuntimeEntryType.USER_INPUT)
.sessionUid(sessionId)
.build())));
}
@GetMapping("/runs/{runUid}")
@Operation(summary = "Get an owned Agent Gateway run")
public ResponseEntity<Message<AgentRunSnapshot>> getRun(
@Parameter(description = "Run UID") @PathVariable String runUid) {
GatewaySingleResponse response = (GatewaySingleResponse) commandRouter.handle(
GetRunCommand.builder()
.envelope(webUiEnvelope())
.replyMode(ReplyMode.FINAL_ONLY)
.commandId("get-run:" + runUid)
.originEntryType(AgentRuntimeEntryType.USER_INPUT)
.runUid(runUid)
.build());
return ResponseEntity.ok(Message.success((AgentRunSnapshot) response.body()));
}
@GetMapping("/sessions/{sessionUid}/latest-run")
@Operation(summary = "Get the latest owned Agent Gateway run for a session")
public ResponseEntity<Message<AgentRunSnapshot>> getLatestSessionRun(
@Parameter(description = "Session UID") @PathVariable String sessionUid) {
GatewaySingleResponse response = (GatewaySingleResponse) commandRouter.handle(
GetLatestSessionRunCommand.builder()
.envelope(webUiEnvelope())
.replyMode(ReplyMode.FINAL_ONLY)
.commandId("get-latest-run:" + sessionUid)
.originEntryType(AgentRuntimeEntryType.USER_INPUT)
.sessionUid(sessionUid)
.build());
return ResponseEntity.ok(Message.success((AgentRunSnapshot) response.body()));
}
@GetMapping("/sessions")
@Operation(summary = "List current WebUI user's Agent Gateway sessions")
@SuppressWarnings("unchecked")
public ResponseEntity<Message<Page<AgentSessionListItem>>> listSessions(
@RequestParam(defaultValue = "0") int pageIndex,
@RequestParam(defaultValue = "50") int pageSize) {
GatewaySingleResponse response = (GatewaySingleResponse) commandRouter.handle(
ListSessionsCommand.builder()
.envelope(webUiEnvelope())
.replyMode(ReplyMode.FINAL_ONLY)
.commandId("list-sessions:" + pageIndex)
.originEntryType(AgentRuntimeEntryType.USER_INPUT)
.title(null)
.pageIndex(pageIndex)
.pageSize(pageSize)
.build());
return ResponseEntity.ok(Message.success((Page<AgentSessionListItem>) response.body()));
}
@GetMapping("/alert-analysis/sessions")
@Operation(summary = "List automatic alert analysis sessions")
@SuppressWarnings("unchecked")
public ResponseEntity<Message<Page<AgentSessionListItem>>> listAlertAnalysisSessions(
@RequestParam(defaultValue = "0") int pageIndex,
@RequestParam(defaultValue = "50") int pageSize,
@RequestParam(required = false) String search) {
GatewaySingleResponse response = (GatewaySingleResponse) commandRouter.handle(
ListSessionsCommand.builder()
.envelope(alertAnalysisEnvelope())
.replyMode(ReplyMode.FINAL_ONLY)
.commandId("list-alert-analysis-sessions:" + pageIndex)
.originEntryType(AgentRuntimeEntryType.ALERT_TRIGGER)
.title(search)
.pageIndex(pageIndex)
.pageSize(pageSize)
.build());
return ResponseEntity.ok(Message.success((Page<AgentSessionListItem>) response.body()));
}
@GetMapping("/alert-analysis/sessions/{sessionId}")
@Operation(summary = "Get automatic alert analysis session")
public ResponseEntity<Message<GatewaySingleResponse>> getAlertAnalysisSession(
@Parameter(description = "Session ID or UID") @PathVariable String sessionId) {
return ResponseEntity.ok(Message.success((GatewaySingleResponse) commandRouter.handle(
GetSessionCommand.builder()
.envelope(alertAnalysisEnvelope())
.replyMode(ReplyMode.FINAL_ONLY)
.commandId("get-alert-analysis-session:" + sessionId)
.originEntryType(AgentRuntimeEntryType.ALERT_TRIGGER)
.sessionUid(sessionId)
.build())));
}
@GetMapping("/sessions/{sessionUid}/transcript")
@Operation(summary = "List Agent Gateway session transcript entries")
@SuppressWarnings("unchecked")
public ResponseEntity<Message<Page<AgentTranscriptEntry>>> listSessionTranscript(
@Parameter(description = "Session UID or numeric ID") @PathVariable String sessionUid,
@RequestParam(defaultValue = "0") int pageIndex,
@RequestParam(defaultValue = "50") int pageSize) {
GatewaySingleResponse response = (GatewaySingleResponse) commandRouter.handle(
GetSessionTranscriptCommand.builder()
.envelope(webUiEnvelope())
.replyMode(ReplyMode.FINAL_ONLY)
.commandId("get-session-transcript:" + sessionUid)
.originEntryType(AgentRuntimeEntryType.USER_INPUT)
.sessionUid(sessionUid)
.pageIndex(pageIndex)
.pageSize(pageSize)
.build());
return ResponseEntity.ok(Message.success((Page<AgentTranscriptEntry>) response.body()));
}
@GetMapping("/alert-analysis/sessions/{sessionUid}/transcript")
@Operation(summary = "List automatic alert analysis session transcript entries")
@SuppressWarnings("unchecked")
public ResponseEntity<Message<Page<AgentTranscriptEntry>>> listAlertAnalysisSessionTranscript(
@Parameter(description = "Session UID or numeric ID") @PathVariable String sessionUid,
@RequestParam(defaultValue = "0") int pageIndex,
@RequestParam(defaultValue = "50") int pageSize) {
GatewaySingleResponse response = (GatewaySingleResponse) commandRouter.handle(
GetSessionTranscriptCommand.builder()
.envelope(alertAnalysisEnvelope())
.replyMode(ReplyMode.FINAL_ONLY)
.commandId("get-alert-analysis-session-transcript:" + sessionUid)
.originEntryType(AgentRuntimeEntryType.ALERT_TRIGGER)
.sessionUid(sessionUid)
.pageIndex(pageIndex)
.pageSize(pageSize)
.build());
return ResponseEntity.ok(Message.success((Page<AgentTranscriptEntry>) response.body()));
}
private GatewayEnvelope webUiEnvelope() {
return GatewayEnvelope.builder()
.channelId(ChannelId.WEB_UI.id())
.receivedAt(System.currentTimeMillis())
.actor(ActorSupport.requireCurrentSurenessActor())
.workspaceId(AuthTokenRequestContext.currentWorkspaceId())
.build();
}
private GatewayEnvelope alertAnalysisEnvelope() {
ActorSupport.requireCurrentAdminSurenessActor();
return GatewayEnvelope.builder()
.channelId(ChannelId.SYSTEM.id())
.receivedAt(System.currentTimeMillis())
.actor(AgentActor.alertAnalysisActor())
.workspaceId(AuthTokenScopes.DEFAULT_WORKSPACE_ID)
.build();
}
}
@@ -1,217 +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.gateway.channel.webui;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
import static org.springframework.http.MediaType.TEXT_EVENT_STREAM_VALUE;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ApprovalDecisionCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.CancelRunCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.InvokeCommand;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommand.ReplyMode;
import org.apache.hertzbeat.ai.gateway.application.GatewayCommandRouter;
import org.apache.hertzbeat.ai.gateway.application.AgentTargetCanonicalizationService.FailureKind;
import org.apache.hertzbeat.ai.gateway.application.AgentTargetCanonicalizationService.TargetCanonicalizationException;
import org.apache.hertzbeat.ai.gateway.channel.core.ChannelId;
import org.apache.hertzbeat.ai.gateway.channel.webui.dto.WebUiChatStreamRequest;
import org.apache.hertzbeat.ai.gateway.contract.AgentResponseLanguage;
import org.apache.hertzbeat.ai.gateway.contract.GatewayEnvelope;
import org.apache.hertzbeat.ai.gateway.contract.UserInput;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.ErrorPayload;
import org.apache.hertzbeat.ai.gateway.application.GatewayEvent.GatewayEventType;
import org.apache.hertzbeat.ai.gateway.application.GatewayResponse.GatewaySingleResponse;
import org.apache.hertzbeat.ai.gateway.application.GatewayResponse.GatewayStreamResponse;
import org.apache.hertzbeat.common.entity.dto.Message;
import org.apache.hertzbeat.common.observability.gateway.AuthTokenRequestContext;
import org.apache.hertzbeat.ai.gateway.identity.ActorSupport;
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeEntryType;
import org.apache.hertzbeat.ai.gateway.tool.core.AgentApprovalDecision;
import org.apache.hertzbeat.ai.gateway.tool.interaction.AgentInteractionInputService;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
/**
* Interactive Agent Gateway endpoints used by the WebUI.
*/
@Tag(name = "Agent WebUI API")
@RestController
@RequestMapping(path = "/api/agent", produces = {APPLICATION_JSON_VALUE})
public class WebUiController {
private final GatewayCommandRouter commandRouter;
private final AgentInteractionInputService interactionInputService;
public WebUiController(GatewayCommandRouter commandRouter,
AgentInteractionInputService interactionInputService) {
this.commandRouter = commandRouter;
this.interactionInputService = interactionInputService;
}
@PostMapping("/webui/chat")
@Operation(summary = "Send a WebUI chat message")
public ResponseEntity<Message<GatewaySingleResponse>> chat(
@Valid @RequestBody WebUiChatStreamRequest request,
@RequestHeader(name = HttpHeaders.ACCEPT_LANGUAGE, required = false) String acceptLanguage) {
return ResponseEntity.ok(Message.success((GatewaySingleResponse) commandRouter.handle(
chatCommand(request, ReplyMode.FINAL_ONLY, acceptLanguage))));
}
@PostMapping(value = "/webui/chat/stream", produces = TEXT_EVENT_STREAM_VALUE)
@Operation(summary = "Stream a WebUI chat message")
public Flux<ServerSentEvent<GatewayEvent>> streamChat(
@Valid @RequestBody WebUiChatStreamRequest request,
@RequestHeader(name = HttpHeaders.ACCEPT_LANGUAGE, required = false) String acceptLanguage) {
InvokeCommand command = chatCommand(request, ReplyMode.STREAM, acceptLanguage);
return Flux.defer(() -> ((GatewayStreamResponse) commandRouter.handle(command)).events())
.map(this::toServerSentEvent)
.onErrorResume(exception -> Flux.just(toServerSentEvent(errorEvent(exception))));
}
@PostMapping("/runs/{runUid}/stop")
@Operation(summary = "Stop an Agent Gateway run")
public ResponseEntity<Message<GatewaySingleResponse>> stopRun(
@Parameter(description = "Run UID") @PathVariable String runUid) {
return ResponseEntity.ok(Message.success((GatewaySingleResponse) commandRouter.handle(
CancelRunCommand.builder()
.envelope(envelope())
.replyMode(ReplyMode.FINAL_ONLY)
.commandId("stop-run:" + runUid)
.originEntryType(AgentRuntimeEntryType.USER_INPUT)
.runUid(runUid)
.reason("Stopped by the WebUI user.")
.build())));
}
@PostMapping("/approvals/{approvalId}/approve")
@Operation(summary = "Approve Agent Gateway approval")
public ResponseEntity<Message<GatewaySingleResponse>> approve(
@Parameter(description = "Approval ID") @PathVariable String approvalId) {
ApprovalDecisionCommand command = ApprovalDecisionCommand.builder()
.envelope(envelope())
.replyMode(ReplyMode.FINAL_ONLY)
.commandId(approvalId)
.originEntryType(AgentRuntimeEntryType.USER_INPUT)
.approvalId(approvalId)
.decision(AgentApprovalDecision.APPROVED)
.build();
return ResponseEntity.ok(Message.success((GatewaySingleResponse) commandRouter.handle(command)));
}
@PostMapping("/approvals/{approvalId}/reject")
@Operation(summary = "Reject Agent Gateway approval")
public ResponseEntity<Message<GatewaySingleResponse>> reject(
@Parameter(description = "Approval ID") @PathVariable String approvalId) {
ApprovalDecisionCommand command = ApprovalDecisionCommand.builder()
.envelope(envelope())
.replyMode(ReplyMode.FINAL_ONLY)
.commandId(approvalId)
.originEntryType(AgentRuntimeEntryType.USER_INPUT)
.approvalId(approvalId)
.decision(AgentApprovalDecision.REJECTED)
.build();
return ResponseEntity.ok(Message.success((GatewaySingleResponse) commandRouter.handle(command)));
}
@PostMapping("/interactions/{interactionId}/submit")
@Operation(summary = "Submit values requested by an Agent interaction tool")
public ResponseEntity<Message<String>> submitInteraction(
@Parameter(description = "Interaction ID") @PathVariable String interactionId,
@RequestBody InteractionSubmission submission) {
GatewayEnvelope envelope = envelope();
interactionInputService.submit(interactionId, envelope.getActor(), envelope.getWorkspaceId(),
submission == null ? Map.of() : submission.values());
return ResponseEntity.ok(Message.success("submitted"));
}
private InvokeCommand chatCommand(WebUiChatStreamRequest request, ReplyMode replyMode, String acceptLanguage) {
// WebUI attachments are optional; commands expose an immutable collection to downstream services.
List<String> attachments = request.getAttachments() == null
? List.of()
: List.copyOf(request.getAttachments());
UserInput input = UserInput.builder()
.messageId(request.getMessageId())
.conversationId(request.getConversationId())
.target(request.getTarget())
.message(UserInput.Message.builder()
.text(request.getMessage())
.attachments(attachments)
.build())
.build();
return InvokeCommand.builder()
.envelope(envelope().toBuilder()
.preferredLanguage(AgentResponseLanguage.fromAcceptLanguage(acceptLanguage))
.build())
.replyMode(replyMode)
.commandId(input.getMessageId())
.userInput(input)
.entryType(AgentRuntimeEntryType.USER_INPUT)
.build();
}
private GatewayEnvelope envelope() {
return GatewayEnvelope.builder()
.channelId(ChannelId.WEB_UI.id())
.receivedAt(System.currentTimeMillis())
.actor(ActorSupport.requireCurrentSurenessActor())
.workspaceId(AuthTokenRequestContext.currentWorkspaceId())
.build();
}
private ServerSentEvent<GatewayEvent> toServerSentEvent(GatewayEvent item) {
return ServerSentEvent.builder(item)
.event(item.type().name().toLowerCase(Locale.ROOT))
.id(item.eventId())
.build();
}
private GatewayEvent errorEvent(Throwable failure) {
if (failure instanceof TargetCanonicalizationException canonicalization) {
boolean mismatch = canonicalization.kind() == FailureKind.MISMATCH;
return new GatewayEvent(GatewayEventType.ERROR, "webui:error", null, null, null, null,
new ErrorPayload(null, mismatch ? "Investigation target does not match"
: "Investigation target is unavailable",
mismatch ? "TARGET_MISMATCH" : "TARGET_UNAVAILABLE"), System.currentTimeMillis());
}
return new GatewayEvent(GatewayEventType.ERROR, "webui:error", null, null, null, null,
new ErrorPayload(null, "Agent Gateway stream failed", null), System.currentTimeMillis());
}
/** Values supplied to a pending interaction request. */
public record InteractionSubmission(Map<String, Object> values) {
public InteractionSubmission {
values = values == null ? Map.of() : Map.copyOf(values);
}
}
}
@@ -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.gateway.channel.webui.dto;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetRef;
/**
* WebUI chat stream request.
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class WebUiChatStreamRequest {
/**
* Stable conversation ID used to locate the Gateway session.
*/
@NotBlank
@Size(max = 256)
private String conversationId;
/**
* Stable message ID used to locate the Gateway run on retries.
*/
@NotBlank
@Size(max = 128)
private String messageId;
/**
* User input text.
*/
@NotBlank
@Size(max = 8192)
private String message;
/**
* Optional target reference.
*/
@Valid
private AgentTargetRef target;
/**
* Optional attachment references.
*/
private List<String> attachments;
}
@@ -1,52 +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.gateway.contract;
import jakarta.validation.constraints.Size;
import java.util.List;
import java.util.Objects;
import lombok.Builder;
/**
* Trusted, non-persistent identifiers describing one automatically correlated alert incident.
*/
@Builder
public record AgentAlertIncidentContext(
Long analysisPolicyId,
Long triggerAlertId,
@Size(max = 256) List<Long> alertIds,
int alertCount,
long windowStartedAt) {
public AgentAlertIncidentContext {
analysisPolicyId = Objects.requireNonNull(analysisPolicyId, "Analysis policy id is required");
alertIds = alertIds == null ? List.of() : List.copyOf(alertIds);
if (alertIds.size() > 256) {
throw new IllegalArgumentException("Alert incident supports at most 256 alert ids");
}
if (alertIds.stream().anyMatch(Objects::isNull)) {
throw new IllegalArgumentException("Alert incident ids must not contain null values");
}
if (alertCount <= 0) {
throw new IllegalArgumentException("Alert incident count must be positive");
}
if (windowStartedAt < 0) {
throw new IllegalArgumentException("Alert incident window start must not be negative");
}
}
}
@@ -1,99 +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.gateway.contract;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.PositiveOrZero;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/** Exact non-live Log Explore page scope selected by an operator. */
@Data
@Builder(toBuilder = true)
@AllArgsConstructor
@NoArgsConstructor
public class AgentLogRef {
@Positive
private Long start;
@Positive
private Long end;
@Pattern(regexp = "[A-Za-z0-9][A-Za-z0-9._:-]{0,127}")
private String traceId;
@Pattern(regexp = "[A-Za-z0-9][A-Za-z0-9._:-]{0,127}")
private String spanId;
@Min(1)
@Max(24)
private Integer severityNumber;
@Pattern(regexp = "TRACE|DEBUG|INFO|WARN|ERROR|FATAL")
private String severityText;
@Size(max = 256)
private String search;
@Size(max = 512)
private String serviceName;
@Size(max = 512)
private String serviceNamespace;
@Size(max = 512)
private String environment;
@Size(max = 2048)
private String resourceFilter;
@Size(max = 2048)
private String attributeFilter;
@NotNull
private Boolean hideInternal;
@NotNull
private Boolean hideNoise;
@NotNull
@PositiveOrZero
@Max(10_000)
private Integer pageIndex;
@NotNull
@Min(1)
@Max(100)
private Integer pageSize;
@AssertTrue
@JsonIgnore
public boolean isTimeWindowValid() {
return start != null && end != null && start < end;
}
}
@@ -1,60 +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.gateway.contract;
import java.util.List;
import java.util.Locale;
import org.springframework.util.StringUtils;
/**
* Resolves model response languages against the locales supported by the HertzBeat WebUI.
*/
public final class AgentResponseLanguage {
public static final String DEFAULT = "en-US";
private static final List<Locale> SUPPORTED = List.of(
Locale.forLanguageTag("en-US"),
Locale.forLanguageTag("zh-CN"),
Locale.forLanguageTag("zh-TW"),
Locale.forLanguageTag("ja-JP"),
Locale.forLanguageTag("pt-BR"));
private AgentResponseLanguage() {
}
public static String fromAcceptLanguage(String acceptLanguage) {
if (StringUtils.hasText(acceptLanguage)) {
try {
Locale matched = Locale.lookup(Locale.LanguageRange.parse(acceptLanguage), SUPPORTED);
if (matched != null) {
return matched.toLanguageTag();
}
} catch (IllegalArgumentException ignored) {
// Malformed external headers fall back to the configured system locale.
}
}
return systemDefault();
}
public static String systemDefault() {
Locale matched = Locale.lookup(
Locale.LanguageRange.parse(Locale.getDefault().toLanguageTag()), SUPPORTED);
return matched == null ? DEFAULT : matched.toLanguageTag();
}
}
@@ -1,43 +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.gateway.contract;
import java.util.List;
import lombok.Builder;
/** Versioned execution material persisted on the authoritative USER transcript entry. */
@Builder(toBuilder = true)
public record AgentRunRequestSnapshot(
String version,
String conversationId,
String messageId,
String entryType,
AgentTargetRef target,
AgentAlertIncidentContext alertIncident,
String message,
List<String> attachments,
String preferredLanguage,
String approvalHandling,
String replyMode) {
public static final String VERSION = "1";
public AgentRunRequestSnapshot {
attachments = attachments == null ? List.of() : List.copyOf(attachments);
}
}
@@ -1,43 +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.gateway.contract;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/** Canonical service identity exported from persisted entity identities. */
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AgentServiceRef {
@NotBlank
@Size(max = 512)
private String name;
@Size(max = 512)
private String namespace;
@Size(max = 512)
private String environment;
}
@@ -1,87 +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.gateway.contract;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.PositiveOrZero;
import jakarta.validation.constraints.Size;
import java.time.DateTimeException;
import java.time.ZoneId;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Signal query and time-window context selected by an operator.
*/
@Data
@Builder(toBuilder = true)
@AllArgsConstructor
@NoArgsConstructor
public class AgentSignalRef {
@NotBlank
@Pattern(regexp = "metrics|logs|traces")
private String type;
@Size(max = 2048)
private String query;
@Size(max = 64)
private String timeRange;
@PositiveOrZero
private Long start;
@PositiveOrZero
private Long end;
@Size(max = 64)
private String timezone;
/**
* Absolute windows must be complete and ordered; relative-only windows leave both boundaries absent.
*/
@AssertTrue
@JsonIgnore
public boolean isAbsoluteWindowValid() {
if (start == null && end == null) {
return true;
}
return start != null && end != null && start < end;
}
/** A supplied timezone must be a real IANA or fixed-offset zone. */
@AssertTrue
@JsonIgnore
public boolean isTimezoneValid() {
if (timezone == null) {
return true;
}
try {
ZoneId.of(timezone);
return true;
} catch (DateTimeException ignored) {
return false;
}
}
}
@@ -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.gateway.contract;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/** Manager-issued authority snapshot for a canonical Agent target. */
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AgentTargetAuthority {
@Positive
private Long bindingId;
@NotBlank
@Size(max = 64)
private String version;
@NotBlank
@Pattern(regexp = "sha256:[0-9a-f]{64}")
private String hash;
}
@@ -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.ai.gateway.contract;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* HertzBeat resource target referenced by an Agent Gateway request.
*/
@Data
@Builder(toBuilder = true)
@AllArgsConstructor
@NoArgsConstructor
public class AgentTargetRef {
@Size(max = 64)
private String version;
@Positive
private Long monitorId;
@Positive
private Long alertId;
@Size(max = 16)
private String alertType;
@Positive
private Long entityId;
@Size(max = 128)
private String collector;
@Valid
private AgentSignalRef signal;
@Valid
private AgentTopologyRef topology;
@Valid
private AgentTraceRef trace;
@Valid
private AgentLogRef log;
@Valid
private AgentServiceRef service;
@Valid
private AgentTargetAuthority authority;
}
@@ -1,90 +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.gateway.contract;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.PositiveOrZero;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Topology scope and selection selected by an operator.
*/
@Data
@Builder(toBuilder = true)
@AllArgsConstructor
@NoArgsConstructor
public class AgentTopologyRef {
@Positive
private Long rootEntityId;
@Size(max = 512)
private String nodeId;
@Size(max = 512)
private String edgeId;
@Min(1)
@Max(2)
private Integer depth;
@Size(max = 128)
private String environment;
@Size(max = 64)
private String sourceKind;
@Positive
private Long start;
@Positive
private Long end;
@Size(max = 128)
private String relationType;
private Boolean hideInternal;
@PositiveOrZero
@Max(10_000)
private Integer pageIndex;
@Min(1)
@Max(100)
private Integer pageSize;
@AssertTrue
@JsonIgnore
public boolean isSingleSelectionValid() {
return nodeId == null || edgeId == null;
}
@AssertTrue
@JsonIgnore
public boolean isTimeWindowValid() {
return start == null && end == null || start != null && end != null && start < end;
}
}
@@ -1,84 +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.gateway.contract;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.PositiveOrZero;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/** Exact Trace Explore detail scope selected by an operator. */
@Data
@Builder(toBuilder = true)
@AllArgsConstructor
@NoArgsConstructor
public class AgentTraceRef {
@NotBlank
@Pattern(regexp = "[A-Za-z0-9][A-Za-z0-9._:-]{0,127}")
private String traceId;
@Pattern(regexp = "[A-Za-z0-9][A-Za-z0-9._:-]{0,127}")
private String spanId;
@Positive
private Long start;
@Positive
private Long end;
@Size(max = 512)
private String serviceName;
@Size(max = 512)
private String serviceNamespace;
@Size(max = 512)
private String environment;
@Size(max = 2048)
private String resourceFilter;
@Size(max = 2048)
private String attributeFilter;
@PositiveOrZero
private Long minDurationMs;
@PositiveOrZero
private Long maxDurationMs;
@AssertTrue
@JsonIgnore
public boolean isTimeWindowValid() {
return start != null && end != null && start < end;
}
@AssertTrue
@JsonIgnore
public boolean isDurationRangeValid() {
return minDurationMs == null || maxDurationMs == null || minDurationMs <= maxDurationMs;
}
}
@@ -1,69 +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.gateway.contract;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Size;
import java.util.Objects;
import lombok.Builder;
import org.apache.hertzbeat.ai.gateway.identity.AgentActor;
import org.apache.hertzbeat.common.observability.gateway.AuthTokenScopes;
import org.springframework.util.StringUtils;
/**
* Trusted invocation metadata supplied by a channel boundary.
*/
@Builder(toBuilder = true)
public record GatewayEnvelope(
@Size(max = 64) String channelId,
Long receivedAt,
@Valid AgentActor actor,
@Size(max = 16) String preferredLanguage,
@Size(max = 128) String workspaceId) {
public GatewayEnvelope {
if (!StringUtils.hasText(channelId)) {
throw new IllegalArgumentException("Gateway envelope channel id is required");
}
receivedAt = Objects.requireNonNull(receivedAt, "Gateway envelope received time is required");
if (receivedAt < 0) {
throw new IllegalArgumentException("Gateway envelope received time must not be negative");
}
workspaceId = AuthTokenScopes.normalizeWorkspaceId(workspaceId);
}
public String getChannelId() {
return channelId;
}
public Long getReceivedAt() {
return receivedAt;
}
public AgentActor getActor() {
return actor;
}
public String getPreferredLanguage() {
return preferredLanguage;
}
public String getWorkspaceId() {
return workspaceId;
}
}
@@ -1,84 +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.gateway.contract;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Size;
import java.util.List;
import java.util.Objects;
import lombok.Builder;
import lombok.Getter;
import org.springframework.util.StringUtils;
/**
* Normalized user input accepted by Agent Gateway channels.
*/
@Getter
public class UserInput {
@Size(max = 128)
private final String messageId;
@Size(max = 256)
private final String conversationId;
@Valid
private final AgentTargetRef target;
@Valid
private final AgentAlertIncidentContext alertIncident;
@Valid
private final Message message;
@Builder(toBuilder = true)
private UserInput(String messageId, String conversationId, AgentTargetRef target,
AgentAlertIncidentContext alertIncident, Message message) {
if (!StringUtils.hasText(conversationId)) {
throw new IllegalArgumentException("Agent input conversation id is required");
}
this.messageId = messageId;
this.conversationId = conversationId;
this.target = target;
this.alertIncident = alertIncident;
// UserInput is the channel-normalization boundary consumed by session, run, transcript, and runtime services.
this.message = Objects.requireNonNull(message, "Agent input message is required");
}
/**
* Normalized message content owned by one user input.
*/
@Builder
public record Message(String text, List<String> attachments) {
public Message {
if (!StringUtils.hasText(text)) {
throw new IllegalArgumentException("Agent message text is required");
}
attachments = attachments == null ? List.of() : List.copyOf(attachments);
}
public String getText() {
return text;
}
public List<String> getAttachments() {
return attachments;
}
}
}
@@ -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.ai.gateway.conversation;
import java.util.List;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetRef;
/** Safe owner-scoped WebUI request material used for explicit fresh-ID retry. */
public record AgentRetryRequest(
String conversationId,
String messageId,
String message,
AgentTargetRef target,
List<String> attachments,
String preferredLanguage) {
public AgentRetryRequest {
attachments = attachments == null ? List.of() : List.copyOf(attachments);
}
}
@@ -1,34 +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.gateway.conversation;
import java.time.LocalDateTime;
import org.apache.hertzbeat.common.entity.agent.AgentRun;
/** Latest durable run fields used to project and order a session-list row. */
public record AgentRunListProjection(String status, LocalDateTime gmtUpdate) {
public static AgentRunListProjection from(AgentRun run) {
LocalDateTime update = run.getGmtUpdate();
if (update == null) {
update = run.getCompletedAt() != null ? run.getCompletedAt()
: run.getStartedAt() != null ? run.getStartedAt() : run.getGmtCreate();
}
return new AgentRunListProjection(run.getStatus(), update);
}
}
@@ -1,233 +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.gateway.conversation;
import jakarta.persistence.EntityManager;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.apache.hertzbeat.ai.gateway.conversation.persistence.AgentRunDao;
import org.apache.hertzbeat.ai.gateway.conversation.persistence.AgentSessionDao;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetRef;
import org.apache.hertzbeat.ai.gateway.contract.UserInput;
import org.apache.hertzbeat.ai.gateway.runtime.AgentRuntimeEntryType;
import org.apache.hertzbeat.ai.gateway.text.GatewayText;
import org.apache.hertzbeat.common.entity.agent.AgentRun;
import org.apache.hertzbeat.common.entity.agent.AgentSession;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.hertzbeat.common.util.SnowFlakeIdGenerator;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
/**
* Default run ledger service for Agent Gateway.
*/
@Service
public class AgentRunService {
private final AgentRunDao runDao;
private final AgentSessionDao sessionDao;
private final EntityManager entityManager;
public AgentRunService(AgentRunDao runDao, AgentSessionDao sessionDao, EntityManager entityManager) {
this.runDao = runDao;
this.sessionDao = sessionDao;
this.entityManager = entityManager;
}
public AgentRun createOrResumeRun(AgentSession session, UserInput userInput,
AgentRuntimeEntryType entryType) {
String messageId = userInput.getMessageId();
Optional<AgentRun> existingRun = runDao.findBySessionIdAndMessageId(session.getId(), messageId);
if (existingRun.isPresent()) {
return existingRun.get();
}
AgentRun run = buildRun(session, userInput, messageId, entryType);
try {
return runDao.saveAndFlush(run);
} catch (DataIntegrityViolationException e) {
entityManager.clear();
return runDao.findBySessionIdAndMessageId(session.getId(), messageId)
.orElseThrow(() -> e);
}
}
@Transactional
public AgentRun markRunning(AgentRun run) {
LocalDateTime transitionAt = LocalDateTime.now();
run.setStatus(AgentRunStatus.RUNNING.name());
run.setStartedAt(transitionAt);
run.setCompletedAt(null);
run.setErrorMessage(null);
AgentRun saved = runDao.save(run);
touchSession(run, transitionAt);
return saved;
}
@Transactional
public AgentRun markSucceeded(AgentRun run, String resultSummary) {
LocalDateTime transitionAt = LocalDateTime.now();
run.setStatus(AgentRunStatus.SUCCEEDED.name());
run.setResultSummary(GatewayText.redactSecrets(resultSummary));
run.setCompletedAt(transitionAt);
AgentRun saved = runDao.save(run);
touchSession(run, transitionAt);
return saved;
}
@Transactional
public AgentRun markFailed(AgentRun run, String errorMessage) {
LocalDateTime transitionAt = LocalDateTime.now();
run.setStatus(AgentRunStatus.FAILED.name());
run.setErrorMessage(GatewayText.safeSummary(errorMessage, 1024));
run.setCompletedAt(transitionAt);
AgentRun saved = runDao.save(run);
touchSession(run, transitionAt);
return saved;
}
@Transactional
public AgentRun markCancelled(AgentRun run, String reason) {
LocalDateTime transitionAt = LocalDateTime.now();
String safeReason = GatewayText.safeSummary(reason, 1024);
int updated = runDao.cancelIfActive(run.getId(),
List.of(AgentRunStatus.CREATED.name(), AgentRunStatus.RUNNING.name()),
AgentRunStatus.CANCELLED.name(), safeReason, transitionAt);
if (updated == 1) {
touchSession(run, transitionAt);
}
return runDao.findById(run.getId()).orElseThrow(
() -> new IllegalStateException("Agent run disappeared during cancellation"));
}
@Transactional
public AgentRun markRecoveryRequired(AgentRun run, String reason) {
LocalDateTime transitionAt = LocalDateTime.now();
run.setStatus(AgentRunStatus.RECOVERY_REQUIRED.name());
run.setErrorMessage(GatewayText.safeSummary(reason, 1024));
run.setCompletedAt(transitionAt);
AgentRun saved = runDao.save(run);
touchSession(run, transitionAt);
return saved;
}
public Optional<AgentRun> findRun(String runUid) {
// External run lookup values may contain surrounding whitespace; normalize before the persistence query.
String normalized = GatewayText.normalize(runUid);
if (normalized == null) {
return Optional.empty();
}
return runDao.findByRunUid(normalized);
}
public Optional<AgentRun> findLatestRun(Long sessionId) {
return sessionId == null ? Optional.empty() : runDao.findTopBySessionIdOrderByIdDesc(sessionId);
}
public Map<Long, AgentRunListProjection> findLatestRunProjections(Collection<Long> sessionIds) {
if (sessionIds == null || sessionIds.isEmpty()) {
return Map.of();
}
Map<Long, AgentRunListProjection> projections = new LinkedHashMap<>();
for (AgentRun run : runDao.findLatestBySessionIds(sessionIds)) {
if (run.getSessionId() != null && StringUtils.hasText(run.getStatus())) {
projections.put(run.getSessionId(), AgentRunListProjection.from(run));
}
}
return Map.copyOf(projections);
}
/**
* Restore the complete target snapshot recorded for a run, with a legacy-column fallback.
*/
public static AgentTargetRef targetFromRun(AgentRun run) {
if (run == null) {
return null;
}
if (StringUtils.hasText(run.getTargetContextJson())) {
AgentTargetRef target = JsonUtil.fromJson(run.getTargetContextJson(), AgentTargetRef.class);
if (target == null) {
throw new IllegalStateException("Agent run target context cannot be decoded");
}
return target;
}
if (run.getTargetMonitorId() == null
&& run.getTargetAlertId() == null
&& !StringUtils.hasText(run.getTargetCollector())) {
return null;
}
return AgentTargetRef.builder()
.monitorId(run.getTargetMonitorId())
.alertId(run.getTargetAlertId())
.collector(run.getTargetCollector())
.build();
}
public Optional<AgentRun> findCreatedRun(Long sessionId) {
return runDao.findFirstBySessionIdAndStatusOrderByGmtCreateAsc(
sessionId, AgentRunStatus.CREATED.name());
}
public Optional<AgentRun> findRunningRun(Long sessionId) {
return runDao.findFirstBySessionIdAndStatusOrderByGmtCreateAsc(
sessionId, AgentRunStatus.RUNNING.name());
}
public boolean hasActiveRun(Long sessionId) {
return runDao.existsBySessionIdAndStatusIn(sessionId,
List.of(AgentRunStatus.CREATED.name(), AgentRunStatus.RUNNING.name()));
}
private void touchSession(AgentRun run, LocalDateTime transitionAt) {
if (sessionDao.advanceGmtUpdate(run.getSessionId(), transitionAt) != 1) {
throw new IllegalStateException("Agent run session does not exist");
}
}
private AgentRun buildRun(AgentSession session, UserInput userInput, String messageId,
AgentRuntimeEntryType entryType) {
AgentTargetRef target = userInput.getTarget();
return AgentRun.builder()
.runUid("run_" + SnowFlakeIdGenerator.generateId())
.sessionId(session.getId())
.messageId(messageId)
.entryType(entryType.name())
.targetMonitorId(target == null ? null : target.getMonitorId())
.targetAlertId(target == null ? null : target.getAlertId())
.targetCollector(target == null ? null : target.getCollector())
.targetContextJson(targetJson(target))
.status(AgentRunStatus.CREATED.name())
.build();
}
private static String targetJson(AgentTargetRef target) {
if (target == null) {
return null;
}
String json = JsonUtil.toJson(target);
if (!StringUtils.hasText(json)) {
throw new IllegalArgumentException("Agent run target context cannot be serialized");
}
return GatewayText.redactSecrets(json);
}
}
@@ -1,38 +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.gateway.conversation;
import java.time.LocalDateTime;
import org.apache.hertzbeat.ai.gateway.contract.AgentTargetRef;
/**
* Owner-checked durable projection of one Agent Gateway run.
*/
public record AgentRunSnapshot(
String runUid,
String sessionUid,
String messageId,
String status,
AgentTargetRef target,
String result,
String errorMessage,
boolean replayAvailable,
LocalDateTime startedAt,
LocalDateTime completedAt,
AgentRetryRequest retryRequest) {
}

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