Compare commits

...
Author SHA1 Message Date
tomsun28 5d3ab01c56 [doc] update 2025-11-01 17:27:29 +08:00
tomsun28 8310a9abd4 [webapp] update ui
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-11-01 17:22:00 +08:00
tomsun28 40178765a6 [webapp] update ui
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-11-01 17:14:21 +08:00
tomsun28 01ef70baf5 [webapp] update ui
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-11-01 16:56:26 +08:00
tomsun28 a7cc0870c1 [webapp] update ui
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-11-01 16:44:27 +08:00
tomsun28 4693b0a296 [webapp] update ui
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-11-01 16:33:46 +08:00
tomsun28 271b94693a [webapp] update ui
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-11-01 16:27:52 +08:00
tomsun28 13691cb09b [webapp] update ui
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-11-01 15:26:51 +08:00
tomsun28 d1c42d4bad [webapp] update ui
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-11-01 15:23:48 +08:00
tomsun28 35b8d87045 [webapp] update ui
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-11-01 14:46:07 +08:00
tomsun28 b131061fc0 refactor
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-11-01 14:23:42 +08:00
tomsun28 04a9335a3b [doc] update 2025-11-01 12:25:42 +08:00
Tomsun28 0f86d32afb Merge branch 'master' into update-ai-with 2025-11-01 12:21:17 +08:00
Calvin 5a9f371e9d [doc] add pmc blog of Calvin, add Duansg as committer and Calvin as PMC (#3838) 2025-11-01 09:38:45 +08:00
tomsun28 faf2399b0a refactor
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-11-01 00:48:17 +08:00
tomsun28 76539fb6f1 refactor
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-31 22:26:31 +08:00
tomsun28 49bb1f4652 Merge branch 'master' into update-ai-with 2025-10-31 21:20:11 +08:00
tomsun28 27c68e31c6 [webapp] update ui
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-31 21:14:36 +08:00
Duansg 5c3d1a04de [improve] SLS integrated access supports merged alarm resolution (#3835) 2025-10-31 21:00:34 +08:00
tomsun28 a68956c34c [webapp] update ui
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-31 21:00:01 +08:00
tomsun28 908ae61f0a [webapp] update ui
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-31 20:47:11 +08:00
tomsun28 065fcf48bf [webapp] update ui
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-31 20:44:04 +08:00
tomsun28 41fec4149c refactor
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-31 20:25:17 +08:00
tomsun28 48d0f5e1a8 refactor
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-31 20:21:59 +08:00
tomsun28 212852fe9b refactor
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-31 20:17:47 +08:00
tomsun28 dc98713c0f refactor
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-31 20:13:06 +08:00
aias00 dd59aedbef Merge branch 'master' into update-ai-with 2025-10-31 10:16:23 +08:00
Cyantyandaias00 024b44f477 [ci] replace outdated link addresses in dead link check (#3834)
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-10-31 10:16:10 +08:00
aias00 83630a1331 Merge branch 'master' into update-ai-with 2025-10-31 09:36:49 +08:00
Cyanty 35046f8a06 [ci] fix markdown lint ci (#3833) 2025-10-31 09:36:28 +08:00
tomsun28 5e488d4a4a refactor
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-31 01:08:37 +08:00
tomsun28 b781a0bcbe [doc] update 2025-10-31 01:05:13 +08:00
tomsun28 be31129428 refactor
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-31 00:59:38 +08:00
tomsun28 6a54bdfbb0 [doc] update 2025-10-31 00:52:30 +08:00
tomsun28 7846f3276d refactor
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-31 00:49:49 +08:00
tomsun28 a3cfaf2be6 refactor
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-31 00:48:37 +08:00
tomsun28 7f5513ae56 [webapp] update ui
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-31 00:37:02 +08:00
tomsun28 22d6ad7ec0 [webapp] update ui
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-31 00:14:40 +08:00
tomsun28 cf7efb09f8 [webapp] update ui
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-30 23:52:41 +08:00
tomsun28 f7428c25c3 [webapp] update ui
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-30 23:42:30 +08:00
tomsun28 7b92448172 [webapp] update ui
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-30 23:04:32 +08:00
tomsun28 fd7d9980ee refactor
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-30 23:00:36 +08:00
tomsun28 776d806826 refactor
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-30 22:48:33 +08:00
tomsun28 4a63eb5cc0 refactor
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-30 22:38:45 +08:00
tomsun28 3b58e3dcdd fix bug
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-30 22:11:59 +08:00
Duansgandaias00 23c2f85e8a [fix] Optimize the issue where missing tags cause alert statuses to become disordered (#3829)
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-10-29 14:24:02 +08:00
Tomsun28 56226c1d52 refactor: split manager to startup module (#3830)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-29 13:49:04 +08:00
Tomsun28 9014f117d3 docs: update contributors page (#3827)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-25 13:53:35 +08:00
Tomsun28andCalvin e073bfee91 refactor: refactor ai feature (#3823)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-10-25 12:39:53 +08:00
Duansg 897d5d4220 [fix]Fixed issue where refresh did not take effect (#3825) 2025-10-24 21:58:22 +08:00
DuansgandTomsun28 4228605a3d [improve] Jexl validation rule enhancements (#3821)
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2025-10-19 23:08:19 +08:00
DeleiGuo e7131b1881 [docs] fix the issue of avatar reference on the team page (#3820) 2025-10-18 17:59:16 +08:00
DuansgandTomsun28 8a22797bca [fix] Only POST and PUT methods carry payload (#3814)
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2025-10-14 21:51:50 +08:00
b9e569a1e0 [fix]: fix bugs in frontend for monitoring (#3806)
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2025-10-13 23:54:11 +08:00
1284b4b46b [feat] Monitoring Center - Added cron expression support for scheduling type (#3777)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: Sherlock Yin <sherlock.yin1994@gmail.com>
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2025-10-12 00:12:26 +08:00
175c289e66 modify apt sources from http to https. (#3808)
Co-authored-by: liwei <liwei@itg.com.cn>
Co-authored-by: Logic <zqr10159@dromara.org>
2025-10-11 14:37:06 +08:00
Duansg 899e92882d [refactor]Refactoring prometheus label value utf8 support (#3810) 2025-10-11 11:31:09 +08:00
Yang ChenandTomsun28 d516b3a15d improvement: improve log severity text input (#3805)
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2025-10-08 00:31:31 +08:00
pentium100andTomsun28 7678c3f139 fix some bug in service discovery: (#3804)
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2025-10-08 00:29:30 +08:00
Tomsun28 3b49c0cb75 chore: use pnpm instead of yarn (#3802) 2025-10-06 21:33:11 +08:00
zhou yong kangandTomsun28 32d784e57e fix: use UTF-8 to solve Chineses bug (#3792)
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2025-10-05 09:17:33 +08:00
pentium100andTomsun28 d8f8444275 [feature] dns add record type (#3799)
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2025-10-05 08:58:34 +08:00
Dedy Kurniawan Santoso 41676c1f13 [improve] nitpicking "Dispatch ALl" on en-US.json (#3800)
Signed-off-by: Dedy Kurniawan Santoso <dedy@xendit.co>
2025-10-04 11:08:13 +08:00
Yang ChenandTomsun28 a3def497b4 [fix] remove v174 and add v180 flway script (#3787)
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2025-10-01 00:20:55 +08:00
381db85149 [OSPP] Modify Dockerfile to support RISC-V. (#3713)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: Tom <tomsun28@outlook.com>
2025-09-30 23:22:31 +08:00
会功夫的李白andCalvin d0daa2eec5 [improve] Optimize Gretimedb time-series statistics. (#3776)
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-09-25 22:56:13 +08:00
9b5f0f7fce [OSPP] Apache HertzBeat Log Monitoring Capability (#3673)
Signed-off-by: Yang Chen <1597081640@qq.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Jast <shenghang@apache.org>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-25 00:29:00 +08:00
Tom b3ef3d8a6f [docs] update doc and add new contributors (#3783)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-09-24 14:15:49 +08:00
432 changed files with 54389 additions and 29781 deletions
+74 -8
View File
@@ -7,7 +7,6 @@
"README.md",
"README_CN.md",
"README_JP.md"
"home/src/pages/team/index.jsx"
],
"imageSize": 100,
"commit": true,
@@ -1275,11 +1274,7 @@
"avatar_url": "https://avatars.githubusercontent.com/u/105542329?v=4",
"profile": "https://github.com/lynx009",
"contributions": [
"code",
"doc",
"blog",
"bug",
"design"
"doc"
]
},
{
@@ -2589,8 +2584,7 @@
"avatar_url": "https://avatars.githubusercontent.com/u/95274591?v=4",
"profile": "https://github.com/tuzuy",
"contributions": [
"code",
"design"
"code"
]
},
{
@@ -2601,6 +2595,78 @@
"contributions": [
"code"
]
},
{
"login": "cxhello",
"name": "cxhello",
"avatar_url": "https://avatars.githubusercontent.com/u/49056040?v=4",
"profile": "https://cxhello.top/",
"contributions": [
"doc"
]
},
{
"login": "jl15988",
"name": "会功夫的李白",
"avatar_url": "https://avatars.githubusercontent.com/u/70638770?v=4",
"profile": "https://github.com/jl15988",
"contributions": [
"code"
]
},
{
"login": "AlbertYang0801",
"name": "Albert.Yang",
"avatar_url": "https://avatars.githubusercontent.com/u/37127008?v=4",
"profile": "https://blog.aytop.cloud/",
"contributions": [
"code"
]
},
{
"login": "mengnankkkk",
"name": "zhou yong kang",
"avatar_url": "https://avatars.githubusercontent.com/u/150590575?v=4",
"profile": "https://blog.tokenlen.top/",
"contributions": [
"code"
]
},
{
"login": "warrobe",
"name": "warrobe",
"avatar_url": "https://avatars.githubusercontent.com/u/89446159?v=4",
"profile": "https://github.com/warrobe",
"contributions": [
"code"
]
},
{
"login": "Jetiaime",
"name": "TeAmo",
"avatar_url": "https://avatars.githubusercontent.com/u/93769000?v=4",
"profile": "https://github.com/Jetiaime",
"contributions": [
"code"
]
},
{
"login": "pentium100",
"name": "pentium100",
"avatar_url": "https://avatars.githubusercontent.com/u/27917?v=4",
"profile": "https://github.com/pentium100",
"contributions": [
"code"
]
},
{
"login": "dedyks",
"name": "Dedy Kurniawan Santoso",
"avatar_url": "https://avatars.githubusercontent.com/u/23741665?v=4",
"profile": "https://github.com/dedyks",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,
-1
View File
@@ -15,7 +15,6 @@
"customizations": {
"vscode": {
"settings": {},
// same extensions as Gitpod, should match /.gitpod.yml
"extensions": [
"vscjava.vscode-java-pack",
"editorconfig.editorconfig",
+1 -1
View File
@@ -39,7 +39,7 @@ jobs:
python-version: '3.8'
- name: Check Markdown
uses: DavidAnson/markdownlint-cli2-action@v16
uses: DavidAnson/markdownlint-cli2-action@992badcdf24e3b8eb7e87ff9287fe931bcb00c6e
with:
globs: './home/**/*.md'
+6 -3
View File
@@ -35,9 +35,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Yarn Install
- uses: pnpm/action-setup@v4
with:
version: 10
- name: Install
working-directory: web-app
run: yarn
run: pnpm install
- name: EsLint Test
working-directory: web-app
run: yarn lint:ts
run: pnpm lint:ts
+5 -2
View File
@@ -31,12 +31,15 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: ./script/ci/github-actions/setup-deps
- uses: pnpm/action-setup@v4
with:
version: 10
- name: Build the Frontend
run: |
cd web-app
yarn install
yarn package
pnpm install
pnpm package
- name: Build the Backend
run: |
+1
View File
@@ -66,6 +66,7 @@ header:
- '.gitpod.yml'
- 'karma.conf.js'
- 'proxy.conf.js'
- 'pnpm-lock.yaml'
- '.helmignore'
- 'script/helm/**'
- 'web-app/src/app/core/**'
+10 -14
View File
@@ -44,19 +44,17 @@ Even small corrections to typos are very welcome :)
#### Frontend start
1. Need `Node Yarn` Environment, Make sure `Node.js >= 18`
1. Need `Node pnpm` Environment, Make sure `Node.js >= 18`
2. Cd to the `web-app` directory: `cd web-app`
3. Install yarn if not existed `npm install -g yarn`
3. Install pnpm if not existed `npm install -g pnpm`
4. Install Dependencies: `yarn install` or `yarn install --registry=https://registry.npmmirror.com` in `web-app`
4. Install Dependencies: `pnpm install` or `pnpm install --registry=https://registry.npmmirror.com` in `web-app`
5. Install angular-cli globally: `yarn global add @angular/cli@15` or `yarn global add @angular/cli@15 --registry=https://registry.npmmirror.com`
5. After the local backend is started, start the local frontend in the web-app directory: `pnpm start`
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*
6. Browser access to localhost:4200 to start, default account/password is *admin/hertzbeat*
### Find tasks
@@ -198,19 +196,17 @@ Add WeChat account `ahertzbeat` to pull you into the WeChat group.
#### 前端启动
1. 需要 `nodejs yarn` 环境, Make sure `Node.js >= 18`
1. 需要 `nodejs pnpm` 环境, Make sure `Node.js >= 18`
2. 进入 `web-app` 目录: `cd web-app`
3. 安装yarn: `npm install -g yarn`
3. 安装pnpm: `npm install -g pnpm`
4. 在前端工程目录 `web-app` 下执行: `yarn install` or `yarn install --registry=https://registry.npmmirror.com` in `web-app`
4. 在前端工程目录 `web-app` 下执行: `pnpm install` or `pnpm install --registry=https://registry.npmmirror.com` in `web-app`
5. 全局安装 `angular-cli`: `yarn global add @angular/cli@15` or `yarn global add @angular/cli@15 --registry=https://registry.npmmirror.com`
5. 待本地后端启动后,在web-app目录下启动本地前端 `pnpm start`
6. 待本地后端启动后,在web-app目录下启动本地前端 `ng serve --open`
7. 浏览器访问 localhost:4200 即可开始,默认账号密码 *admin/hertzbeat*
6. 浏览器访问 localhost:4200 即可开始,默认账号密码 *admin/hertzbeat*
### 寻找任务
+22 -2
View File
@@ -361,7 +361,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/a-little-fool"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="小笨蛋"/><br /><sub><b>小笨蛋</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=a-little-fool" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=a-little-fool" title="Documentation">📖</a> <a href="#blog-a-little-fool" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aa-little-fool" title="Bug reports">🐛</a> <a href="#design-a-little-fool" title="Design">🎨</a></td>
<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>
@@ -385,7 +385,7 @@ Thanks to these wonderful people, welcome to join us:
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1036664317"><img src="https://avatars.githubusercontent.com/u/7696697?v=4?s=100" width="100px;" alt="moghn"/><br /><sub><b>moghn</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1036664317" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaoguolong"><img src="https://avatars.githubusercontent.com/u/33684988?v=4?s=100" width="100px;" alt="xiaoguolong"/><br /><sub><b>xiaoguolong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaoguolong" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Clownsw"><img src="https://avatars.githubusercontent.com/u/28394742?v=4?s=100" width="100px;" alt="Smliexx"/><br /><sub><b>Smliexx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Clownsw" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AClownsw" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Calvin979"><img src="https://avatars.githubusercontent.com/u/131688897?v=4?s=100" width="100px;" alt="Naruse"/><br /><sub><b>Naruse</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Code">💻</a> <a href="#design-Calvin979" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACalvin979" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/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>
@@ -529,6 +529,26 @@ Thanks to these wonderful people, welcome to join us:
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaomizhou2"><img src="https://avatars.githubusercontent.com/u/47807926?v=4?s=100" width="100px;" alt="zhangyaxi"/><br /><sub><b>zhangyaxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RainBondsongyg"><img src="https://avatars.githubusercontent.com/u/94501396?v=4?s=100" width="100px;" alt="songyg"/><br /><sub><b>songyg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=RainBondsongyg" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lx1229"><img src="https://avatars.githubusercontent.com/u/44620005?v=4?s=100" width="100px;" alt="Liuxin"/><br /><sub><b>Liuxin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lx1229" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yy549159265"><img src="https://avatars.githubusercontent.com/u/40821310?v=4?s=100" width="100px;" alt="yy549159265"/><br /><sub><b>yy549159265</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Tests">⚠️</a> <a href="#design-yy549159265" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cto-huhang"><img src="https://avatars.githubusercontent.com/u/53338629?v=4?s=100" width="100px;" alt="cto-huhang"/><br /><sub><b>cto-huhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cto-huhang" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Saramanda9988"><img src="https://avatars.githubusercontent.com/u/176664901?v=4?s=100" width="100px;" alt="LunaRain_079"/><br /><sub><b>LunaRain_079</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/delei"><img src="https://avatars.githubusercontent.com/u/17263766?v=4?s=100" width="100px;" alt="DeleiGuo"/><br /><sub><b>DeleiGuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Adelei" title="Bug reports">🐛</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chingjustwe"><img src="https://avatars.githubusercontent.com/u/13643747?v=4?s=100" width="100px;" alt="Rocky, Chi"/><br /><sub><b>Rocky, Chi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=chingjustwe" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rowankid"><img src="https://avatars.githubusercontent.com/u/18652781?v=4?s=100" width="100px;" alt="Wenqi Luo"/><br /><sub><b>Wenqi Luo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3Arowankid" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tuzuy"><img src="https://avatars.githubusercontent.com/u/95274591?v=4?s=100" width="100px;" alt="tuzuy"/><br /><sub><b>tuzuy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tuzuy" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/carlpinto25"><img src="https://avatars.githubusercontent.com/u/117299909?v=4?s=100" width="100px;" alt="carl pinto"/><br /><sub><b>carl pinto</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=carlpinto25" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://cxhello.top/"><img src="https://avatars.githubusercontent.com/u/49056040?v=4?s=100" width="100px;" alt="cxhello"/><br /><sub><b>cxhello</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cxhello" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jl15988"><img src="https://avatars.githubusercontent.com/u/70638770?v=4?s=100" width="100px;" alt="会功夫的李白"/><br /><sub><b>会功夫的李白</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jl15988" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://blog.aytop.cloud/"><img src="https://avatars.githubusercontent.com/u/37127008?v=4?s=100" width="100px;" alt="Albert.Yang"/><br /><sub><b>Albert.Yang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=AlbertYang0801" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://blog.tokenlen.top/"><img src="https://avatars.githubusercontent.com/u/150590575?v=4?s=100" width="100px;" alt="zhou yong kang"/><br /><sub><b>zhou yong kang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mengnankkkk" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/warrobe"><img src="https://avatars.githubusercontent.com/u/89446159?v=4?s=100" width="100px;" alt="warrobe"/><br /><sub><b>warrobe</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=warrobe" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Jetiaime"><img src="https://avatars.githubusercontent.com/u/93769000?v=4?s=100" width="100px;" alt="TeAmo"/><br /><sub><b>TeAmo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Jetiaime" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pentium100"><img src="https://avatars.githubusercontent.com/u/27917?v=4?s=100" width="100px;" alt="pentium100"/><br /><sub><b>pentium100</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pentium100" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dedyks"><img src="https://avatars.githubusercontent.com/u/23741665?v=4?s=100" width="100px;" alt="Dedy Kurniawan Santoso"/><br /><sub><b>Dedy Kurniawan Santoso</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dedyks" title="Code">💻</a></td>
</tr>
</tbody>
</table>
+22 -2
View File
@@ -359,7 +359,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/a-little-fool"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="小笨蛋"/><br /><sub><b>小笨蛋</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=a-little-fool" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=a-little-fool" title="Documentation">📖</a> <a href="#blog-a-little-fool" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aa-little-fool" title="Bug reports">🐛</a> <a href="#design-a-little-fool" title="Design">🎨</a></td>
<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>
@@ -383,7 +383,7 @@ Thanks these wonderful people, welcome to join us:
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1036664317"><img src="https://avatars.githubusercontent.com/u/7696697?v=4?s=100" width="100px;" alt="moghn"/><br /><sub><b>moghn</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1036664317" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaoguolong"><img src="https://avatars.githubusercontent.com/u/33684988?v=4?s=100" width="100px;" alt="xiaoguolong"/><br /><sub><b>xiaoguolong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaoguolong" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Clownsw"><img src="https://avatars.githubusercontent.com/u/28394742?v=4?s=100" width="100px;" alt="Smliexx"/><br /><sub><b>Smliexx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Clownsw" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AClownsw" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Calvin979"><img src="https://avatars.githubusercontent.com/u/131688897?v=4?s=100" width="100px;" alt="Naruse"/><br /><sub><b>Naruse</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Code">💻</a> <a href="#design-Calvin979" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACalvin979" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/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>
@@ -527,6 +527,26 @@ Thanks these wonderful people, welcome to join us:
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaomizhou2"><img src="https://avatars.githubusercontent.com/u/47807926?v=4?s=100" width="100px;" alt="zhangyaxi"/><br /><sub><b>zhangyaxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RainBondsongyg"><img src="https://avatars.githubusercontent.com/u/94501396?v=4?s=100" width="100px;" alt="songyg"/><br /><sub><b>songyg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=RainBondsongyg" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lx1229"><img src="https://avatars.githubusercontent.com/u/44620005?v=4?s=100" width="100px;" alt="Liuxin"/><br /><sub><b>Liuxin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lx1229" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yy549159265"><img src="https://avatars.githubusercontent.com/u/40821310?v=4?s=100" width="100px;" alt="yy549159265"/><br /><sub><b>yy549159265</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Tests">⚠️</a> <a href="#design-yy549159265" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cto-huhang"><img src="https://avatars.githubusercontent.com/u/53338629?v=4?s=100" width="100px;" alt="cto-huhang"/><br /><sub><b>cto-huhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cto-huhang" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Saramanda9988"><img src="https://avatars.githubusercontent.com/u/176664901?v=4?s=100" width="100px;" alt="LunaRain_079"/><br /><sub><b>LunaRain_079</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/delei"><img src="https://avatars.githubusercontent.com/u/17263766?v=4?s=100" width="100px;" alt="DeleiGuo"/><br /><sub><b>DeleiGuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Adelei" title="Bug reports">🐛</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chingjustwe"><img src="https://avatars.githubusercontent.com/u/13643747?v=4?s=100" width="100px;" alt="Rocky, Chi"/><br /><sub><b>Rocky, Chi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=chingjustwe" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rowankid"><img src="https://avatars.githubusercontent.com/u/18652781?v=4?s=100" width="100px;" alt="Wenqi Luo"/><br /><sub><b>Wenqi Luo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3Arowankid" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tuzuy"><img src="https://avatars.githubusercontent.com/u/95274591?v=4?s=100" width="100px;" alt="tuzuy"/><br /><sub><b>tuzuy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tuzuy" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/carlpinto25"><img src="https://avatars.githubusercontent.com/u/117299909?v=4?s=100" width="100px;" alt="carl pinto"/><br /><sub><b>carl pinto</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=carlpinto25" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://cxhello.top/"><img src="https://avatars.githubusercontent.com/u/49056040?v=4?s=100" width="100px;" alt="cxhello"/><br /><sub><b>cxhello</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cxhello" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jl15988"><img src="https://avatars.githubusercontent.com/u/70638770?v=4?s=100" width="100px;" alt="会功夫的李白"/><br /><sub><b>会功夫的李白</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jl15988" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://blog.aytop.cloud/"><img src="https://avatars.githubusercontent.com/u/37127008?v=4?s=100" width="100px;" alt="Albert.Yang"/><br /><sub><b>Albert.Yang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=AlbertYang0801" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://blog.tokenlen.top/"><img src="https://avatars.githubusercontent.com/u/150590575?v=4?s=100" width="100px;" alt="zhou yong kang"/><br /><sub><b>zhou yong kang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mengnankkkk" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/warrobe"><img src="https://avatars.githubusercontent.com/u/89446159?v=4?s=100" width="100px;" alt="warrobe"/><br /><sub><b>warrobe</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=warrobe" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Jetiaime"><img src="https://avatars.githubusercontent.com/u/93769000?v=4?s=100" width="100px;" alt="TeAmo"/><br /><sub><b>TeAmo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Jetiaime" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pentium100"><img src="https://avatars.githubusercontent.com/u/27917?v=4?s=100" width="100px;" alt="pentium100"/><br /><sub><b>pentium100</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pentium100" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dedyks"><img src="https://avatars.githubusercontent.com/u/23741665?v=4?s=100" width="100px;" alt="Dedy Kurniawan Santoso"/><br /><sub><b>Dedy Kurniawan Santoso</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dedyks" title="Code">💻</a></td>
</tr>
</tbody>
</table>
+22 -2
View File
@@ -363,7 +363,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/a-little-fool"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="小笨蛋"/><br /><sub><b>小笨蛋</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=a-little-fool" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=a-little-fool" title="Documentation">📖</a> <a href="#blog-a-little-fool" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aa-little-fool" title="Bug reports">🐛</a> <a href="#design-a-little-fool" title="Design">🎨</a></td>
<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>
@@ -387,7 +387,7 @@ Thanks these wonderful people, welcome to join us:
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1036664317"><img src="https://avatars.githubusercontent.com/u/7696697?v=4?s=100" width="100px;" alt="moghn"/><br /><sub><b>moghn</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1036664317" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaoguolong"><img src="https://avatars.githubusercontent.com/u/33684988?v=4?s=100" width="100px;" alt="xiaoguolong"/><br /><sub><b>xiaoguolong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaoguolong" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Clownsw"><img src="https://avatars.githubusercontent.com/u/28394742?v=4?s=100" width="100px;" alt="Smliexx"/><br /><sub><b>Smliexx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Clownsw" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AClownsw" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Calvin979"><img src="https://avatars.githubusercontent.com/u/131688897?v=4?s=100" width="100px;" alt="Naruse"/><br /><sub><b>Naruse</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Code">💻</a> <a href="#design-Calvin979" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACalvin979" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/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>
@@ -531,6 +531,26 @@ Thanks these wonderful people, welcome to join us:
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaomizhou2"><img src="https://avatars.githubusercontent.com/u/47807926?v=4?s=100" width="100px;" alt="zhangyaxi"/><br /><sub><b>zhangyaxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RainBondsongyg"><img src="https://avatars.githubusercontent.com/u/94501396?v=4?s=100" width="100px;" alt="songyg"/><br /><sub><b>songyg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=RainBondsongyg" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lx1229"><img src="https://avatars.githubusercontent.com/u/44620005?v=4?s=100" width="100px;" alt="Liuxin"/><br /><sub><b>Liuxin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lx1229" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yy549159265"><img src="https://avatars.githubusercontent.com/u/40821310?v=4?s=100" width="100px;" alt="yy549159265"/><br /><sub><b>yy549159265</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Tests">⚠️</a> <a href="#design-yy549159265" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cto-huhang"><img src="https://avatars.githubusercontent.com/u/53338629?v=4?s=100" width="100px;" alt="cto-huhang"/><br /><sub><b>cto-huhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cto-huhang" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Saramanda9988"><img src="https://avatars.githubusercontent.com/u/176664901?v=4?s=100" width="100px;" alt="LunaRain_079"/><br /><sub><b>LunaRain_079</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/delei"><img src="https://avatars.githubusercontent.com/u/17263766?v=4?s=100" width="100px;" alt="DeleiGuo"/><br /><sub><b>DeleiGuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Adelei" title="Bug reports">🐛</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chingjustwe"><img src="https://avatars.githubusercontent.com/u/13643747?v=4?s=100" width="100px;" alt="Rocky, Chi"/><br /><sub><b>Rocky, Chi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=chingjustwe" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rowankid"><img src="https://avatars.githubusercontent.com/u/18652781?v=4?s=100" width="100px;" alt="Wenqi Luo"/><br /><sub><b>Wenqi Luo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3Arowankid" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tuzuy"><img src="https://avatars.githubusercontent.com/u/95274591?v=4?s=100" width="100px;" alt="tuzuy"/><br /><sub><b>tuzuy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tuzuy" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/carlpinto25"><img src="https://avatars.githubusercontent.com/u/117299909?v=4?s=100" width="100px;" alt="carl pinto"/><br /><sub><b>carl pinto</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=carlpinto25" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://cxhello.top/"><img src="https://avatars.githubusercontent.com/u/49056040?v=4?s=100" width="100px;" alt="cxhello"/><br /><sub><b>cxhello</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cxhello" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jl15988"><img src="https://avatars.githubusercontent.com/u/70638770?v=4?s=100" width="100px;" alt="会功夫的李白"/><br /><sub><b>会功夫的李白</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jl15988" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://blog.aytop.cloud/"><img src="https://avatars.githubusercontent.com/u/37127008?v=4?s=100" width="100px;" alt="Albert.Yang"/><br /><sub><b>Albert.Yang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=AlbertYang0801" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://blog.tokenlen.top/"><img src="https://avatars.githubusercontent.com/u/150590575?v=4?s=100" width="100px;" alt="zhou yong kang"/><br /><sub><b>zhou yong kang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mengnankkkk" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/warrobe"><img src="https://avatars.githubusercontent.com/u/89446159?v=4?s=100" width="100px;" alt="warrobe"/><br /><sub><b>warrobe</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=warrobe" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Jetiaime"><img src="https://avatars.githubusercontent.com/u/93769000?v=4?s=100" width="100px;" alt="TeAmo"/><br /><sub><b>TeAmo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Jetiaime" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pentium100"><img src="https://avatars.githubusercontent.com/u/27917?v=4?s=100" width="100px;" alt="pentium100"/><br /><sub><b>pentium100</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pentium100" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dedyks"><img src="https://avatars.githubusercontent.com/u/23741665?v=4?s=100" width="100px;" alt="Dedy Kurniawan Santoso"/><br /><sub><b>Dedy Kurniawan Santoso</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dedyks" title="Code">💻</a></td>
</tr>
</tbody>
</table>
@@ -1,81 +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.agent.adapters;
import org.apache.hertzbeat.ai.agent.pojo.dto.Hierarchy;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.springframework.data.domain.Page;
import java.util.List;
/**
* Interface that provides access to alert definition information by retrieving data
* through the underlying alert define service.
*/
public interface AlertDefineServiceAdapter {
/**
* Add a new alert rule definition
* @param alertDefine Alert definition to add
* @return Created alert definition with ID
*/
AlertDefine addAlertDefine(AlertDefine alertDefine);
/**
* Get alert definitions with filtering and pagination
* @param search Search term
* @param app Monitor type filter
* @param enabled Enabled status filter
* @param sort Sort field
* @param order Sort order
* @param pageIndex Page index
* @param pageSize Page size
* @return Page of alert definitions
*/
Page<AlertDefine> getAlertDefines(String search, String app, Boolean enabled, String sort, String order, int pageIndex, int pageSize);
/**
* Get alert definition by ID
* @param id Alert definition ID
* @return Alert definition if found
*/
AlertDefine getAlertDefine(Long id);
/**
* Enable or disable alert definition
* @param id Alert definition ID
* @param enabled Whether to enable
*/
void toggleAlertDefineStatus(Long id, boolean enabled);
/**
* Modify/update an existing alert definition
* @param alertDefine Alert definition to update
* @return Updated alert definition
*/
AlertDefine modifyAlertDefine(AlertDefine alertDefine);
/**
* Get specific app hierarchy structure
* @param app App type
* @param lang Language for localization
* @return List of hierarchy objects for specific app
*/
List<Hierarchy> getAppHierarchy(String app, String lang);
}
@@ -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.agent.adapters;
import org.apache.hertzbeat.alert.dto.AlertSummary;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.springframework.data.domain.Page;
/**
* Interface that provides access to alert information by retrieving alert data
* through the underlying alert service.
*/
public interface AlertServiceAdapter {
/**
* Get single alerts with filtering and pagination
* @param status Alert status
* @param search Search term
* @param sort Sort field
* @param order Sort order
* @param pageIndex Page index
* @param pageSize Page size
* @return Page of single alerts
*/
Page<SingleAlert> getSingleAlerts(String status, String search, String sort, String order, int pageIndex, int pageSize);
/**
* Get group alerts with filtering and pagination
* @param status Alert status
* @param search Search term
* @param sort Sort field
* @param order Sort order
* @param pageIndex Page index
* @param pageSize Page size
* @return Page of group alerts
*/
Page<GroupAlert> getGroupAlerts(String status, String search, String sort, String order, int pageIndex, int pageSize);
/**
* Get alerts summary statistics
* @return Alert summary information
*/
AlertSummary getAlertsSummary();
}
@@ -1,55 +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.agent.adapters;
import org.apache.hertzbeat.common.entity.dto.MetricsData;
import org.apache.hertzbeat.common.entity.dto.MetricsHistoryData;
/**
* Interface that provides access to metrics information by retrieving metrics data
* through the underlying metrics service.
*/
public interface MetricsServiceAdapter {
/**
* Check warehouse storage server status
* @return true if warehouse is available, false otherwise
*/
Boolean getWarehouseStorageServerStatus();
/**
* Query real-time metrics data
* @param monitorId Monitor ID
* @param metrics Metrics name
* @return Real-time metrics data
*/
MetricsData getMetricsData(Long monitorId, String metrics);
/**
* Query historical metrics data
* @param monitorId Monitor ID
* @param app Monitor type
* @param metrics Metrics name
* @param metric Metric field name
* @param label Label filter
* @param history Query historical time period
* @param interval Whether to aggregate data
* @return Historical metrics data
*/
MetricsHistoryData getMetricHistoryData(Long monitorId, String app, String metrics, String metric, String label, String history, Boolean interval);
}
@@ -1,71 +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.agent.adapters;
import org.springframework.data.domain.Page;
import org.apache.hertzbeat.common.entity.manager.Monitor;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import java.util.List;
import java.util.Map;
/**
* Interface that provides access to monitor information by retrieving monitor data
* through the underlying monitor service.
*/
public interface MonitorServiceAdapter {
Page<Monitor> getMonitors(
List<Long> ids,
String app,
String search,
Byte status,
String sort,
String order,
Integer pageIndex,
Integer pageSize,
String labels
);
/**
* Add a new monitor
*
* @param monitor The monitor entity to create
* @param params List of parameters for the monitor
* @param collector Optional collector assignment
* @return The created monitor ID
*/
Long addMonitor(Monitor monitor, List<Param> params, String collector);
/**
* Get all available monitor types with their display names
*
* @param language Language code (e.g., "en-US", "zh-CN")
* @return Map of monitor type key to display name
*/
Map<String, String> getAvailableMonitorTypes(String language);
/**
* Get parameter definitions for a specific monitor type
*
* @param app Monitor type/application name (e.g., "linux", "mysql", "redis")
* @return List of parameter definitions for the monitor type
*/
List<ParamDefine> getMonitorParamDefines(String app);
}
@@ -1,257 +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.agent.adapters.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.adapters.AlertDefineServiceAdapter;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.apache.hertzbeat.ai.agent.pojo.dto.Hierarchy;
import org.apache.hertzbeat.ai.agent.utils.UtilityClass;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.List;
/**
* Implementation of the AlertDefineServiceAdapter interface that provides access to alert definition information
* through reflection by invoking the underlying alert define service implementation.
*/
@Slf4j
@Component
public class AlertDefineServiceAdapterImpl implements AlertDefineServiceAdapter {
@Override
public AlertDefine addAlertDefine(AlertDefine alertDefine) {
try {
Object alertDefineService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for addAlertDefine: {}", subjectSum);
try {
alertDefineService = SpringContextHolder.getBean("alertDefineServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertDefineServiceImpl'");
}
assert alertDefineService != null;
log.debug("AlertDefineService bean found: {}", alertDefineService.getClass().getSimpleName());
Method method = alertDefineService.getClass().getMethod("addAlertDefine", AlertDefine.class);
method.invoke(alertDefineService, alertDefine);
log.debug("Successfully added alert define with ID: {}", alertDefine.getId());
return alertDefine;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: addAlertDefine", e);
} catch (Exception e) {
log.error("Failed to invoke addAlertDefine via adapter", e);
throw new RuntimeException("Failed to invoke addAlertDefine via adapter: " + e.getMessage(), e);
}
}
@Override
public Page<AlertDefine> getAlertDefines(String search, String app, Boolean enabled, String sort, String order, int pageIndex, int pageSize) {
try {
Object alertDefineService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAlertDefines: {}", subjectSum);
try {
alertDefineService = SpringContextHolder.getBean("alertDefineServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertDefineServiceImpl'");
}
assert alertDefineService != null;
log.debug("AlertDefineService bean found: {}", alertDefineService.getClass().getSimpleName());
Method method = alertDefineService.getClass().getMethod(
"getAlertDefines",
List.class, String.class, String.class, String.class, int.class, int.class);
@SuppressWarnings("unchecked")
Page<AlertDefine> result = (Page<AlertDefine>) method.invoke(
alertDefineService, null, search, sort, order, pageIndex, pageSize);
log.debug("Successfully retrieved {} alert defines", result.getContent().size());
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getAlertDefines", e);
} catch (Exception e) {
log.error("Failed to invoke getAlertDefines via adapter", e);
throw new RuntimeException("Failed to invoke getAlertDefines via adapter: " + e.getMessage(), e);
}
}
@Override
public AlertDefine getAlertDefine(Long id) {
try {
Object alertDefineService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAlertDefine: {}", subjectSum);
try {
alertDefineService = SpringContextHolder.getBean("alertDefineServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertDefineServiceImpl'");
}
assert alertDefineService != null;
log.debug("AlertDefineService bean found: {}", alertDefineService.getClass().getSimpleName());
Method method = alertDefineService.getClass().getMethod("getAlertDefine", long.class);
AlertDefine result = (AlertDefine) method.invoke(alertDefineService, id);
log.debug("Successfully retrieved alert define with ID: {}", id);
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getAlertDefine", e);
} catch (Exception e) {
log.error("Failed to invoke getAlertDefine via adapter for ID: {}", id, e);
throw new RuntimeException("Failed to invoke getAlertDefine via adapter: " + e.getMessage(), e);
}
}
@Override
public void toggleAlertDefineStatus(Long id, boolean enabled) {
try {
Object alertDefineService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for toggleAlertDefineStatus: {}", subjectSum);
try {
alertDefineService = SpringContextHolder.getBean("alertDefineServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertDefineServiceImpl'");
}
assert alertDefineService != null;
log.debug("AlertDefineService bean found: {}", alertDefineService.getClass().getSimpleName());
// First get the existing AlertDefine
Method getMethod = alertDefineService.getClass().getMethod("getAlertDefine", long.class);
AlertDefine alertDefine = (AlertDefine) getMethod.invoke(alertDefineService, id);
if (alertDefine == null) {
throw new RuntimeException("AlertDefine with ID " + id + " not found");
}
// Update the enable status
alertDefine.setEnable(enabled);
// Use modifyAlertDefine to save the changes
Method modifyMethod = alertDefineService.getClass().getMethod("modifyAlertDefine", AlertDefine.class);
modifyMethod.invoke(alertDefineService, alertDefine);
log.debug("Successfully toggled alert define status for ID: {} to enabled: {}", id, enabled);
} catch (Exception e) {
log.error("Failed to invoke toggleAlertDefineStatus via adapter for ID: {}", id, e);
throw new RuntimeException("Failed to invoke toggleAlertDefineStatus via adapter: " + e.getMessage(), e);
}
}
@Override
public AlertDefine modifyAlertDefine(AlertDefine alertDefine) {
try {
Object alertDefineService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for modifyAlertDefine: {}", subjectSum);
try {
alertDefineService = SpringContextHolder.getBean("alertDefineServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertDefineServiceImpl'");
}
assert alertDefineService != null;
log.debug("AlertDefineService bean found: {}", alertDefineService.getClass().getSimpleName());
Method method = alertDefineService.getClass().getMethod("modifyAlertDefine", AlertDefine.class);
method.invoke(alertDefineService, alertDefine);
log.debug("Successfully modified alert define with ID: {}", alertDefine.getId());
return alertDefine;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: modifyAlertDefine", e);
} catch (Exception e) {
log.error("Failed to invoke modifyAlertDefine via adapter", e);
throw new RuntimeException("Failed to invoke modifyAlertDefine via adapter: " + e.getMessage(), e);
}
}
/**
* Retrieves the application hierarchy for a given app and language.
* Uses reflection to call the underlying app service method.
*
* @param app The application name
* @param lang The language code (optional, defaults to "en-US")
* @return List of Hierarchy objects representing the app hierarchy
*/
@Override
public List<Hierarchy> getAppHierarchy(String app, String lang) {
try {
Object appService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAppHierarchy: {}", subjectSum);
try {
appService = SpringContextHolder.getBean("appServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'appServiceImpl', trying by class name");
}
assert appService != null;
log.debug("AppService bean found for getAppHierarchy: {}", appService.getClass().getSimpleName());
// Provide default language if not specified
if (lang == null || lang.trim().isEmpty()) {
lang = "en-US";
}
// Call getAppHierarchy method: getAppHierarchy(String app, String lang)
Method method = appService.getClass().getMethod("getAppHierarchy", String.class, String.class);
List<?> managerHierarchies = (List<?>) method.invoke(appService, app, lang);
// Convert manager DTOs to ai-agent DTOs
List<Hierarchy> result = UtilityClass.convertToAgentHierarchies(managerHierarchies);
log.debug("Successfully retrieved and converted {} hierarchies for app '{}'", result.size(), app);
return result;
} catch (Exception e) {
log.error("Failed to get app hierarchy for app '{}': {}", app, e.getMessage(), e);
throw new RuntimeException("Failed to get app hierarchy for " + app, e);
}
}
}
@@ -1,141 +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.agent.adapters.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.adapters.AlertServiceAdapter;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.apache.hertzbeat.alert.dto.AlertSummary;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* Implementation of the AlertServiceAdapter interface that provides access to alert information
* through reflection by invoking the underlying alert service implementation.
*/
@Slf4j
@Component
public class AlertServiceAdapterImpl implements AlertServiceAdapter {
@Override
public Page<SingleAlert> getSingleAlerts(String status, String search, String sort, String order, int pageIndex, int pageSize) {
try {
Object alertService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getSingleAlerts: {}", subjectSum);
try {
alertService = SpringContextHolder.getBean("alertServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertServiceImpl'");
}
assert alertService != null;
log.debug("AlertService bean found: {}", alertService.getClass().getSimpleName());
Method method = alertService.getClass().getMethod(
"getSingleAlerts",
String.class, String.class, String.class, String.class, int.class, int.class);
@SuppressWarnings("unchecked")
Page<SingleAlert> result = (Page<SingleAlert>) method.invoke(
alertService, status, search, sort, order, pageIndex, pageSize);
log.debug("Successfully retrieved {} single alerts", result.getContent().size());
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getSingleAlerts", e);
} catch (Exception e) {
log.error("Failed to invoke getSingleAlerts via adapter", e);
throw new RuntimeException("Failed to invoke getSingleAlerts via adapter: " + e.getMessage(), e);
}
}
@Override
public Page<GroupAlert> getGroupAlerts(String status, String search, String sort, String order, int pageIndex, int pageSize) {
try {
Object alertService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getGroupAlerts: {}", subjectSum);
try {
alertService = SpringContextHolder.getBean("alertServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertServiceImpl'");
}
assert alertService != null;
log.debug("AlertService bean found: {}", alertService.getClass().getSimpleName());
Method method = alertService.getClass().getMethod(
"getGroupAlerts",
String.class, String.class, String.class, String.class, int.class, int.class);
@SuppressWarnings("unchecked")
Page<GroupAlert> result = (Page<GroupAlert>) method.invoke(
alertService, status, search, sort, order, pageIndex, pageSize);
log.debug("Successfully retrieved {} group alerts", result.getContent().size());
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getGroupAlerts", e);
} catch (Exception e) {
log.error("Failed to invoke getGroupAlerts via adapter", e);
throw new RuntimeException("Failed to invoke getGroupAlerts via adapter: " + e.getMessage(), e);
}
}
@Override
public AlertSummary getAlertsSummary() {
try {
Object alertService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAlertsSummary: {}", subjectSum);
try {
alertService = SpringContextHolder.getBean("alertServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertServiceImpl'");
}
assert alertService != null;
log.debug("AlertService bean found: {}", alertService.getClass().getSimpleName());
Method method = alertService.getClass().getMethod("getAlertsSummary");
AlertSummary result = (AlertSummary) method.invoke(alertService);
log.debug("Successfully retrieved alerts summary");
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getAlertsSummary", e);
} catch (Exception e) {
log.error("Failed to invoke getAlertsSummary via adapter", e);
throw new RuntimeException("Failed to invoke getAlertsSummary via adapter: " + e.getMessage(), e);
}
}
}
@@ -1,136 +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.agent.adapters.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.adapters.MetricsServiceAdapter;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.apache.hertzbeat.common.entity.dto.MetricsData;
import org.apache.hertzbeat.common.entity.dto.MetricsHistoryData;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* Implementation of the MetricsServiceAdapter interface that provides access to metrics information
* through reflection by invoking the underlying metrics service implementation.
*/
@Slf4j
@Component
public class MetricsServiceAdapterImpl implements MetricsServiceAdapter {
@Override
public Boolean getWarehouseStorageServerStatus() {
try {
Object metricsDataService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getWarehouseStorageServerStatus: {}", subjectSum);
try {
metricsDataService = SpringContextHolder.getBean("metricsDataServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'metricsDataServiceImpl'");
}
assert metricsDataService != null;
log.debug("MetricsDataService bean found: {}", metricsDataService.getClass().getSimpleName());
Method method = metricsDataService.getClass().getMethod("getWarehouseStorageServerStatus");
Boolean result = (Boolean) method.invoke(metricsDataService);
log.debug("Successfully retrieved warehouse storage server status: {}", result);
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getWarehouseStorageServerStatus", e);
} catch (Exception e) {
log.error("Failed to invoke getWarehouseStorageServerStatus via adapter", e);
throw new RuntimeException("Failed to invoke getWarehouseStorageServerStatus via adapter: " + e.getMessage(), e);
}
}
@Override
public MetricsData getMetricsData(Long monitorId, String metrics) {
try {
Object metricsDataService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getMetricsData: {}", subjectSum);
try {
metricsDataService = SpringContextHolder.getBean("metricsDataServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'metricsDataServiceImpl'");
}
assert metricsDataService != null;
log.debug("MetricsDataService bean found: {}", metricsDataService.getClass().getSimpleName());
Method method = metricsDataService.getClass().getMethod(
"getMetricsData",
Long.class, String.class);
MetricsData result = (MetricsData) method.invoke(metricsDataService, monitorId, metrics);
log.debug("Successfully retrieved metrics data for monitor {} and metrics {}", monitorId, metrics);
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getMetricsData", e);
} catch (Exception e) {
log.error("Failed to invoke getMetricsData via adapter for monitor {} and metrics {}", monitorId, metrics, e);
throw new RuntimeException("Failed to invoke getMetricsData via adapter: " + e.getMessage(), e);
}
}
@Override
public MetricsHistoryData getMetricHistoryData(Long monitorId, String app, String metrics, String metric, String label, String history, Boolean interval) {
try {
Object metricsDataService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getMetricHistoryData: {}", subjectSum);
try {
metricsDataService = SpringContextHolder.getBean("metricsDataServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'metricsDataServiceImpl'");
}
assert metricsDataService != null;
log.debug("MetricsDataService bean found: {}", metricsDataService.getClass().getSimpleName());
Method method = metricsDataService.getClass().getMethod(
"getMetricHistoryData",
Long.class, String.class, String.class, String.class, String.class, String.class, Boolean.class);
MetricsHistoryData result = (MetricsHistoryData) method.invoke(
metricsDataService, monitorId, app, metrics, metric, label, history, interval);
log.debug("Successfully retrieved historical metrics data for monitor {} and metrics {}", monitorId, metrics);
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getMetricHistoryData", e);
} catch (Exception e) {
log.error("Failed to invoke getMetricHistoryData via adapter for monitor {} and metrics {}", monitorId, metrics, e);
throw new RuntimeException("Failed to invoke getMetricHistoryData via adapter: " + e.getMessage(), e);
}
}
}
@@ -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.agent.adapters.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.adapters.MonitorServiceAdapter;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.springframework.data.domain.Page;
import org.apache.hertzbeat.common.entity.manager.Monitor;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
/**
* Implementation of the MonitorServiceAdapter interface that provides access to monitor information
* through reflection by invoking the underlying monitor service implementation.
*/
@Slf4j
@Component
public class MonitorServiceAdapterImpl implements MonitorServiceAdapter {
@Override
public Page<Monitor> getMonitors(
List<Long> ids,
String app,
String search,
Byte status,
String sort,
String order,
Integer pageIndex,
Integer pageSize,
String labels) {
try {
// Provide default values for all nullable parameters
if (sort == null || sort.trim().isEmpty()) {
sort = "gmtCreate";
}
if (order == null || order.trim().isEmpty()) {
order = "desc";
}
if (pageIndex == null) {
pageIndex = 0;
}
if (pageSize == null) {
pageSize = 8;
}
Object monitorService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject: {}", subjectSum);
try {
monitorService = SpringContextHolder.getBean("monitorServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'monitorServiceImpl', trying by class name");
}
assert monitorService != null;
log.debug("MonitorService bean found: {}", monitorService.getClass().getSimpleName());
Method method = monitorService.getClass().getMethod(
"getMonitors",
List.class, String.class, String.class, Byte.class,
String.class, String.class, int.class, int.class, String.class);
@SuppressWarnings("unchecked")
Page<Monitor> result = (Page<Monitor>) method.invoke(
monitorService,
ids, app, search, status, sort, order, pageIndex, pageSize, labels);
log.debug("MonitorServiceAdapter.getMonitors result: {}", result.getContent());
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getMonitors", e);
} catch (Exception e) {
log.debug("Failed to invoke getMonitors via adapter", e);
throw new RuntimeException("Failed to invoke getMonitors via adapter", e);
}
}
@Override
public Long addMonitor(Monitor monitor, List<Param> params, String collector) {
try {
Object monitorService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for addMonitor: {}", subjectSum);
try {
monitorService = SpringContextHolder.getBean("monitorServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'monitorServiceImpl', trying by class name");
}
assert monitorService != null;
log.debug("MonitorService bean found for addMonitor: {}", monitorService.getClass().getSimpleName());
// Call addMonitor method: addMonitor(Monitor monitor, List<Param> params, String collector, GrafanaDashboard dashboard)
Method method = monitorService.getClass().getMethod(
"addMonitor",
Monitor.class, List.class, String.class,
Class.forName("org.apache.hertzbeat.common.entity.grafana.GrafanaDashboard"));
// Call the method with null dashboard
method.invoke(monitorService, monitor, params, collector, null);
log.debug("Successfully added monitor: {} with ID: {}", monitor.getName(), monitor.getId());
return monitor.getId();
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: addMonitor", e);
} catch (Exception e) {
log.error("Failed to invoke addMonitor via adapter", e);
throw new RuntimeException("Failed to invoke addMonitor via adapter: " + e.getMessage(), e);
}
}
@Override
public Map<String, String> getAvailableMonitorTypes(String language) {
try {
Object appService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAvailableMonitorTypes: {}", subjectSum);
try {
appService = SpringContextHolder.getBean("appServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'appServiceImpl', trying by class name");
}
assert appService != null;
log.debug("AppService bean found for getAvailableMonitorTypes: {}", appService.getClass().getSimpleName());
// Provide default language if not specified
if (language == null || language.trim().isEmpty()) {
language = "en-US";
}
// Call getI18nApps method: getI18nApps(String lang)
Method method = appService.getClass().getMethod("getI18nApps", String.class);
@SuppressWarnings("unchecked")
Map<String, String> result = (Map<String, String>) method.invoke(appService, language);
log.debug("Successfully retrieved {} monitor types", result.size());
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getI18nApps", e);
} catch (Exception e) {
log.error("Failed to invoke getI18nApps via adapter", e);
throw new RuntimeException("Failed to invoke getI18nApps via adapter: " + e.getMessage(), e);
}
}
@Override
public List<ParamDefine> getMonitorParamDefines(String app) {
try {
Object appService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getMonitorParamDefines: {}", subjectSum);
try {
appService = SpringContextHolder.getBean("appServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'appServiceImpl', trying by class name");
}
assert appService != null;
log.debug("AppService bean found for getMonitorParamDefines: {}", appService.getClass().getSimpleName());
// Validate app parameter
if (app == null || app.trim().isEmpty()) {
throw new IllegalArgumentException("Monitor type/app parameter is required");
}
// Call getAppParamDefines method: getAppParamDefines(String app)
Method method = appService.getClass().getMethod("getAppParamDefines", String.class);
@SuppressWarnings("unchecked")
List<ParamDefine> result = (List<ParamDefine>) method.invoke(appService, app.toLowerCase().trim());
log.debug("Successfully retrieved {} parameter definitions for monitor type: {}", result.size(), app);
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getAppParamDefines", e);
} catch (Exception e) {
log.error("Failed to invoke getAppParamDefines via adapter for app: {}", app, e);
throw new RuntimeException("Failed to invoke getAppParamDefines via adapter: " + e.getMessage(), e);
}
}
}
@@ -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.agent.config;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.pojo.dto.OpenAiConfigDto;
import org.apache.hertzbeat.ai.agent.service.OpenAiConfigService;
import org.springframework.ai.model.ApiKey;
import org.springframework.stereotype.Component;
/**
* Dynamic OpenAI API Key implementation that retrieves the API key
* from our configuration service (database first, then YAML fallback)
*/
@Slf4j
@Component
public class DynamicOpenAiApiKey implements ApiKey {
private final OpenAiConfigService openAiConfigService;
public DynamicOpenAiApiKey(OpenAiConfigService openAiConfigService) {
this.openAiConfigService = openAiConfigService;
}
@Override
public String getValue() {
try {
OpenAiConfigDto effectiveConfig = openAiConfigService.getEffectiveConfig();
if (effectiveConfig != null && effectiveConfig.isEnable() && effectiveConfig.getApiKey() != null) {
log.debug("Retrieved OpenAI API key from configuration service");
return effectiveConfig.getApiKey();
} else {
log.warn("No valid OpenAI API key found in configuration");
return null;
}
} catch (Exception e) {
log.error("Error retrieving OpenAI API key from configuration", e);
return null;
}
}
}
@@ -1,76 +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.agent.config;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Configuration class for Large Language Model (LLM) settings.
*/
@Configuration
public class LlmConfig {
@Value("${spring.ai.openai.chat.options.model}")
private String model;
/**
* Create OpenAI API instance with dynamic API key
*/
@Bean
public OpenAiApi openAiApi(DynamicOpenAiApiKey dynamicApiKey) {
return OpenAiApi.builder()
.apiKey(dynamicApiKey)
.build();
}
/**
* Create OpenAI Chat Options with custom settings
*/
@Bean
public OpenAiChatOptions openAiChatOptions() {
return OpenAiChatOptions.builder()
.model(model)
.temperature(0.3)
.build();
}
/**
* Create OpenAI Chat Model with custom API configuration
*/
@Bean
public OpenAiChatModel openAiChatModel(OpenAiApi openAiApi, OpenAiChatOptions openAiChatOptions) {
return OpenAiChatModel.builder()
.openAiApi(openAiApi)
.defaultOptions(openAiChatOptions)
.build();
}
@Bean
public ChatClient openAiChatClient(OpenAiChatModel openAiChatModel) {
return ChatClient.create(openAiChatModel);
}
}
@@ -1,160 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.pojo.dto.OpenAiConfigDto;
import org.apache.hertzbeat.ai.agent.service.OpenAiConfigService;
import org.springframework.http.ResponseEntity;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
/**
* OpenAI Configuration API
*/
@RestController
@RequestMapping(value = "/api/ai-agent/config", produces = {APPLICATION_JSON_VALUE})
@Tag(name = "OpenAI Configuration API")
@Slf4j
public class OpenAiConfigController {
private final OpenAiConfigService openAiConfigService;
public OpenAiConfigController(OpenAiConfigService openAiConfigService) {
this.openAiConfigService = openAiConfigService;
}
@PostMapping("/openai")
@Operation(summary = "Save OpenAI configuration", description = "Save or update OpenAI configuration")
public ResponseEntity<Map<String, Object>> saveOpenAiConfig(@Valid @RequestBody OpenAiConfigDto config) {
try {
Map<String, Object> response = new HashMap<>();
// Validate API key if enabled
if (config.isEnable() && config.getApiKey() != null && !config.getApiKey().trim().isEmpty()) {
OpenAiConfigService.ValidationResult validationResult = openAiConfigService.validateApiKey(config.getApiKey());
if (!validationResult.isValid()) {
log.warn("API key validation failed during save: {}", validationResult.getMessage());
response.put("code", 1);
response.put("msg", "API key validation failed: " + validationResult.getMessage());
return ResponseEntity.ok(response);
}
log.info("API key validation successful during save");
}
// Save the configuration
openAiConfigService.saveConfig(config);
response.put("code", 0);
response.put("msg", "OpenAI configuration saved successfully");
return ResponseEntity.ok(response);
} catch (Exception e) {
log.error("Failed to save OpenAI configuration", e);
Map<String, Object> response = new HashMap<>();
response.put("code", 1);
response.put("msg", "Failed to save configuration: " + e.getMessage());
return ResponseEntity.ok(response);
}
}
@GetMapping("/openai")
@Operation(summary = "Get OpenAI configuration", description = "Get current OpenAI configuration")
public ResponseEntity<Map<String, Object>> getOpenAiConfig() {
try {
OpenAiConfigDto config = openAiConfigService.getConfig();
Map<String, Object> response = new HashMap<>();
response.put("code", 0);
response.put("data", config);
response.put("msg", "Success");
return ResponseEntity.ok(response);
} catch (Exception e) {
log.error("Failed to get OpenAI configuration", e);
Map<String, Object> response = new HashMap<>();
response.put("code", 1);
response.put("msg", "Failed to get configuration: " + e.getMessage());
return ResponseEntity.ok(response);
}
}
@GetMapping("/openai/status")
@Operation(summary = "Check OpenAI configuration status", description = "Check if OpenAI is properly configured")
public ResponseEntity<Map<String, Object>> getOpenAiConfigStatus() {
try {
boolean configured = openAiConfigService.isConfigured();
OpenAiConfigDto effectiveConfig = openAiConfigService.getEffectiveConfig();
boolean hasDbConfig = openAiConfigService.getConfig() != null;
boolean hasYamlConfig = effectiveConfig != null && !hasDbConfig;
// Validate the effective configuration
boolean validationPassed = false;
String validationMessage = "No configuration found";
if (effectiveConfig != null && effectiveConfig.isEnable() && effectiveConfig.getApiKey() != null && !effectiveConfig.getApiKey().trim().isEmpty()) {
OpenAiConfigService.ValidationResult validationResult = openAiConfigService.validateApiKey(effectiveConfig.getApiKey());
validationPassed = validationResult.isValid();
validationMessage = validationResult.getMessage();
if (!validationPassed) {
log.warn("OpenAI API key validation failed during status check: {}", validationMessage);
}
}
Map<String, Object> response = new HashMap<>();
response.put("code", 0);
response.put("data", Map.of(
"configured", configured && validationPassed,
"hasDbConfig", hasDbConfig,
"hasYamlConfig", hasYamlConfig,
"validationPassed", validationPassed,
"validationMessage", validationMessage
));
response.put("msg", "Success");
return ResponseEntity.ok(response);
} catch (Exception e) {
log.error("Failed to get OpenAI configuration status", e);
Map<String, Object> response = new HashMap<>();
response.put("code", 1);
response.put("msg", "Failed to get status: " + e.getMessage());
return ResponseEntity.ok(response);
}
}
}
@@ -1,53 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.List;
/**
* Conversation DTO for AI chat conversations.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Conversation information")
public class ConversationDto {
@Schema(description = "Conversation ID", example = "conv-123")
private String conversationId;
@Schema(description = "Creation time")
private LocalDateTime createdAt;
@Schema(description = "Last updated time")
private LocalDateTime updatedAt;
@Schema(description = "Messages in this conversation")
private List<MessageDto> messages;
@Schema(description = "Message count")
private Integer messageCount;
}
@@ -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.agent.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE;
/**
* Hierarchical structure
* eg: Monitoring Type metrics Information Hierarchy Relationship
*/
@AllArgsConstructor
@NoArgsConstructor
@Data
@Schema(description = "Monitor Hierarchy")
public class Hierarchy {
/**
* Category value
*/
@Schema(description = "Category Value", example = "os", accessMode = READ_WRITE)
String category;
/**
* Attribute value
*/
@Schema(description = "Attribute value", example = "linux", accessMode = READ_WRITE)
String value;
/**
* Attribute internationalization tag
*/
@Schema(description = "Attribute internationalization tag", example = "Linux system", accessMode = READ_WRITE)
String label;
/**
* Is it a leaf node
*/
@Schema(description = "Is it a leaf node", example = "true", accessMode = READ_WRITE)
Boolean isLeaf = false;
/**
* Is hide this app type in main menus layout
*/
@Schema(description = "Is hide this app in main menus layout, only for app type, default true.", example = "true")
Boolean hide = true;
/**
* For leaf metric
* metric type 0-number: number 1-string: string
*/
@Schema(description = "metric type 0-number: number 1-string: string")
private Byte type;
/**
* metric unit
*/
@Schema(description = "metric unit")
private String unit;
/**
* Next level of association
*/
@Schema(description = "Next Hierarchy", accessMode = READ_WRITE)
private List<Hierarchy> children;
}
@@ -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.agent.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* Message DTO for chat messages.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Chat message")
public class MessageDto {
@Schema(description = "Message ID", example = "msg-123")
private String messageId;
@Schema(description = "Conversation ID", example = "conv-123")
private String conversationId;
@Schema(description = "Message content", example = "List all monitors")
private String content;
@Schema(description = "Message role", example = "user", allowableValues = {"user", "assistant"})
private String role;
@Schema(description = "Message timestamp")
private LocalDateTime timestamp;
}
@@ -1,100 +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.agent.service;
import org.apache.hertzbeat.ai.agent.event.OpenAiConfigChangeEvent;
import org.apache.hertzbeat.ai.agent.pojo.dto.OpenAiConfigDto;
/**
* OpenAI Configuration Service
* Consolidated service for OpenAI configuration, validation, and client factory management
*/
public interface OpenAiConfigService {
/**
* Save OpenAI configuration
* @param config OpenAI configuration
*/
void saveConfig(OpenAiConfigDto config);
/**
* Get OpenAI configuration
* @return OpenAI configuration
*/
OpenAiConfigDto getConfig();
/**
* Check if OpenAI is properly configured
* @return true if configured and enabled
*/
boolean isConfigured();
/**
* Get effective OpenAI configuration (DB first, then YAML fallback)
* @return effective configuration or null if not configured
*/
OpenAiConfigDto getEffectiveConfig();
/**
* Validate OpenAI API key by calling the OpenAI API
* @param apiKey the API key to validate
* @return validation result with success status and message
*/
ValidationResult validateApiKey(String apiKey);
/**
* Force reload of OpenAI configuration cache
* This method is typically called when configuration changes
*/
void reloadConfig();
/**
* Handle OpenAI configuration change events
* @param event OpenAI configuration change event
*/
void onOpenAiConfigChange(OpenAiConfigChangeEvent event);
/**
* Validation result class
*/
class ValidationResult {
private final boolean valid;
private final String message;
private ValidationResult(boolean valid, String message) {
this.valid = valid;
this.message = message;
}
public static ValidationResult success(String message) {
return new ValidationResult(true, message);
}
public static ValidationResult failure(String message) {
return new ValidationResult(false, message);
}
public boolean isValid() {
return valid;
}
public String getMessage() {
return message;
}
}
}
@@ -1,276 +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.agent.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatRequestContext;
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatResponseDto;
import org.apache.hertzbeat.ai.agent.pojo.dto.ConversationDto;
import org.apache.hertzbeat.ai.agent.pojo.dto.MessageDto;
import org.apache.hertzbeat.ai.agent.service.ChatClientProviderService;
import org.apache.hertzbeat.ai.agent.service.ConversationService;
import org.apache.hertzbeat.ai.agent.service.OpenAiConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* Implementation of the ConversationService interface for managing chat conversations.
*/
@Slf4j
@Service
public class ConversationServiceImpl implements ConversationService {
private final Map<String, Map<String, Object>> conversations = new ConcurrentHashMap<>();
private final Map<String, List<Map<String, Object>>> conversationMessages = new ConcurrentHashMap<>();
@Autowired
private ChatClientProviderService chatClientProviderService;
@Autowired
private OpenAiConfigService openAiConfigService;
@Override
public ConversationDto createConversation() {
String conversationId = createNewConversation();
return getConversation(conversationId);
}
@Override
public Flux<ServerSentEvent<ChatResponseDto>> streamChat(String message, String conversationId) {
// Validate conversation exists
if (!conversationExists(conversationId)) {
ChatResponseDto errorResponse = ChatResponseDto.builder()
.conversationId(conversationId)
.response("Error: Conversation not found: " + conversationId)
.build();
return Flux.just(ServerSentEvent.builder(errorResponse)
.event("error")
.build());
}
// Check if OpenAI is properly configured
if (!openAiConfigService.isConfigured()) {
ChatResponseDto errorResponse = ChatResponseDto.builder()
.conversationId(conversationId)
.response("OpenAI is not configured. Please configure your OpenAI API key in the settings or application.yml file.")
.build();
return Flux.just(ServerSentEvent.builder(errorResponse)
.event("error")
.build());
}
log.info("Starting streaming conversation: {}", conversationId);
// Add user message to conversation
String userMessageId = addMessageToConversation(conversationId, message, "user");
// Get conversation history for context
List<Map<String, Object>> messagesList = conversationMessages.get(conversationId);
List<MessageDto> conversationHistory = new ArrayList<>();
if (messagesList != null && messagesList.size() > 1) {
// Get all messages except the last one (which is the current user message we just added)
for (int i = 0; i < messagesList.size() - 1; i++) {
Map<String, Object> msgMap = messagesList.get(i);
conversationHistory.add(mapToMessageDto(msgMap));
}
}
ChatRequestContext context = ChatRequestContext.builder()
.message(message)
.conversationId(conversationId)
.conversationHistory(conversationHistory)
.build();
// Stream response from AI service
StringBuilder fullResponse = new StringBuilder();
return chatClientProviderService.streamChat(context)
.map(chunk -> {
fullResponse.append(chunk);
ChatResponseDto responseDto = ChatResponseDto.builder()
.conversationId(conversationId)
.response(chunk)
.userMessageId(userMessageId)
.build();
return ServerSentEvent.builder(responseDto)
.event("message")
.build();
})
.concatWith(Flux.defer(() -> {
// Add the complete AI response to conversation
String assistantMessageId = addMessageToConversation(conversationId, fullResponse.toString(), "assistant");
ChatResponseDto finalResponse = ChatResponseDto.builder()
.conversationId(conversationId)
.response("")
.userMessageId(userMessageId)
.assistantMessageId(assistantMessageId)
.build();
return Flux.just(ServerSentEvent.builder(finalResponse)
.event("complete")
.build());
}))
.doOnComplete(() -> log.info("Streaming completed for conversation: {}", conversationId))
.doOnError(error -> log.error("Error in streaming chat for conversation {}: {}", conversationId, error.getMessage(), error))
.onErrorResume(error -> {
ChatResponseDto errorResponse = ChatResponseDto.builder()
.conversationId(conversationId)
.response("An error occurred: " + error.getMessage())
.userMessageId(userMessageId)
.build();
return Flux.just(ServerSentEvent.builder(errorResponse)
.event("error")
.build());
});
}
@Override
public ConversationDto getConversation(String conversationId) {
if (conversationId == null || conversationId.isEmpty()) {
return null;
}
Map<String, Object> conversation = conversations.get(conversationId);
if (conversation == null) {
return null;
}
List<Map<String, Object>> messagesList = conversationMessages.get(conversationId);
List<MessageDto> messages = messagesList != null
? messagesList.stream().map(this::mapToMessageDto).collect(Collectors.toList()) :
new ArrayList<>();
return ConversationDto.builder()
.conversationId((String) conversation.get("conversationId"))
.createdAt((LocalDateTime) conversation.get("createdAt"))
.updatedAt((LocalDateTime) conversation.get("updatedAt"))
.messages(messages)
.build();
}
@Override
public List<ConversationDto> getAllConversations() {
List<ConversationDto> result = new ArrayList<>();
for (Map.Entry<String, Map<String, Object>> entry : conversations.entrySet()) {
Map<String, Object> conv = entry.getValue();
List<Map<String, Object>> messages = conversationMessages.get(entry.getKey());
ConversationDto dto = ConversationDto.builder()
.conversationId((String) conv.get("conversationId"))
.createdAt((LocalDateTime) conv.get("createdAt"))
.updatedAt((LocalDateTime) conv.get("updatedAt"))
.messages(new ArrayList<>()) // Don't include messages in list view for performance
.build();
result.add(dto);
}
result.sort((a, b) -> b.getUpdatedAt().compareTo(a.getUpdatedAt()));
return result;
}
@Override
public boolean deleteConversation(String conversationId) {
if (conversationId == null || conversationId.isEmpty()) {
return false;
}
boolean existed = conversations.containsKey(conversationId);
if (existed) {
conversations.remove(conversationId);
conversationMessages.remove(conversationId);
log.info("Deleted conversation: {}", conversationId);
}
return existed;
}
@Override
public boolean conversationExists(String conversationId) {
return conversationId != null && !conversationId.isEmpty() && conversations.containsKey(conversationId);
}
private String createNewConversation() {
String conversationId = "conv-" + UUID.randomUUID().toString().substring(0, 8);
LocalDateTime now = LocalDateTime.now();
Map<String, Object> conversation = new HashMap<>();
conversation.put("conversationId", conversationId);
conversation.put("createdAt", now);
conversation.put("updatedAt", now);
conversations.put(conversationId, conversation);
conversationMessages.put(conversationId, new ArrayList<>());
log.info("Created new conversation: {}", conversationId);
return conversationId;
}
private MessageDto mapToMessageDto(Map<String, Object> messageMap) {
return MessageDto.builder()
.messageId((String) messageMap.get("messageId"))
.conversationId((String) messageMap.get("conversationId"))
.content((String) messageMap.get("content"))
.role((String) messageMap.get("role"))
.timestamp((LocalDateTime) messageMap.get("timestamp"))
.build();
}
private String addMessageToConversation(String conversationId, String content, String role) {
List<Map<String, Object>> messages = conversationMessages.computeIfAbsent(conversationId, k -> new ArrayList<>());
String messageId = "msg-" + UUID.randomUUID().toString().substring(0, 8);
Map<String, Object> message = new HashMap<>();
message.put("messageId", messageId);
message.put("conversationId", conversationId);
message.put("content", content);
message.put("role", role);
message.put("timestamp", LocalDateTime.now());
messages.add(message);
// Update conversation timestamp
Map<String, Object> conversation = conversations.get(conversationId);
if (conversation != null) {
conversation.put("updatedAt", LocalDateTime.now());
// Auto-generate title from first user message
if ("user".equals(role) && messages.stream().filter(m -> "user".equals(m.get("role"))).count() == 1) {
String title = content.length() > 30 ? content.substring(0, 27) + "..." : content;
conversation.put("title", title);
}
}
return messageId;
}
}
@@ -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.agent.service.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.config.OpenAiYamlConfig;
import org.apache.hertzbeat.ai.agent.dao.OpenAiConfigDao;
import org.apache.hertzbeat.ai.agent.entity.OpenAiConfig;
import org.apache.hertzbeat.ai.agent.event.OpenAiConfigChangeEvent;
import org.apache.hertzbeat.ai.agent.pojo.dto.OpenAiConfigDto;
import org.apache.hertzbeat.ai.agent.service.OpenAiConfigService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.event.EventListener;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
/**
* OpenAI Configuration Service Implementation
* Consolidated service for OpenAI configuration, validation, and client factory management
*/
@Slf4j
@Service
public class OpenAiConfigServiceImpl implements OpenAiConfigService {
private static final String CONFIG_TYPE = "openai";
private static final String OPENAI_MODELS_ENDPOINT = "https://api.openai.com/v1/models";
private final OpenAiConfigDao openAiConfigDao;
private final ObjectMapper objectMapper;
private final ApplicationContext applicationContext;
private final OpenAiYamlConfig yamlConfig;
private final RestTemplate restTemplate;
// Client factory cache
private volatile OpenAiConfigDto currentConfig;
public OpenAiConfigServiceImpl(OpenAiConfigDao openAiConfigDao,
ObjectMapper objectMapper,
ApplicationContext applicationContext,
OpenAiYamlConfig yamlConfig) {
this.openAiConfigDao = openAiConfigDao;
this.objectMapper = objectMapper;
this.applicationContext = applicationContext;
this.yamlConfig = yamlConfig;
this.restTemplate = new RestTemplate();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void saveConfig(OpenAiConfigDto config) {
try {
String contentJson = objectMapper.writeValueAsString(config);
OpenAiConfig openAiConfig = OpenAiConfig.builder()
.type(CONFIG_TYPE)
.content(contentJson)
.build();
openAiConfigDao.save(openAiConfig);
log.info("OpenAI configuration saved successfully");
applicationContext.publishEvent(new OpenAiConfigChangeEvent(applicationContext));
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Failed to save OpenAI configuration: " + e.getMessage());
}
}
@Override
public OpenAiConfigDto getConfig() {
OpenAiConfig config = openAiConfigDao.findByType(CONFIG_TYPE);
if (config == null || !StringUtils.hasText(config.getContent())) {
return null;
}
try {
return objectMapper.readValue(config.getContent(), OpenAiConfigDto.class);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Failed to parse OpenAI configuration: " + e.getMessage());
}
}
@Override
public boolean isConfigured() {
OpenAiConfigDto effective = getEffectiveConfig();
return effective != null && effective.isEnable() && StringUtils.hasText(effective.getApiKey());
}
@Override
public OpenAiConfigDto getEffectiveConfig() {
OpenAiConfigDto dbConfig = getConfig();
if (dbConfig != null && dbConfig.isEnable() && StringUtils.hasText(dbConfig.getApiKey())) {
log.debug("Using database OpenAI configuration");
return dbConfig;
}
if (yamlConfig != null && yamlConfig.isEnable() && StringUtils.hasText(yamlConfig.getApiKey())) {
log.debug("Using YAML OpenAI configuration from spring.ai.openai.api-key");
OpenAiConfigDto yamlDto = new OpenAiConfigDto();
yamlDto.setEnable(true);
yamlDto.setApiKey(yamlConfig.getApiKey());
return yamlDto;
}
log.debug("No valid OpenAI configuration found");
return null;
}
@Override
public ValidationResult validateApiKey(String apiKey) {
if (!StringUtils.hasText(apiKey)) {
return ValidationResult.failure("API key cannot be empty");
}
if (!apiKey.startsWith("sk-")) {
return ValidationResult.failure("Invalid API key format. OpenAI API keys should start with 'sk-'");
}
try {
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + apiKey);
headers.set("Content-Type", "application/json");
HttpEntity<String> entity = new HttpEntity<>(headers);
log.debug("Validating OpenAI API key by calling models endpoint");
ResponseEntity<String> response = restTemplate.exchange(
OPENAI_MODELS_ENDPOINT,
HttpMethod.GET,
entity,
String.class
);
if (response.getStatusCode() == HttpStatus.OK) {
log.info("OpenAI API key validation successful");
return ValidationResult.success("API key is valid");
} else {
log.warn("OpenAI API key validation failed with status: {}", response.getStatusCode());
return ValidationResult.failure("API key validation failed: " + response.getStatusCode());
}
} catch (Exception e) {
log.error("Error validating OpenAI API key", e);
String errorMessage = e.getMessage();
// Parse common error messages
if (errorMessage.contains("401")) {
return ValidationResult.failure("Invalid API key - authentication failed");
} else if (errorMessage.contains("403")) {
return ValidationResult.failure("API key does not have permission to access models");
} else if (errorMessage.contains("429")) {
return ValidationResult.failure("Rate limit exceeded - please try again later");
} else if (errorMessage.contains("timeout") || errorMessage.contains("connect")) {
return ValidationResult.failure("Network error - unable to connect to OpenAI API");
} else {
return ValidationResult.failure("API key validation failed: " + errorMessage);
}
}
}
@Override
public void reloadConfig() {
synchronized (this) {
currentConfig = null; // Force reload
}
}
/**
* OpenAI configuration change event listener
*/
@EventListener(OpenAiConfigChangeEvent.class)
public void onOpenAiConfigChange(OpenAiConfigChangeEvent event) {
log.info("[OpenAiConfigService] OpenAI configuration change event received");
reloadConfig();
}
}
@@ -23,10 +23,10 @@
<artifactId>hertzbeat</artifactId>
<version>2.0-SNAPSHOT</version>
</parent>
<artifactId>hertzbeat-ai-agent</artifactId>
<artifactId>hertzbeat-ai</artifactId>
<version>${hertzbeat.version}</version>
<properties>
<spring-ai.version>1.0.1</spring-ai.version>
<spring-ai.version>1.0.3</spring-ai.version>
<java.version>17</java.version>
</properties>
@@ -51,16 +51,29 @@
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
<artifactId>spring-ai-openai</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-client-chat</artifactId>
</dependency>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-common</artifactId>
</dependency>
<!-- common -->
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-base</artifactId>
</dependency>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-alerter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-manager</artifactId>
</dependency>
<dependency>
<groupId>com.usthe.sureness</groupId>
<artifactId>spring-boot3-starter-sureness</artifactId>
@@ -90,4 +103,4 @@
</plugins>
</build>
</project>
</project>
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.config;
package org.apache.hertzbeat.ai.config;
import com.fasterxml.jackson.core.type.TypeReference;
@@ -0,0 +1,156 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.config;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.support.event.AiProviderConfigChangeEvent;
import org.apache.hertzbeat.common.entity.dto.ModelProviderConfig;
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
import org.apache.hertzbeat.common.entity.manager.GeneralConfig;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
/**
* Configuration class for Large Language Model (LLM) settings.
*/
@Configuration
@Slf4j
public class LlmConfig {
private final GeneralConfigDao generalConfigDao;
private ApplicationContext applicationContext;
public LlmConfig(GeneralConfigDao generalConfigDao, ApplicationContext applicationContext) {
this.generalConfigDao = generalConfigDao;
this.applicationContext = applicationContext;
}
/**
* Create ChatClient bean with all dependencies created internally
*/
@Bean
public ChatClient openAiChatClient() {
return createChatClient();
}
/**
* Create ChatClient with all necessary components
*/
private ChatClient createChatClient() {
GeneralConfig providerConfig = generalConfigDao.findByType("provider");
if (providerConfig == null || providerConfig.getContent() == null) {
log.warn("LLM Provider is not set, ChatClient bean will not be created");
return null;
}
ModelProviderConfig modelProviderConfig = JsonUtil.fromJson(providerConfig.getContent(), ModelProviderConfig.class);
if (modelProviderConfig == null || modelProviderConfig.getApiKey() == null) {
log.warn("LLM Provider configuration is incomplete, ChatClient bean will not be created");
return null;
}
if (modelProviderConfig.getBaseUrl() == null) {
if ("openai".equals(modelProviderConfig.getCode())) {
modelProviderConfig.setBaseUrl("https://api.openai.com/v1");
} else if ("zhipu".equals(modelProviderConfig.getCode())) {
modelProviderConfig.setBaseUrl("https://open.bigmodel.cn/api/paas/v4");
} else if ("zai".equals(modelProviderConfig.getCode())) {
modelProviderConfig.setBaseUrl("https://api.z.ai/api/paas/v4");
} else {
modelProviderConfig.setBaseUrl("https://api.openai.com/v1");
}
}
if (modelProviderConfig.getModel() == null) {
if ("openai".equals(modelProviderConfig.getCode())) {
modelProviderConfig.setModel("gpt-5");
} else if ("zhipu".equals(modelProviderConfig.getCode())) {
modelProviderConfig.setModel("glm-4.6");
} else if ("zai".equals(modelProviderConfig.getCode())) {
modelProviderConfig.setModel("glm-4.6");
} else {
modelProviderConfig.setModel("gpt-5");
}
}
OpenAiApi.Builder builder = new OpenAiApi.Builder();
builder.baseUrl(modelProviderConfig.getBaseUrl());
builder.apiKey(modelProviderConfig.getApiKey());
builder.completionsPath("/chat/completions");
// Create Chat Options
OpenAiChatOptions openAiChatOptions = OpenAiChatOptions.builder()
.model(modelProviderConfig.getModel())
.temperature(0.3)
.build();
// Create Chat Model
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
.openAiApi(builder.build())
.defaultOptions(openAiChatOptions)
.build();
// Create and return ChatClient
return ChatClient.create(openAiChatModel);
}
/**
* AI configuration change event listener
* Uses ApplicationContext to unregister and re-register the ChatClient bean
*/
@EventListener(AiProviderConfigChangeEvent.class)
public void onAiProviderConfigChange(AiProviderConfigChangeEvent event) {
log.info("Provider configuration change event received, refreshing ChatClient bean");
try {
ConfigurableApplicationContext configurableContext = (ConfigurableApplicationContext) applicationContext;
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) configurableContext.getBeanFactory();
// Remove the existing ChatClient bean
if (beanFactory.containsSingleton("openAiChatClient")) {
beanFactory.destroySingleton("openAiChatClient");
log.info("Existing ChatClient bean destroyed");
}
// Create new ChatClient with updated configuration
ChatClient newChatClient = createChatClient();
// Register the new ChatClient bean
beanFactory.registerSingleton("openAiChatClient", newChatClient);
log.info("ChatClient bean refreshed successfully with new AI provider configuration");
} catch (Exception e) {
log.error("Failed to refresh ChatClient bean after configuration change", e);
}
}
}
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.config;
package org.apache.hertzbeat.ai.config;
import com.usthe.sureness.subject.SubjectSum;
import org.springframework.core.NamedInheritableThreadLocal;
@@ -16,7 +16,7 @@
*/
package org.apache.hertzbeat.ai.agent.config;
package org.apache.hertzbeat.ai.config;
import org.springframework.stereotype.Component;
@@ -179,4 +179,4 @@ public class PromptProvider {
Keep responses focused on monitoring topics and HertzBeat's comprehensive capabilities.
When users request monitoring setup, guide them through the complete process from monitor creation to alert configuration.
""";
}
}
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.controller;
package org.apache.hertzbeat.ai.controller;
import com.usthe.sureness.subject.SubjectSum;
import com.usthe.sureness.util.SurenessContextHolder;
@@ -23,11 +23,11 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatRequestContext;
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatResponseDto;
import org.apache.hertzbeat.ai.agent.pojo.dto.ConversationDto;
import org.apache.hertzbeat.ai.agent.service.ConversationService;
import org.apache.hertzbeat.ai.config.McpContextHolder;
import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext;
import org.apache.hertzbeat.ai.pojo.dto.ChatResponseChunk;
import org.apache.hertzbeat.ai.service.ConversationService;
import org.apache.hertzbeat.common.entity.ai.ChatConversation;
import org.apache.hertzbeat.common.entity.dto.Message;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
@@ -63,23 +63,6 @@ public class ChatController {
this.conversationService = conversationService;
}
/**
* Create a new conversation
*
* @return Created conversation details
*/
@PostMapping(path = "/conversations")
@Operation(summary = "Create a new conversation", description = "Create a new conversation")
public ResponseEntity<Message<ConversationDto>> createConversation() {
try {
ConversationDto conversation = conversationService.createConversation();
return ResponseEntity.ok(Message.success(conversation));
} catch (Exception e) {
log.error("Error creating conversation: ", e);
return ResponseEntity.ok(Message.fail((byte) -1, "Failed to create conversation"));
}
}
/**
* Send a message and get a streaming response with conversation tracking
*
@@ -88,14 +71,13 @@ public class ChatController {
*/
@PostMapping(value = "/stream", produces = TEXT_EVENT_STREAM_VALUE)
@Operation(summary = "Send a chat message with streaming response", description = "Send a message to AI and get a streaming response with conversation tracking")
public Flux<ServerSentEvent<ChatResponseDto>> streamChat(@Valid @RequestBody ChatRequestContext context) {
public Flux<ServerSentEvent<ChatResponseChunk>> streamChat(@Valid @RequestBody ChatRequestContext context) {
try {
// Validate message is not empty
SubjectSum subject = SurenessContextHolder.getBindSubject();
log.info(subject.toString());
McpContextHolder.setSubject(subject);
if (context.getMessage() == null || context.getMessage().trim().isEmpty()) {
ChatResponseDto errorResponse = ChatResponseDto.builder()
ChatResponseChunk errorResponse = ChatResponseChunk.builder()
.conversationId(context.getConversationId())
.response("Error: Message cannot be empty")
.build();
@@ -109,7 +91,7 @@ public class ChatController {
} catch (Exception e) {
log.error("Error in stream chat endpoint: ", e);
ChatResponseDto errorResponse = ChatResponseDto.builder()
ChatResponseChunk errorResponse = ChatResponseChunk.builder()
.conversationId(context.getConversationId())
.response("An error occurred: " + e.getMessage())
.build();
@@ -119,6 +101,18 @@ public class ChatController {
}
}
/**
* Create a new conversation
*
* @return Created conversation details
*/
@PostMapping(path = "/conversations")
@Operation(summary = "Create a new conversation", description = "Create a new conversation")
public ResponseEntity<Message<ChatConversation>> createConversation() {
ChatConversation conversation = conversationService.createConversation();
return ResponseEntity.ok(Message.success(conversation));
}
/**
* Get all conversations
*
@@ -126,14 +120,9 @@ public class ChatController {
*/
@GetMapping(path = "/conversations")
@Operation(summary = "List all conversations", description = "Get a list of all conversations")
public ResponseEntity<Message<List<ConversationDto>>> listConversations() {
try {
List<ConversationDto> conversations = conversationService.getAllConversations();
return ResponseEntity.ok(Message.success(conversations));
} catch (Exception e) {
log.error("Error listing conversations: ", e);
return ResponseEntity.ok(Message.fail((byte) -1, "Failed to retrieve conversations"));
}
public ResponseEntity<Message<List<ChatConversation>>> listConversations() {
List<ChatConversation> conversations = conversationService.getAllConversations();
return ResponseEntity.ok(Message.success(conversations));
}
/**
@@ -144,26 +133,10 @@ public class ChatController {
*/
@GetMapping(path = "/conversations/{conversationId}")
@Operation(summary = "Get conversation history", description = "Get detailed information and message history for a specific conversation")
public ResponseEntity<Message<ConversationDto>> getConversation(
@Parameter(description = "Conversation ID", example = "conv-12345678") @PathVariable("conversationId") String conversationId) {
try {
// Validate conversation ID
if (conversationId == null || conversationId.trim().isEmpty()) {
return ResponseEntity.ok(Message.fail((byte) -1, "Conversation ID is required"));
}
ConversationDto conversation = conversationService.getConversation(conversationId);
if (conversation == null) {
return ResponseEntity.ok(Message.fail((byte) -1, "Conversation not found: " + conversationId));
}
return ResponseEntity.ok(Message.success(conversation));
} catch (Exception e) {
log.error("Error getting conversation: ", e);
return ResponseEntity.ok(Message.fail((byte) -1, "Failed to retrieve conversation"));
}
public ResponseEntity<Message<ChatConversation>> getConversation(
@Parameter(description = "Conversation ID", example = "12345678") @PathVariable(value = "conversationId") Long conversationId) {
ChatConversation conversation = conversationService.getConversation(conversationId);
return ResponseEntity.ok(Message.success(conversation));
}
/**
@@ -175,23 +148,8 @@ public class ChatController {
@DeleteMapping(path = "/conversations/{conversationId}")
@Operation(summary = "Delete conversation", description = "Delete a specific conversation and all its messages")
public ResponseEntity<Message<Void>> deleteConversation(
@Parameter(description = "Conversation ID", example = "conv-12345678") @PathVariable("conversationId") String conversationId) {
try {
// Validate conversation ID
if (conversationId == null || conversationId.trim().isEmpty()) {
return ResponseEntity.ok(Message.fail((byte) -1, "Conversation ID is required"));
}
boolean deleted = conversationService.deleteConversation(conversationId);
if (!deleted) {
return ResponseEntity.ok(Message.fail((byte) -1, "Conversation not found: " + conversationId));
}
return ResponseEntity.ok(Message.success("Conversation deleted successfully"));
} catch (Exception e) {
log.error("Error deleting conversation: ", e);
return ResponseEntity.ok(Message.fail((byte) -1, "Failed to delete conversation"));
}
@Parameter(description = "Conversation ID", example = "2345678") @PathVariable("conversationId") Long conversationId) {
conversationService.deleteConversation(conversationId);
return ResponseEntity.ok(Message.success());
}
}
}
@@ -15,13 +15,15 @@
* limitations under the License.
*/
package org.apache.hertzbeat.manager.service;
package org.apache.hertzbeat.ai.dao;
import org.apache.hertzbeat.manager.service.ai.AlibabaAiServiceImpl;
import org.apache.hertzbeat.common.entity.ai.ChatConversation;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
/**
* test case for {@link AlibabaAiServiceImpl}
* Repository interface for Conversation entities
*/
class AlibabaAiServiceTest {
@Repository
public interface ChatConversationDao extends JpaRepository<ChatConversation, Long> {
}
@@ -15,13 +15,16 @@
* limitations under the License.
*/
package org.apache.hertzbeat.manager.service;
package org.apache.hertzbeat.ai.dao;
import org.apache.hertzbeat.manager.service.ai.ZhiPuServiceImpl;
import org.apache.hertzbeat.common.entity.ai.ChatMessage;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
/**
* test case for {@link ZhiPuServiceImpl}
* Repository interface for Message entities
*/
class ZhiPuServiceTest {
@Repository
public interface ChatMessageDao extends JpaRepository<ChatMessage, Long> {
}
@@ -16,7 +16,7 @@
*/
package org.apache.hertzbeat.ai.agent.pojo.dto;
package org.apache.hertzbeat.ai.pojo.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
@@ -24,6 +24,7 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import org.apache.hertzbeat.common.entity.ai.ChatMessage;
/**
* Chat request context for AI chat endpoint.
@@ -40,10 +41,10 @@ public class ChatRequestContext {
/**
* Optional conversation ID for context
*/
private String conversationId;
private Long conversationId;
/**
* Conversation history messages for context
*/
private List<MessageDto> conversationHistory;
}
private List<ChatMessage> conversationHistory;
}
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.pojo.dto;
package org.apache.hertzbeat.ai.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
@@ -31,17 +31,18 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Chat response")
public class ChatResponseDto {
public class ChatResponseChunk {
@Schema(description = "Conversation ID", example = "conv-123")
private String conversationId;
@Schema(description = "Conversation ID", example = "123")
private Long conversationId;
@Schema(description = "AI response message", example = "Here are your monitors...")
private String response;
@Schema(description = "User message ID", example = "msg-user-123")
private String userMessageId;
@Schema(description = "User message ID", example = "123")
private Long userMessageId;
@Schema(description = "Assistant message ID", example = "123")
private Long assistantMessageId;
@Schema(description = "Assistant message ID", example = "msg-assistant-123")
private String assistantMessageId;
}
}
@@ -16,9 +16,9 @@
*/
package org.apache.hertzbeat.ai.agent.service;
package org.apache.hertzbeat.ai.service;
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatRequestContext;
import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext;
import reactor.core.publisher.Flux;
/**
@@ -33,4 +33,10 @@ public interface ChatClientProviderService {
* @return Flux of string chunks from the LLM response
*/
Flux<String> streamChat(ChatRequestContext context);
}
/**
* Check if provider is properly configured
* @return true if configured and enabled
*/
boolean isConfigured();
}
@@ -16,10 +16,10 @@
*/
package org.apache.hertzbeat.ai.agent.service;
package org.apache.hertzbeat.ai.service;
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatResponseDto;
import org.apache.hertzbeat.ai.agent.pojo.dto.ConversationDto;
import org.apache.hertzbeat.ai.pojo.dto.ChatResponseChunk;
import org.apache.hertzbeat.common.entity.ai.ChatConversation;
import org.springframework.http.codec.ServerSentEvent;
import reactor.core.publisher.Flux;
@@ -30,13 +30,6 @@ import java.util.List;
*/
public interface ConversationService {
/**
* Create a new conversation
*
* @return Created conversation data
*/
ConversationDto createConversation();
/**
* Send a message and receive a streaming response
*
@@ -44,8 +37,14 @@ public interface ConversationService {
* @param conversationId Optional conversation ID for continuing a chat
* @return Flux of ServerSentEvent for streaming the response
*/
Flux<ServerSentEvent<ChatResponseDto>> streamChat(String message, String conversationId);
Flux<ServerSentEvent<ChatResponseChunk>> streamChat(String message, Long conversationId);
/**
* Create a new conversation
*
* @return Created conversation data
*/
ChatConversation createConversation();
/**
* Get conversation history for a specific conversation
@@ -53,28 +52,19 @@ public interface ConversationService {
* @param conversationId Conversation ID
* @return Conversation data including messages
*/
ConversationDto getConversation(String conversationId);
ChatConversation getConversation(Long conversationId);
/**
* Get all conversations for the current user
*
* @return List of conversations
*/
List<ConversationDto> getAllConversations();
List<ChatConversation> getAllConversations();
/**
* Delete a conversation
*
* @param conversationId Conversation ID to delete
* @return true if deleted, false if conversation not found
*/
boolean deleteConversation(String conversationId);
/**
* Check if a conversation exists
*
* @param conversationId Conversation ID to check
* @return true if conversation exists, false otherwise
*/
boolean conversationExists(String conversationId);
}
void deleteConversation(Long conversationId);
}
@@ -16,10 +16,10 @@
*/
package org.apache.hertzbeat.ai.agent.service;
package org.apache.hertzbeat.ai.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.hertzbeat.ai.agent.config.CustomSseServerTransport;
import org.apache.hertzbeat.ai.config.CustomSseServerTransport;
import org.springframework.ai.mcp.server.autoconfigure.McpServerProperties;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.web.servlet.function.RouterFunction;
@@ -16,14 +16,18 @@
*/
package org.apache.hertzbeat.ai.agent.service.impl;
package org.apache.hertzbeat.ai.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.config.PromptProvider;
import org.apache.hertzbeat.ai.agent.pojo.dto.MessageDto;
import org.apache.hertzbeat.ai.agent.service.ChatClientProviderService;
import org.apache.hertzbeat.ai.config.PromptProvider;
import org.apache.hertzbeat.common.entity.ai.ChatMessage;
import org.apache.hertzbeat.common.entity.dto.ModelProviderConfig;
import org.apache.hertzbeat.ai.service.ChatClientProviderService;
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
import org.apache.hertzbeat.common.entity.manager.GeneralConfig;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.springframework.stereotype.Service;
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatRequestContext;
import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
@@ -31,6 +35,7 @@ import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
@@ -45,32 +50,33 @@ import java.util.List;
@Service
public class ChatClientProviderServiceImpl implements ChatClientProviderService {
private final ChatClient chatClient;
private final ApplicationContext applicationContext;
private final GeneralConfigDao generalConfigDao;
@Qualifier("hertzbeatTools")
@Autowired
private ToolCallbackProvider toolCallbackProvider;
private boolean isConfigured = false;
@Autowired
public ChatClientProviderServiceImpl(ChatClient openAiChatClient) {
this.chatClient = openAiChatClient;
}
public String complete(String message) {
return this.chatClient.prompt()
.user(message)
.call()
.content();
public ChatClientProviderServiceImpl(ApplicationContext applicationContext, GeneralConfigDao generalConfigDao) {
this.applicationContext = applicationContext;
this.generalConfigDao = generalConfigDao;
}
@Override
public Flux<String> streamChat(ChatRequestContext context) {
try {
// Get the current (potentially refreshed) ChatClient instance
ChatClient chatClient = applicationContext.getBean("openAiChatClient", ChatClient.class);
List<Message> messages = new ArrayList<>();
// Add conversation history if available
if (context.getConversationHistory() != null && !context.getConversationHistory().isEmpty()) {
for (MessageDto historyMessage : context.getConversationHistory()) {
for (ChatMessage historyMessage : context.getConversationHistory()) {
if ("user".equals(historyMessage.getRole())) {
messages.add(new UserMessage(historyMessage.getContent()));
} else if ("assistant".equals(historyMessage.getRole())) {
@@ -83,7 +89,7 @@ public class ChatClientProviderServiceImpl implements ChatClientProviderService
log.info("Starting streaming chat for conversation: {}", context.getConversationId());
return this.chatClient.prompt()
return chatClient.prompt()
.messages(messages)
.system(PromptProvider.HERTZBEAT_SYSTEM_PROMPT)
.toolCallbacks(toolCallbackProvider)
@@ -97,4 +103,14 @@ public class ChatClientProviderServiceImpl implements ChatClientProviderService
return Flux.error(e);
}
}
}
@Override
public boolean isConfigured() {
if (!isConfigured) {
GeneralConfig providerConfig = generalConfigDao.findByType("provider");
ModelProviderConfig modelProviderConfig = JsonUtil.fromJson(providerConfig.getContent(), ModelProviderConfig.class);
isConfigured = modelProviderConfig != null && modelProviderConfig.getApiKey() != null;
}
return isConfigured;
}
}
@@ -0,0 +1,160 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.dao.ChatConversationDao;
import org.apache.hertzbeat.ai.dao.ChatMessageDao;
import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext;
import org.apache.hertzbeat.ai.pojo.dto.ChatResponseChunk;
import org.apache.hertzbeat.ai.service.ChatClientProviderService;
import org.apache.hertzbeat.ai.service.ConversationService;
import org.apache.hertzbeat.common.entity.ai.ChatConversation;
import org.apache.hertzbeat.common.entity.ai.ChatMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import reactor.core.publisher.Flux;
import java.util.List;
import java.util.UUID;
/**
* Implementation of the ConversationService interface for managing chat conversations.
*/
@Slf4j
@Service
public class ConversationServiceImpl implements ConversationService {
@Autowired
private ChatConversationDao conversationDao;
@Autowired
private ChatMessageDao messageDao;
@Autowired
private ChatClientProviderService chatClientProviderService;
@Override
public Flux<ServerSentEvent<ChatResponseChunk>> streamChat(String message, Long conversationId) {
// Check if provider is properly configured
if (!chatClientProviderService.isConfigured()) {
ChatResponseChunk errorResponse = ChatResponseChunk.builder()
.conversationId(conversationId)
.response("Provider is not configured. Please configure your AI Provider.")
.build();
return Flux.just(ServerSentEvent.builder(errorResponse)
.event("error")
.build());
}
log.info("Starting streaming conversation: {}", conversationId);
// Add user message to conversation
ChatMessage chatMessage = ChatMessage.builder().conversationId(conversationId)
.content(message).role("user").build();
chatMessage = messageDao.save(chatMessage);
ChatConversation conversation = conversationDao.getReferenceById(conversationId);
if (conversation.getTitle().startsWith("conversation")) {
// Auto-generate title from first user message
String title = message.length() > 30 ? message.substring(0, 27) + "..." : message;
conversation.setTitle(title);
conversationDao.save(conversation);
}
ChatRequestContext context = ChatRequestContext.builder()
.message(message)
.conversationId(conversationId)
.conversationHistory(CollectionUtils.isEmpty(conversation.getMessages()) ? null
: conversation.getMessages().subList(0, conversation.getMessages().size() - 1))
.build();
// Stream response from AI service
StringBuilder fullResponse = new StringBuilder();
ChatMessage finalChatMessage = chatMessage;
return chatClientProviderService.streamChat(context)
.map(chunk -> {
fullResponse.append(chunk);
ChatResponseChunk responseChunk = ChatResponseChunk.builder()
.conversationId(conversationId)
.userMessageId(finalChatMessage.getId())
.response(chunk)
.build();
return ServerSentEvent.builder(responseChunk)
.event("message")
.build();
})
.concatWith(Flux.defer(() -> {
// Add the complete AI response to conversation
ChatMessage assistantMessage = ChatMessage.builder().conversationId(conversationId)
.content(fullResponse.toString()).role("assistant").build();
assistantMessage = messageDao.save(assistantMessage);
ChatResponseChunk finalResponse = ChatResponseChunk.builder()
.conversationId(conversationId)
.response("")
.assistantMessageId(assistantMessage.getId())
.build();
return Flux.just(ServerSentEvent.builder(finalResponse)
.event("complete")
.build());
}))
.doOnComplete(() -> log.info("Streaming completed for conversation: {}", conversationId))
.doOnError(error -> log.error("Error in streaming chat for conversation {}: {}", conversationId, error.getMessage(), error))
.onErrorResume(error -> {
ChatResponseChunk errorResponse = ChatResponseChunk.builder()
.conversationId(conversationId)
.response("An error occurred: " + error.getMessage())
.userMessageId(finalChatMessage.getId())
.build();
return Flux.just(ServerSentEvent.builder(errorResponse)
.event("error")
.build());
});
}
@Override
public ChatConversation createConversation() {
ChatConversation conversation = new ChatConversation();
conversation.setTitle("conversation-" + UUID.randomUUID().toString().substring(0, 4));
return conversationDao.save(conversation);
}
@Override
public ChatConversation getConversation(Long conversationId) {
if (conversationId == null) {
return null;
}
return conversationDao.getReferenceById(conversationId);
}
@Override
public List<ChatConversation> getAllConversations() {
return conversationDao.findAll(Sort.by(Sort.Direction.DESC, "id"));
}
@Override
public void deleteConversation(Long conversationId) {
conversationDao.deleteById(conversationId);
}
}
@@ -16,14 +16,14 @@
*/
package org.apache.hertzbeat.ai.agent.service.impl;
package org.apache.hertzbeat.ai.service.impl;
import org.apache.hertzbeat.ai.agent.config.CustomSseServerTransport;
import org.apache.hertzbeat.ai.agent.service.McpServerService;
import org.apache.hertzbeat.ai.agent.tools.AlertDefineTools;
import org.apache.hertzbeat.ai.agent.tools.AlertTools;
import org.apache.hertzbeat.ai.agent.tools.MetricsTools;
import org.apache.hertzbeat.ai.agent.tools.MonitorTools;
import org.apache.hertzbeat.ai.config.CustomSseServerTransport;
import org.apache.hertzbeat.ai.service.McpServerService;
import org.apache.hertzbeat.ai.tools.AlertDefineTools;
import org.apache.hertzbeat.ai.tools.AlertTools;
import org.apache.hertzbeat.ai.tools.MetricsTools;
import org.apache.hertzbeat.ai.tools.MonitorTools;
import org.springframework.ai.mcp.server.autoconfigure.McpServerProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.tools;
package org.apache.hertzbeat.ai.tools;
/**
* Tools for alert definition and threshold configuration operations
@@ -88,4 +88,4 @@ public interface AlertDefineTools {
* @return Result message indicating success or failure
*/
String bindMonitorsToAlertRule(Long ruleId, String monitorIds);
}
}
@@ -16,7 +16,7 @@
*/
package org.apache.hertzbeat.ai.agent.tools;
package org.apache.hertzbeat.ai.tools;
/**
* Tools for alert operations and alarm data queries
@@ -16,7 +16,7 @@
*/
package org.apache.hertzbeat.ai.agent.tools;
package org.apache.hertzbeat.ai.tools;
/**
* Tools for metrics data operations and queries
@@ -16,7 +16,7 @@
*/
package org.apache.hertzbeat.ai.agent.tools;
package org.apache.hertzbeat.ai.tools;
import java.util.List;
@@ -94,4 +94,4 @@ public interface MonitorTools {
* @return Formatted string with parameter definitions including field names, types, and requirements
*/
String getMonitorAdditionalParams(String app);
}
}
@@ -15,19 +15,20 @@
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.tools.impl;
package org.apache.hertzbeat.ai.tools.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.adapters.AlertDefineServiceAdapter;
import org.apache.hertzbeat.ai.agent.pojo.dto.Hierarchy;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.apache.hertzbeat.ai.agent.tools.AlertDefineTools;
import org.apache.hertzbeat.ai.agent.utils.UtilityClass;
import org.apache.hertzbeat.ai.config.McpContextHolder;
import org.apache.hertzbeat.alert.service.AlertDefineService;
import org.apache.hertzbeat.manager.service.AppService;
import org.apache.hertzbeat.ai.tools.AlertDefineTools;
import org.apache.hertzbeat.ai.utils.UtilityClass;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.manager.pojo.dto.Hierarchy;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.beans.factory.annotation.Autowired;
@@ -46,7 +47,9 @@ import java.util.Map;
@Service
public class AlertDefineToolsImpl implements AlertDefineTools {
@Autowired
private AlertDefineServiceAdapter alertDefineServiceAdapter;
private AlertDefineService alertDefineService;
@Autowired
private AppService appService;
@Override
@@ -240,17 +243,19 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
.datasource(datasource)
.enable(enable)
.build();
log.debug("Current security subject for addAlertDefine: {}", subjectSum);
AlertDefine createdAlertDefine = alertDefineServiceAdapter.addAlertDefine(alertDefine);
alertDefineService.addAlertDefine(alertDefine);
// Note: Monitor binding is handled separately via bind_monitors_to_alert_rule tool
String bindingNote = String.format(" (Use bind_monitors_to_alert_rule tool to associate specific monitors)");
log.info("Successfully created alert rule '{}' with ID: {}", name, createdAlertDefine.getId());
log.info("Successfully created alert rule '{}' with ID: {}", name, alertDefine.getId());
StringBuilder response = new StringBuilder();
response.append(String.format("Successfully created %s alert rule '%s' with ID: %d\n",
type, name, createdAlertDefine.getId()));
type, name, alertDefine.getId()));
response.append(String.format("Expression: %s\n", expr));
response.append(String.format("Priority: %d (%s)\n", priority, severityLabel));
response.append(String.format("Trigger after: %d consecutive violations\n", times));
@@ -296,8 +301,11 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
pageSize = 10;
}
Page<AlertDefine> result = alertDefineServiceAdapter.getAlertDefines(
search, monitorType, enabled, "gmtCreate", "desc", pageIndex, pageSize);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAlertDefines: {}", subjectSum);
Page<AlertDefine> result = alertDefineService.getAlertDefines(null,
search, "gmtCreate", "desc", pageIndex, pageSize);
StringBuilder response = new StringBuilder();
response.append("Found ").append(result.getContent().size())
@@ -345,7 +353,21 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
try {
log.info("Toggling alert rule ID: {} to enabled: {}", ruleId, enabled);
alertDefineServiceAdapter.toggleAlertDefineStatus(ruleId, enabled);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for toggleAlertDefineStatus: {}", subjectSum);
// First get the existing AlertDefine
AlertDefine alertDefine = alertDefineService.getAlertDefine(ruleId);
if (alertDefine == null) {
throw new RuntimeException("AlertDefine with ID " + ruleId + " not found");
}
// Update the enable status
alertDefine.setEnable(enabled);
// Use modifyAlertDefine to save the changes
alertDefineService.modifyAlertDefine(alertDefine);
log.info("Successfully toggled alert rule ID: {} to enabled: {}", ruleId, enabled);
return String.format("Successfully %s alert rule ID: %d",
@@ -369,7 +391,10 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
try {
log.info("Getting alert rule details for ID: {}", ruleId);
AlertDefine alertDefine = alertDefineServiceAdapter.getAlertDefine(ruleId);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAlertDefine: {}", subjectSum);
AlertDefine alertDefine = alertDefineService.getAlertDefine(ruleId);
if (alertDefine == null) {
return "Alert rule with ID " + ruleId + " not found";
}
@@ -441,7 +466,7 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
log.debug("Current subject in get_apps_metrics_hierarchy tool: {}", subjectSum);
List<Hierarchy> hierarchies;
hierarchies = alertDefineServiceAdapter.getAppHierarchy(app.trim().toLowerCase(), "en-US");
hierarchies = appService.getAppHierarchy(app.trim().toLowerCase(), "en-US");
ObjectMapper mapper = new ObjectMapper();
@@ -459,10 +484,7 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
result.put("message", "No hierarchy data available");
}
String jsonResult = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(result);
log.info("Hierarchy JSON: {}", jsonResult);
return jsonResult;
return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(result);
} catch (Exception e) {
log.error("Failed to get apps metrics hierarchy: {}", e.getMessage(), e);
@@ -494,7 +516,9 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
}
// Get the existing alert rule
AlertDefine existingRule = alertDefineServiceAdapter.getAlertDefine(ruleId);
log.debug("Current security subject for getAlertDefine: {}", subjectSum);
AlertDefine existingRule = alertDefineService.getAlertDefine(ruleId);
if (existingRule == null) {
return String.format("Error: Alert rule with ID %d not found", ruleId);
}
@@ -571,7 +595,9 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
// Update the alert rule
existingRule.setExpr(newExpr);
alertDefineServiceAdapter.modifyAlertDefine(existingRule);
log.debug("Current security subject for modifyAlertDefine: {}", subjectSum);
alertDefineService.modifyAlertDefine(existingRule);
log.info("Successfully added monitors {} to existing bindings for alert rule ID: {}", validMonitorIds, ruleId);
return String.format("Successfully added %d new monitor(s) to alert rule ID %d.\nTotal bound monitors: %s\nUpdated expression: %s",
@@ -608,7 +634,8 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
// Update the alert rule
existingRule.setExpr(newExpr);
alertDefineServiceAdapter.modifyAlertDefine(existingRule);
alertDefineService.modifyAlertDefine(existingRule);
log.info("Successfully bound monitors {} to alert rule ID: {}", validMonitorIds, ruleId);
return String.format("Successfully bound %d monitor(s) to alert rule ID %d.\nMonitor IDs: %s\nUpdated expression: %s",
@@ -633,7 +660,10 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
log.debug("Validating hierarchy relationships: app={}, metrics={}, fieldConditions={}", app, metrics, fieldConditions);
// Get hierarchy for the specified app
List<Hierarchy> hierarchies = alertDefineServiceAdapter.getAppHierarchy(app.toLowerCase(), "en-US");
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAppHierarchy: {}", subjectSum);
List<Hierarchy> hierarchies = appService.getAppHierarchy(app.toLowerCase(), "en-US");
if (hierarchies == null || hierarchies.isEmpty()) {
return String.format("Error: App '%s' not found in hierarchy. Please use list_monitor_types to get valid app names.", app);
@@ -662,4 +692,4 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
}
}
}
}
@@ -16,15 +16,15 @@
*/
package org.apache.hertzbeat.ai.agent.tools.impl;
package org.apache.hertzbeat.ai.tools.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.adapters.AlertServiceAdapter;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.apache.hertzbeat.ai.agent.tools.AlertTools;
import org.apache.hertzbeat.ai.agent.utils.UtilityClass;
import org.apache.hertzbeat.ai.config.McpContextHolder;
import org.apache.hertzbeat.ai.tools.AlertTools;
import org.apache.hertzbeat.ai.utils.UtilityClass;
import org.apache.hertzbeat.alert.dto.AlertSummary;
import org.apache.hertzbeat.alert.service.AlertService;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.springframework.beans.factory.annotation.Autowired;
@@ -41,7 +41,7 @@ import org.springframework.stereotype.Service;
@Service
public class AlertToolsImpl implements AlertTools {
@Autowired
private AlertServiceAdapter alertServiceAdapter;
private AlertService alertService;
@Override
@Tool(name = "query_alerts", description = """
@@ -116,7 +116,7 @@ public class AlertToolsImpl implements AlertTools {
// Handle different alert types
if ("single".equalsIgnoreCase(alertType) || "both".equalsIgnoreCase(alertType)) {
Page<SingleAlert> singleResult = alertServiceAdapter.getSingleAlerts(status, search, sort, order, pageIndex, pageSize);
Page<SingleAlert> singleResult = alertService.getSingleAlerts(status, search, sort, order, pageIndex, pageSize);
response.append("SINGLE ALERTS:\n");
response.append("Found ").append(singleResult.getContent().size()).append(" single alerts (Total: ").append(singleResult.getTotalElements()).append("):\n\n");
@@ -154,7 +154,7 @@ public class AlertToolsImpl implements AlertTools {
response.append("\n");
}
Page<GroupAlert> groupResult = alertServiceAdapter.getGroupAlerts(status, search, sort, order, pageIndex, pageSize);
Page<GroupAlert> groupResult = alertService.getGroupAlerts(status, search, sort, order, pageIndex, pageSize);
response.append("GROUP ALERTS:\n");
response.append("Found ").append(groupResult.getContent().size()).append(" group alerts (Total: ").append(groupResult.getTotalElements()).append("):\n\n");
@@ -205,7 +205,7 @@ public class AlertToolsImpl implements AlertTools {
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current subject in get_alerts_summary tool: {}", subjectSum);
AlertSummary summary = alertServiceAdapter.getAlertsSummary();
AlertSummary summary = alertService.getAlertsSummary();
StringBuilder response = new StringBuilder();
response.append("ALERTS SUMMARY\n");
@@ -16,14 +16,13 @@
*/
package org.apache.hertzbeat.ai.agent.tools.impl;
package org.apache.hertzbeat.ai.tools.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.adapters.MetricsServiceAdapter;
import org.apache.hertzbeat.ai.agent.adapters.MonitorServiceAdapter;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.apache.hertzbeat.ai.agent.tools.MetricsTools;
import org.apache.hertzbeat.ai.config.McpContextHolder;
import org.apache.hertzbeat.warehouse.service.MetricsDataService;
import org.apache.hertzbeat.ai.tools.MetricsTools;
import org.apache.hertzbeat.common.entity.dto.Field;
import org.apache.hertzbeat.common.entity.dto.MetricsData;
import org.apache.hertzbeat.common.entity.dto.MetricsHistoryData;
@@ -44,9 +43,7 @@ import java.util.Map;
@Service
public class MetricsToolsImpl implements MetricsTools {
@Autowired
private MetricsServiceAdapter metricsServiceAdapter;
@Autowired
private MonitorServiceAdapter monitorServiceAdapter;
private MetricsDataService metricsDataService;
@Override
@Tool(name = "query_realtime_metrics", description = """
@@ -78,7 +75,7 @@ public class MetricsToolsImpl implements MetricsTools {
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current subject in get_realtime_metrics tool: {}", subjectSum);
MetricsData metricsData = metricsServiceAdapter.getMetricsData(monitorId, metrics);
MetricsData metricsData = metricsDataService.getMetricsData(monitorId, metrics);
if (metricsData == null) {
return String.format("No real-time metrics data found for monitor ID %d and metrics '%s'", monitorId, metrics);
@@ -173,7 +170,7 @@ public class MetricsToolsImpl implements MetricsTools {
interval = true;
}
MetricsHistoryData historyData = metricsServiceAdapter.getMetricHistoryData(
MetricsHistoryData historyData = metricsDataService.getMetricHistoryData(
monitorId, app, metrics, fieldParameter, label, history, interval);
if (historyData == null) {
@@ -235,8 +232,10 @@ public class MetricsToolsImpl implements MetricsTools {
public String getWarehouseStatus() {
try {
log.info("Checking warehouse storage status");
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getWarehouseStorageServerStatus: {}", subjectSum);
Boolean status = metricsServiceAdapter.getWarehouseStorageServerStatus();
Boolean status = metricsDataService.getWarehouseStorageServerStatus();
StringBuilder response = new StringBuilder();
response.append("METRICS WAREHOUSE STATUS\n");
@@ -260,4 +259,4 @@ public class MetricsToolsImpl implements MetricsTools {
}
}
}
}
@@ -15,17 +15,18 @@
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.tools.impl;
package org.apache.hertzbeat.ai.tools.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.adapters.MonitorServiceAdapter;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.apache.hertzbeat.ai.agent.utils.UtilityClass;
import org.apache.hertzbeat.ai.config.McpContextHolder;
import org.apache.hertzbeat.manager.service.MonitorService;
import org.apache.hertzbeat.manager.service.AppService;
import org.apache.hertzbeat.ai.utils.UtilityClass;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.apache.hertzbeat.ai.agent.tools.MonitorTools;
import org.apache.hertzbeat.ai.tools.MonitorTools;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.apache.hertzbeat.common.entity.manager.Monitor;
@@ -43,7 +44,9 @@ import java.util.Map;
@Service
public class MonitorToolsImpl implements MonitorTools {
@Autowired
private MonitorServiceAdapter monitorServiceAdapter;
private MonitorService monitorService;
@Autowired
private AppService appService;
/**
@@ -123,8 +126,12 @@ public class MonitorToolsImpl implements MonitorTools {
includeStats = false;
}
Page<Monitor> result = monitorServiceAdapter.getMonitors(ids, app, search, status, sort, order, pageIndex, pageSize, labels);
log.debug("MonitorServiceAdapter.getMonitors result: {}", result);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject: {}", subjectSum);
Page<Monitor> result = monitorService.getMonitors(
ids, app, search, status, sort, order, pageIndex, pageSize, labels);
log.debug("MonitorService.getMonitors result: {}", result);
StringBuilder response = new StringBuilder();
response.append("MONITOR QUERY RESULTS\n");
@@ -133,10 +140,10 @@ public class MonitorToolsImpl implements MonitorTools {
// Include statistics if requested
if (includeStats) {
// Get status distribution by calling with different status values
long onlineCount = monitorServiceAdapter.getMonitors(null, app, search, (byte) 1, null, null, 0, 1000, labels).getTotalElements();
long offlineCount = monitorServiceAdapter.getMonitors(null, app, search, (byte) 2, null, null, 0, 1000, labels).getTotalElements();
long unreachableCount = monitorServiceAdapter.getMonitors(null, app, search, (byte) 3, null, null, 0, 1000, labels).getTotalElements();
long pausedCount = monitorServiceAdapter.getMonitors(null, app, search, (byte) 0, null, null, 0, 1000, labels).getTotalElements();
long onlineCount = monitorService.getMonitors(null, app, search, (byte) 1, null, null, 0, 1000, labels).getTotalElements();
long offlineCount = monitorService.getMonitors(null, app, search, (byte) 2, null, null, 0, 1000, labels).getTotalElements();
long unreachableCount = monitorService.getMonitors(null, app, search, (byte) 3, null, null, 0, 1000, labels).getTotalElements();
long pausedCount = monitorService.getMonitors(null, app, search, (byte) 0, null, null, 0, 1000, labels).getTotalElements();
response.append("STATUS OVERVIEW:\n");
response.append("- Online: ").append(onlineCount).append("\n");
@@ -266,7 +273,10 @@ public class MonitorToolsImpl implements MonitorTools {
// Validate that all required parameters for this monitor type are provided
try {
List<ParamDefine> requiredParams = monitorServiceAdapter.getMonitorParamDefines(app);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getMonitorParamDefines: {}", subjectSum);
List<ParamDefine> requiredParams = appService.getAppParamDefines(app.toLowerCase().trim());
log.info("Checking required parameters for monitor type '{}': {}", app, requiredParams);
List<String> missingParams = new ArrayList<>();
@@ -290,8 +300,12 @@ public class MonitorToolsImpl implements MonitorTools {
log.warn("Could not validate required parameters for monitor type '{}': {}", app, e.getMessage());
}
// Call adapter - it handles all the complexity (validation, defaults, app-specific logic)
Long monitorId = monitorServiceAdapter.addMonitor(monitor, params, null);
// Call monitor service - it handles all the complexity (validation, defaults, app-specific logic)
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for addMonitor: {}", subjectSum);
monitorService.addMonitor(monitor, params, null, null);
Long monitorId = monitor.getId();
log.info("Successfully added monitor '{}' with ID: {}", name, monitorId);
return String.format("Successfully added %s monitor '%s' with ID: %d (Host: %s, Interval: %d seconds)",
@@ -361,14 +375,14 @@ public class MonitorToolsImpl implements MonitorTools {
log.info("Listing available monitor types for language: {}", language);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current subject in list_monitor_types tool: {}", subjectSum);
// Set default language if not provided
if (language == null || language.trim().isEmpty()) {
language = "en-US";
}
// Get available monitor types from adapter
Map<String, String> monitorTypes = monitorServiceAdapter.getAvailableMonitorTypes(language);
// Get available monitor types from app service
Map<String, String> monitorTypes = appService.getI18nApps(language);
if (monitorTypes == null || monitorTypes.isEmpty()) {
return "No monitor types are currently available.";
@@ -416,14 +430,14 @@ public class MonitorToolsImpl implements MonitorTools {
log.info("Getting parameter definitions for monitor type: {}", app);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current subject in get_monitor_param_defines tool: {}", subjectSum);
// Validate required parameter
if (app == null || app.trim().isEmpty()) {
return "Error: Monitor type/application parameter is required";
}
// Get parameter definitions from adapter
List<ParamDefine> paramDefines = monitorServiceAdapter.getMonitorParamDefines(app);
// Get parameter definitions from app service
List<ParamDefine> paramDefines = appService.getAppParamDefines(app.toLowerCase().trim());
if (paramDefines == null || paramDefines.isEmpty()) {
return String.format("No parameter definitions found for monitor type '%s'. "
@@ -15,16 +15,14 @@
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.utils;
package org.apache.hertzbeat.ai.utils;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.pojo.dto.Hierarchy;
import java.lang.reflect.Method;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
@@ -33,6 +31,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.hertzbeat.manager.pojo.dto.Hierarchy;
/**
* Utility class providing helper methods for field expression validation, parsing,
@@ -227,51 +226,6 @@ public class UtilityClass {
return "VALID";
}
/**
* Helper method to validate operator
*/
public boolean isValidOperator(String operator) {
return operator != null && (operator.equals(">") || operator.equals("<")
|| operator.equals(">=") || operator.equals("<=")
|| operator.equals("==") || operator.equals("!="));
}
/**
* Helper method to validate priority
*/
public boolean isValidPriority(String priority) {
return priority != null && (priority.equalsIgnoreCase("critical")
|| priority.equalsIgnoreCase("warning") || priority.equalsIgnoreCase("info"));
}
/**
* Helper method to build expression
*/
public String buildExpression(String metric, String operator, String threshold) {
return String.format("%s %s %s", metric, operator, threshold);
}
/**
* Helper method to parse existing expression into components
*/
public String[] parseExpression(String expression) {
if (expression == null || expression.trim().isEmpty()) {
return null;
}
// Simple parsing for basic expressions like "metric > value"
String[] operators = {">", "<", ">=", "<=", "==", "!="};
for (String op : operators) {
if (expression.contains(" " + op + " ")) {
String[] parts = expression.split(" " + op + " ");
if (parts.length == 2) {
return new String[]{parts[0].trim(), op, parts[1].trim()};
}
}
}
return null;
}
/**
* Helper method to parse key-value pairs from a string
* Format: "key1:value1, key2:value2, ..."
@@ -473,19 +427,6 @@ public class UtilityClass {
return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
/**
* Parse time range string to milliseconds
*/
public long parseTimeRangeToMillis(String timeRange) {
return switch (timeRange.toLowerCase()) {
case "1h" -> 60 * 60 * 1000L;
case "6h" -> 6 * 60 * 60 * 1000L;
case "24h" -> 24 * 60 * 60 * 1000L;
case "7d" -> 7 * 24 * 60 * 60 * 1000L;
default -> 24 * 60 * 60 * 1000L; // default to 24h
};
}
/**
* Helper method to convert monitor status byte to readable text
* @param status The status byte from monitor
@@ -504,143 +445,6 @@ public class UtilityClass {
};
}
/**
* Helper method to get metrics name for a metric type
*/
public String getMetricsNameForType(String metricType) {
return switch (metricType.toLowerCase()) {
case "cpu" -> "cpu";
case "memory" -> "memory";
case "disk" -> "disk";
case "network" -> "network";
default -> "system";
};
}
/**
* Helper method to check if a field represents usage for a metric type
*/
public boolean isUsageField(String field, String metricType) {
if (field == null) return false;
String fieldLower = field.toLowerCase();
String typeLower = metricType.toLowerCase();
return fieldLower.contains("usage")
|| fieldLower.contains("percent")
|| fieldLower.contains("util")
|| (typeLower.equals("cpu") && (fieldLower.contains("cpu") || fieldLower.contains("idle")))
|| (typeLower.equals("memory") && fieldLower.contains("memory"))
|| (typeLower.equals("disk") && fieldLower.contains("disk"));
}
/**
* Convert manager module Hierarchy objects to ai-agent module Hierarchy objects
* This handles the cross-module DTO conversion to avoid ClassCastException
*/
public List<Hierarchy> convertToAgentHierarchies(List<?> managerHierarchies) {
List<Hierarchy> agentHierarchies = new ArrayList<>();
for (Object managerHierarchy : managerHierarchies) {
Hierarchy agentHierarchy = convertToAgentHierarchy(managerHierarchy);
agentHierarchies.add(agentHierarchy);
}
return agentHierarchies;
}
/**
* Convert a single manager Hierarchy object to ai-agent Hierarchy object using reflection
*/
public Hierarchy convertToAgentHierarchy(Object managerHierarchy) {
try {
Hierarchy agentHierarchy = new Hierarchy();
// Use reflection to copy properties from manager DTO to agent DTO
Class<?> managerClass = managerHierarchy.getClass();
// Copy basic properties
agentHierarchy.setCategory(getStringField(managerHierarchy, managerClass, "category"));
agentHierarchy.setValue(getStringField(managerHierarchy, managerClass, "value"));
agentHierarchy.setLabel(getStringField(managerHierarchy, managerClass, "label"));
agentHierarchy.setIsLeaf(getBooleanField(managerHierarchy, managerClass, "isLeaf"));
agentHierarchy.setHide(getBooleanField(managerHierarchy, managerClass, "hide"));
agentHierarchy.setType(getByteField(managerHierarchy, managerClass, "type"));
agentHierarchy.setUnit(getStringField(managerHierarchy, managerClass, "unit"));
// Handle children recursively
List<?> managerChildren = getListField(managerHierarchy, managerClass, "children");
if (managerChildren != null && !managerChildren.isEmpty()) {
List<Hierarchy> agentChildren = convertToAgentHierarchies(managerChildren);
agentHierarchy.setChildren(agentChildren);
}
return agentHierarchy;
} catch (Exception e) {
log.error("Failed to convert manager hierarchy to agent hierarchy: {}", e.getMessage(), e);
throw new RuntimeException("Failed to convert hierarchy", e);
}
}
public String getStringField(Object obj, Class<?> clazz, String fieldName) {
try {
Method getter = clazz.getMethod("get" + capitalize(fieldName));
Object value = getter.invoke(obj);
return value != null ? value.toString() : null;
} catch (Exception e) {
log.debug("Could not get string field '{}': {}", fieldName, e.getMessage());
return null;
}
}
public Boolean getBooleanField(Object obj, Class<?> clazz, String fieldName) {
try {
Method getter = clazz.getMethod("get" + capitalize(fieldName));
Object value = getter.invoke(obj);
return value instanceof Boolean ? (Boolean) value : null;
} catch (Exception e) {
try {
// Try alternative getter pattern for boolean fields
Method isGetter = clazz.getMethod("is" + capitalize(fieldName));
Object value = isGetter.invoke(obj);
return value instanceof Boolean ? (Boolean) value : null;
} catch (Exception e2) {
log.debug("Could not get boolean field '{}': {}", fieldName, e.getMessage());
return null;
}
}
}
public Byte getByteField(Object obj, Class<?> clazz, String fieldName) {
try {
Method getter = clazz.getMethod("get" + capitalize(fieldName));
Object value = getter.invoke(obj);
return value instanceof Byte ? (Byte) value : null;
} catch (Exception e) {
log.debug("Could not get byte field '{}': {}", fieldName, e.getMessage());
return null;
}
}
public List<?> getListField(Object obj, Class<?> clazz, String fieldName) {
try {
Method getter = clazz.getMethod("get" + capitalize(fieldName));
Object value = getter.invoke(obj);
return value instanceof List ? (List<?>) value : null;
} catch (Exception e) {
log.debug("Could not get list field '{}': {}", fieldName, e.getMessage());
return null;
}
}
public String capitalize(String str) {
if (str == null || str.isEmpty()) {
return str;
}
return str.substring(0, 1).toUpperCase() + str.substring(1);
}
/**
* Extract existing monitor IDs from the alert expression
@@ -35,10 +35,12 @@ public class AlerterWorkerPool {
private ThreadPoolExecutor workerExecutor;
private ThreadPoolExecutor notifyExecutor;
private ThreadPoolExecutor logWorkerExecutor;
public AlerterWorkerPool() {
initWorkExecutor();
initNotifyExecutor();
initLogWorkerExecutor();
}
private void initWorkExecutor() {
@@ -77,6 +79,21 @@ public class AlerterWorkerPool {
new ThreadPoolExecutor.AbortPolicy());
}
private void initLogWorkerExecutor() {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("Alerter logWorkerExecutor has uncaughtException.");
log.error(throwable.getMessage(), throwable);
})
.setDaemon(true)
.setNameFormat("log-worker-%d")
.build();
logWorkerExecutor = new ThreadPoolExecutor(10, 10, 10, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
threadFactory,
new ThreadPoolExecutor.AbortPolicy());
}
/**
* Run the alerter task
* @param runnable task
@@ -96,4 +113,13 @@ public class AlerterWorkerPool {
notifyExecutor.execute(runnable);
}
/**
* Executes the given runnable task using the logWorkerExecutor.
*
* @param runnable the task to be executed
* @throws RejectedExecutionException if the task cannot be accepted for execution
*/
public void executeLogJob(Runnable runnable) throws RejectedExecutionException {
logWorkerExecutor.execute(runnable);
}
}
@@ -0,0 +1,72 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.hertzbeat.alert.calculate;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.jexl3.JexlException;
import org.apache.commons.jexl3.JexlExpression;
import org.apache.hertzbeat.common.util.JexlExpressionRunner;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* JexlExprCalculator is a utility class for evaluating JEXL expressions
*/
@Slf4j
@Component
public class JexlExprCalculator {
/**
* Execute an alert expression
* @param fieldValueMap The field value map for expression evaluation
* @param expr The expression to evaluate
* @param ignoreJexlException Whether to ignore JEXL exceptions
* @return true if the expression matches, false otherwise
*/
public boolean execAlertExpression(Map<String, Object> fieldValueMap, String expr, boolean ignoreJexlException) {
Boolean match;
JexlExpression expression;
try {
expression = JexlExpressionRunner.compile(expr);
} catch (JexlException jexlException) {
log.warn("Alarm Rule: {} Compile Error: {}.", expr, jexlException.getMessage());
throw jexlException;
} catch (Exception e) {
log.error("Alarm Rule: {} Unknown Error: {}.", expr, e.getMessage());
throw e;
}
try {
match = (Boolean) JexlExpressionRunner.evaluate(expression, fieldValueMap);
} catch (JexlException jexlException) {
if (ignoreJexlException) {
log.debug("Alarm Rule: {} Run Error: {}.", expr, jexlException.getMessage());
} else {
log.error("Alarm Rule: {} Run Error: {}.", expr, jexlException.getMessage());
}
throw jexlException;
} catch (Exception e) {
log.error("Alarm Rule: {} Unknown Error: {}.", expr, e.getMessage());
throw e;
}
return match != null && match;
}
}
@@ -0,0 +1,232 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.calculate.periodic;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.alert.service.DataSourceService;
import org.apache.hertzbeat.alert.util.AlertTemplateUtil;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Log Periodic Alert Calculator
*/
@Slf4j
@Component
public class LogPeriodicAlertCalculator {
private static final String ROWS = "__rows__";
private final DataSourceService dataSourceService;
private final AlarmCommonReduce alarmCommonReduce;
public LogPeriodicAlertCalculator(DataSourceService dataSourceService, AlarmCommonReduce alarmCommonReduce) {
this.alarmCommonReduce = alarmCommonReduce;
this.dataSourceService = dataSourceService;
}
public void calculate(AlertDefine define) {
if (!define.isEnable() || StringUtils.isEmpty(define.getExpr())) {
log.error("Log define {} is disabled or expression is empty", define.getName());
return;
}
try {
doCalculate(define);
} catch (Exception e) {
log.error("Calculate periodic define {} failed: {}", define.getName(), e.getMessage());
}
}
private void doCalculate(AlertDefine define) {
try {
// Log-based queries are SQL queries with log-specific expressions
List<Map<String, Object>> results = dataSourceService.query(define.getDatasource(), define.getExpr());
if (CollectionUtils.isEmpty(results)) {
return;
}
afterThresholdRuleMatch(results, define);
} catch (Exception ignored) {
// Ignore the query exception eg: no result, timeout, etc
}
}
String getAlertMode(AlertDefine alertDefine) {
String mode = null;
if (alertDefine.getLabels() != null) {
mode = alertDefine.getLabels().get(CommonConstants.ALERT_MODE_LABEL);
}
if (mode == null || mode.isEmpty()) {
return CommonConstants.ALERT_MODE_GROUP; // Default to group mode if not specified
} else {
return mode;
}
}
/**
* Handle alert after threshold rule match
*/
private void afterThresholdRuleMatch(List<Map<String, Object>> alertContext, AlertDefine define) {
// Determine alert mode from configuration
String alertMode = getAlertMode(define);
long currentTime = System.currentTimeMillis();
switch (alertMode) {
case CommonConstants.ALERT_MODE_INDIVIDUAL:
// Generate individual alerts for each matching log
for (Map<String, Object> context : alertContext) {
generateIndividualAlert(define, context, currentTime);
}
break;
case CommonConstants.ALERT_MODE_GROUP:
// Generate a single alert group for all matching logs
generateGroupAlert(define, alertContext, currentTime);
break;
default:
log.warn("Unknown alert mode for define {}: {}", define.getName(), alertMode);
}
}
private void generateIndividualAlert(AlertDefine define, Map<String, Object> context, long currentTime) {
Map<String, String> alertLabels = new HashMap<>(8);
Map<String, String> commonFingerPrints = createCommonFingerprints(define);
alertLabels.putAll(commonFingerPrints);
addContextToMap(context, alertLabels);
Map<String, Object> fieldValueMap = createFieldValueMap(context, define);
Map<String, String> alertAnnotations = createAlertAnnotations(define, fieldValueMap);
// Create and send group alert
SingleAlert alert = SingleAlert.builder()
.labels(alertLabels)
.annotations(alertAnnotations)
.content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap))
.status(CommonConstants.ALERT_STATUS_FIRING)
.triggerTimes(1)
.startAt(currentTime)
.activeAt(currentTime)
.build();
alarmCommonReduce.reduceAndSendAlarm(alert.clone());
log.debug("Generated individual alert for define: {}", define.getName());
}
private void addContextToMap(Map<String, Object> context, Map<String, String> alertLabels) {
for (Map.Entry<String, Object> entry : context.entrySet()) {
if (entry.getValue() != null) {
alertLabels.put(entry.getKey(), entry.getValue().toString());
}
}
}
private void generateGroupAlert(AlertDefine define, List<Map<String, Object>> alertContext, long currentTime) {
List<SingleAlert> alerts = new ArrayList<>(alertContext.size());
// Create fingerprints for group alert
Map<String, String> commonFingerPrints = createCommonFingerprints(define);
// Add context information to fingerprints
commonFingerPrints.put(ROWS, String.valueOf(alertContext.size()));
commonFingerPrints.put(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_GROUP);
for (Map<String, Object> context : alertContext) {
Map<String, String> alertLabels = new HashMap<>(8);
alertLabels.putAll(commonFingerPrints);
// add the context to commonFingerPrints
addContextToMap(context, alertLabels);
Map<String, Object> fieldValueMap = createFieldValueMap(context, define);
Map<String, String> alertAnnotations = createAlertAnnotations(define, fieldValueMap);
// Create and send group alert
SingleAlert alert = SingleAlert.builder()
.labels(alertLabels)
.annotations(alertAnnotations)
.content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap))
.status(CommonConstants.ALERT_STATUS_FIRING)
.triggerTimes(alertContext.size())
.startAt(currentTime)
.activeAt(currentTime)
.build();
alerts.add(alert.clone());
}
alarmCommonReduce.reduceAndSendAlarmGroup(commonFingerPrints, alerts);
log.debug("Generated group alert for define: {} with {} matching data",
define.getName(), alertContext.size());
}
private Map<String, String> createCommonFingerprints(AlertDefine define) {
Map<String, String> fingerprints = new HashMap<>(8);
fingerprints.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
fingerprints.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(define.getId()));
if (define.getLabels() != null) {
fingerprints.putAll(define.getLabels());
}
return fingerprints;
}
private Map<String, Object> createFieldValueMap(Map<String, Object> context, AlertDefine define) {
Map<String, Object> fieldValueMap = new HashMap<>(8);
for (Map.Entry<String, Object> entry : context.entrySet()) {
if (entry.getValue() != null) {
fieldValueMap.put(entry.getKey(), entry.getValue().toString());
}
}
if (define.getLabels() != null) {
fieldValueMap.putAll(define.getLabels());
}
return fieldValueMap;
}
private Map<String, String> createAlertAnnotations(AlertDefine define, Map<String, Object> fieldValueMap) {
Map<String, String> annotations = new HashMap<>(8);
if (define.getAnnotations() != null) {
for (Map.Entry<String, String> entry : define.getAnnotations().entrySet()) {
annotations.put(entry.getKey(),
AlertTemplateUtil.render(entry.getValue(), fieldValueMap));
}
}
return annotations;
}
}
@@ -15,11 +15,12 @@
* limitations under the License.
*/
package org.apache.hertzbeat.alert.calculate;
package org.apache.hertzbeat.alert.calculate.periodic;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.alert.calculate.AlarmCacheManager;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.alert.service.DataSourceService;
import org.apache.hertzbeat.alert.util.AlertTemplateUtil;
@@ -34,12 +35,11 @@ import java.util.List;
import java.util.Map;
/**
* Periodic Alert Calculator
* Metrics Periodic Alert Calculator
*/
@Slf4j
@Component
public class PeriodicAlertCalculator {
public class MetricsPeriodicAlertCalculator {
private static final String VALUE = "__value__";
private static final String TIMESTAMP = "__timestamp__";
@@ -48,71 +48,82 @@ public class PeriodicAlertCalculator {
private final AlarmCommonReduce alarmCommonReduce;
private final AlarmCacheManager alarmCacheManager;
public PeriodicAlertCalculator(DataSourceService dataSourceService, AlarmCommonReduce alarmCommonReduce,
AlarmCacheManager alarmCacheManager) {
this.dataSourceService = dataSourceService;
public MetricsPeriodicAlertCalculator(DataSourceService dataSourceService, AlarmCommonReduce alarmCommonReduce,
AlarmCacheManager alarmCacheManager) {
this.alarmCommonReduce = alarmCommonReduce;
this.alarmCacheManager = alarmCacheManager;
this.dataSourceService = dataSourceService;
}
/**
* Calculate alerts for the given alert definition
* @param define The alert definition to calculate
*/
public void calculate(AlertDefine define) {
if (!define.isEnable() || StringUtils.isEmpty(define.getExpr())) {
log.error("Periodic define {} is disabled or expression is empty", define.getName());
return;
}
long currentTimeMilli = System.currentTimeMillis();
try {
// for prometheus is instant promql query, for db is sql query
// result: [{'value': 100, 'timestamp': 1343554, 'instance': 'node1'},{'value': 200, 'timestamp': 1343555, 'instance': 'node2'}]
// the return result should be matched with threshold
try {
List<Map<String, Object>> results = dataSourceService.calculate(
define.getDatasource(),
define.getExpr()
);
// if no match the expr threshold, the results item map {'value': null} should be null and others field keep
// if results has multi list, should trigger multi alert
if (CollectionUtils.isEmpty(results)) {
return;
}
for (Map<String, Object> result : results) {
Map<String, String> fingerPrints = new HashMap<>(8);
// here use the alert name as finger, not care the alert name may be changed
fingerPrints.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(define.getId()));
fingerPrints.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
fingerPrints.putAll(define.getLabels());
for (Map.Entry<String, Object> entry : result.entrySet()) {
if (entry.getValue() != null && !VALUE.equals(entry.getKey())
&& !TIMESTAMP.equals(entry.getKey())) {
fingerPrints.put(entry.getKey(), entry.getValue().toString());
}
}
if (result.get(VALUE) == null) {
// recovery the alert
handleRecoveredAlert(define.getId(), fingerPrints);
continue;
}
Map<String, Object> fieldValueMap = new HashMap<>(8);
fieldValueMap.putAll(define.getLabels());
fieldValueMap.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
for (Map.Entry<String, Object> entry : result.entrySet()) {
if (entry.getValue() != null) {
fieldValueMap.put(entry.getKey(), entry.getValue());
}
}
afterThresholdRuleMatch(currentTimeMilli, fingerPrints, fieldValueMap, define);
}
} catch (Exception ignored) {
// ignore the query exception eg: no result, timeout, etc
return;
}
doCalculate(define, currentTimeMilli);
} catch (Exception e) {
log.error("Calculate periodic define {} failed: {}", define.getName(), e.getMessage());
}
}
private void doCalculate(AlertDefine define, long currentTimeMilli) {
try {
List<Map<String, Object>> results = dataSourceService.calculate(
define.getDatasource(),
define.getExpr()
);
// If no match the expr threshold, the results item map {'value': null} should be null and others field keep
// If results has multi list, should trigger multi alert
if (CollectionUtils.isEmpty(results)) {
return;
}
for (Map<String, Object> result : results) {
Map<String, String> fingerPrints = new HashMap<>(8);
// Here use the alert name as finger, not care the alert name may be changed
fingerPrints.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(define.getId()));
fingerPrints.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
fingerPrints.putAll(define.getLabels());
for (Map.Entry<String, Object> entry : result.entrySet()) {
if (entry.getValue() != null && !VALUE.equals(entry.getKey())
&& !TIMESTAMP.equals(entry.getKey())) {
fingerPrints.put(entry.getKey(), entry.getValue().toString());
}
}
if (result.get(VALUE) == null) {
// Recovery the alert
handleRecoveredAlert(define.getId(), fingerPrints);
continue;
}
Map<String, Object> fieldValueMap = new HashMap<>(8);
fieldValueMap.putAll(define.getLabels());
fieldValueMap.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
for (Map.Entry<String, Object> entry : result.entrySet()) {
if (entry.getValue() != null) {
fieldValueMap.put(entry.getKey(), entry.getValue());
}
}
afterThresholdRuleMatch(currentTimeMilli, fingerPrints, fieldValueMap, define);
}
} catch (Exception ignored) {
// Ignore the query exception eg: no result, timeout, etc
}
}
/**
* Handle alert after threshold rule match
*/
private void afterThresholdRuleMatch(long currentTimeMilli, Map<String, String> fingerPrints,
Map<String, Object> fieldValueMap, AlertDefine define) {
Map<String, Object> fieldValueMap, AlertDefine define) {
Long defineId = define.getId();
String fingerprint = AlertUtil.calculateFingerprint(fingerPrints);
SingleAlert existingAlert = alarmCacheManager.getPending(defineId, fingerprint);
@@ -124,7 +135,6 @@ public class PeriodicAlertCalculator {
// First time triggering alert, create new alert and set to pending status
SingleAlert newAlert = SingleAlert.builder()
.labels(labels)
// todo render var content in annotations
.annotations(define.getAnnotations())
.content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap))
.status(CommonConstants.ALERT_STATUS_PENDING)
@@ -158,11 +168,13 @@ public class PeriodicAlertCalculator {
}
}
/**
* Handle recovered alert
*/
private void handleRecoveredAlert(Long defineId, Map<String, String> fingerprints) {
String fingerprint = AlertUtil.calculateFingerprint(fingerprints);
SingleAlert firingAlert = alarmCacheManager.removeFiring(defineId, fingerprint);
if (firingAlert != null) {
// todo consider multi times to tig for resolved alert
firingAlert.setTriggerTimes(1);
firingAlert.setEndAt(System.currentTimeMillis());
firingAlert.setStatus(CommonConstants.ALERT_STATUS_RESOLVED);
@@ -171,4 +183,4 @@ public class PeriodicAlertCalculator {
alarmCacheManager.removePending(defineId, fingerprint);
}
}
}
@@ -15,10 +15,13 @@
* limitations under the License.
*/
package org.apache.hertzbeat.alert.calculate;
package org.apache.hertzbeat.alert.calculate.periodic;
import static org.apache.hertzbeat.common.constants.CommonConstants.ALERT_THRESHOLD_TYPE_PERIODIC;
import static org.apache.hertzbeat.common.constants.CommonConstants.LOG_ALERT_THRESHOLD_TYPE_PERIODIC;
import static org.apache.hertzbeat.common.constants.CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_PERIODIC;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -39,13 +42,15 @@ import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
@Component
public class PeriodicAlertRuleScheduler implements CommandLineRunner {
private final PeriodicAlertCalculator calculator;
private final MetricsPeriodicAlertCalculator metricsCalculator;
private final LogPeriodicAlertCalculator logCalculator;
private final AlertDefineDao alertDefineDao;
private final ScheduledExecutorService scheduledExecutor;
private final Map<Long, ScheduledFuture<?>> scheduledFutures;
public PeriodicAlertRuleScheduler(PeriodicAlertCalculator calculator, AlertDefineDao alertDefineDao) {
this.calculator = calculator;
public PeriodicAlertRuleScheduler(MetricsPeriodicAlertCalculator metricsCalculator, LogPeriodicAlertCalculator logCalculator, AlertDefineDao alertDefineDao) {
this.metricsCalculator = metricsCalculator;
this.logCalculator = logCalculator;
this.alertDefineDao = alertDefineDao;
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
@@ -76,9 +81,14 @@ public class PeriodicAlertRuleScheduler implements CommandLineRunner {
return;
}
cancelSchedule(rule.getId());
if (rule.getType().equals(ALERT_THRESHOLD_TYPE_PERIODIC)) {
if (rule.getType().equals(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC)
|| rule.getType().equals(LOG_ALERT_THRESHOLD_TYPE_PERIODIC)) {
ScheduledFuture<?> future = scheduledExecutor.scheduleAtFixedRate(() -> {
calculator.calculate(rule);
if (rule.getType().equals(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC)) {
metricsCalculator.calculate(rule);
} else if (rule.getType().equals(LOG_ALERT_THRESHOLD_TYPE_PERIODIC)) {
logCalculator.calculate(rule);
}
}, 0, rule.getPeriod(), java.util.concurrent.TimeUnit.SECONDS);
scheduledFutures.put(rule.getId(), future);
}
@@ -87,7 +97,11 @@ public class PeriodicAlertRuleScheduler implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
log.info("Starting periodic alert rule scheduler...");
List<AlertDefine> periodicRules = alertDefineDao.findAlertDefinesByTypeAndEnableTrue(ALERT_THRESHOLD_TYPE_PERIODIC);
List<AlertDefine> metricsPeriodicRules = alertDefineDao.findAlertDefinesByTypeAndEnableTrue(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC);
List<AlertDefine> logPeriodicRules = alertDefineDao.findAlertDefinesByTypeAndEnableTrue(LOG_ALERT_THRESHOLD_TYPE_PERIODIC);
List<AlertDefine> periodicRules = new ArrayList<>(metricsPeriodicRules.size() + logPeriodicRules.size());
periodicRules.addAll(metricsPeriodicRules);
periodicRules.addAll(logPeriodicRules);
for (AlertDefine rule : periodicRules) {
updateSchedule(rule);
}
@@ -15,8 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.alert.calculate;
package org.apache.hertzbeat.alert.calculate.realtime;
import java.util.Collections;
import java.util.HashMap;
@@ -28,10 +27,10 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.jexl3.JexlException;
import org.apache.commons.jexl3.JexlExpression;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.alert.AlerterWorkerPool;
import org.apache.hertzbeat.alert.calculate.AlarmCacheManager;
import org.apache.hertzbeat.alert.calculate.JexlExprCalculator;
import org.apache.hertzbeat.alert.dao.SingleAlertDao;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.alert.service.AlertDefineService;
@@ -43,7 +42,6 @@ import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.queue.CommonDataQueue;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.hertzbeat.common.util.JexlExpressionRunner;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
@@ -53,10 +51,10 @@ import org.springframework.util.CollectionUtils;
*/
@Component
@Slf4j
public class RealTimeAlertCalculator {
public class MetricsRealTimeAlertCalculator {
private static final int CALCULATE_THREADS = 3;
private static final String KEY_INSTANCE = "__instance__";
private static final String KEY_INSTANCE_NAME = "__instancename__";
private static final String KEY_INSTANCE_HOST = "__instancehost__";
@@ -81,38 +79,47 @@ public class RealTimeAlertCalculator {
private final AlertDefineService alertDefineService;
private final AlarmCommonReduce alarmCommonReduce;
private final AlarmCacheManager alarmCacheManager;
private final JexlExprCalculator jexlExprCalculator;
@Autowired
public RealTimeAlertCalculator(AlerterWorkerPool workerPool, CommonDataQueue dataQueue,
AlertDefineService alertDefineService, SingleAlertDao singleAlertDao,
AlarmCommonReduce alarmCommonReduce, AlarmCacheManager alarmCacheManager) {
this(workerPool, dataQueue, alertDefineService, singleAlertDao, alarmCommonReduce, alarmCacheManager, true);
public MetricsRealTimeAlertCalculator(AlerterWorkerPool workerPool, CommonDataQueue dataQueue,
AlertDefineService alertDefineService, SingleAlertDao singleAlertDao,
AlarmCommonReduce alarmCommonReduce, AlarmCacheManager alarmCacheManager,
JexlExprCalculator jexlExprCalculator) {
this(workerPool, dataQueue, alertDefineService, singleAlertDao, alarmCommonReduce, alarmCacheManager, jexlExprCalculator, true);
}
/**
* Constructor for RealTimeAlertCalculator with a toggle to control whether to start alert calculation threads.
* Constructor for MetricsRealTimeAlertCalculator with a toggle to control whether to start alert calculation threads.
*
* @param workerPool The worker pool used for concurrent alert calculation.
* @param dataQueue The queue from which metric data is pulled and pushed.
* @param alertDefineService The service providing alert definition rules.
* @param singleAlertDao The DAO for fetching persisted alert states from storage.
* @param alarmCommonReduce The component responsible for reducing and sending alerts.
* @param alarmCacheManager The cache manager for managing alert states.
* @param start If true, the alert calculation threads will start automatically;
* set to false to disable thread start (useful for unit testing).
*/
public RealTimeAlertCalculator(AlerterWorkerPool workerPool, CommonDataQueue dataQueue,
AlertDefineService alertDefineService, SingleAlertDao singleAlertDao,
AlarmCommonReduce alarmCommonReduce, AlarmCacheManager alarmCacheManager, boolean start) {
public MetricsRealTimeAlertCalculator(AlerterWorkerPool workerPool, CommonDataQueue dataQueue,
AlertDefineService alertDefineService, SingleAlertDao singleAlertDao,
AlarmCommonReduce alarmCommonReduce, AlarmCacheManager alarmCacheManager,
JexlExprCalculator jexlExprCalculator, boolean start) {
this.workerPool = workerPool;
this.dataQueue = dataQueue;
this.alarmCommonReduce = alarmCommonReduce;
this.alertDefineService = alertDefineService;
this.alarmCacheManager = alarmCacheManager;
this.jexlExprCalculator = jexlExprCalculator;
if (start) {
startCalculate();
}
}
/**
* Start the alert calculation threads
*/
public void startCalculate() {
Runnable runnable = () -> {
while (!Thread.currentThread().isInterrupted()) {
@@ -132,7 +139,7 @@ public class RealTimeAlertCalculator {
}
}
private void calculate(CollectRep.MetricsData metricsData) {
protected void calculate(CollectRep.MetricsData metricsData) {
long currentTimeMilli = System.currentTimeMillis();
String instance = String.valueOf(metricsData.getId());
String instanceName = metricsData.getInstanceName();
@@ -146,7 +153,7 @@ public class RealTimeAlertCalculator {
int code = metricsData.getCode().getNumber();
Map<String, String> labels = metricsData.getLabels();
Map<String, String> annotations = metricsData.getAnnotations();
List<AlertDefine> thresholds = this.alertDefineService.getRealTimeAlertDefines();
List<AlertDefine> thresholds = this.alertDefineService.getMetricsRealTimeAlertDefines();
// Filter thresholds by app, metrics, labels and instance
thresholds = filterThresholdsByAppAndMetrics(thresholds, app, metrics, labels, instance, priority);
if (thresholds.isEmpty()) {
@@ -198,7 +205,7 @@ public class RealTimeAlertCalculator {
{
// trigger the expr before the metrics data, due the available up down or others
try {
boolean match = execAlertExpression(fieldValueMap, expr, true);
boolean match = jexlExprCalculator.execAlertExpression(fieldValueMap, expr, true);
try {
if (match) {
// If the threshold rule matches, the number of times the threshold has been triggered is determined and an alarm is triggered
@@ -253,7 +260,7 @@ public class RealTimeAlertCalculator {
}
}
try {
boolean match = execAlertExpression(fieldValueMap, expr, false);
boolean match = jexlExprCalculator.execAlertExpression(fieldValueMap, expr, false);
try {
if (match) {
afterThresholdRuleMatch(defineId, currentTimeMilli, fingerPrints, fieldValueMap, define, annotations);
@@ -274,8 +281,9 @@ public class RealTimeAlertCalculator {
* @param thresholds Alert definitions to filter
* @param app Current app name
* @param metrics Current metrics name
* @param labels Current labels
* @param instance Current instance id
* @param priority Current priority
* @param priority Current priority
* @return Filtered alert definitions
*/
public List<AlertDefine> filterThresholdsByAppAndMetrics(List<AlertDefine> thresholds, String app, String metrics, Map<String, String> labels, String instance, int priority) {
@@ -291,7 +299,7 @@ public class RealTimeAlertCalculator {
if (!appMatcher.find() || !app.equals(appMatcher.group(1))) {
return false;
}
// Extract and check available - required
if (priority != 0) {
Matcher availableMatcher = AVAILABLE_PATTERN.matcher(expr);
@@ -313,7 +321,7 @@ public class RealTimeAlertCalculator {
if (!instanceMatcher.find() && !labelMatcher.find()) {
return true;
}
// Reset matcher to check all instances
instanceMatcher.reset();
labelMatcher.reset();
@@ -336,6 +344,9 @@ public class RealTimeAlertCalculator {
.collect(Collectors.toList());
}
/**
* Handle recovered alert
*/
private void handleRecoveredAlert(Long defineId, Map<String, String> fingerprints) {
String fingerprint = AlertUtil.calculateFingerprint(fingerprints);
SingleAlert firingAlert = alarmCacheManager.removeFiring(defineId, fingerprint);
@@ -349,9 +360,14 @@ public class RealTimeAlertCalculator {
alarmCacheManager.removePending(defineId, fingerprint);
}
/**
* Handle alert after threshold rule match
*/
private void afterThresholdRuleMatch(long defineId, long currentTimeMilli, Map<String, String> fingerPrints,
Map<String, Object> fieldValueMap, AlertDefine define,
Map<String, String> annotations) {
// fingerprint for the padding cache
String fingerprint = AlertUtil.calculateFingerprint(fingerPrints);
SingleAlert existingAlert = alarmCacheManager.getPending(defineId, fingerprint);
fieldValueMap.putAll(define.getLabels());
@@ -377,11 +393,11 @@ public class RealTimeAlertCalculator {
// render var content in content
.content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap))
.status(CommonConstants.ALERT_STATUS_PENDING)
.triggerTimes(1)
.triggerTimes(1)
.startAt(currentTimeMilli)
.activeAt(currentTimeMilli)
.build();
// If required trigger times is 1, set to firing status directly
if (requiredTimes <= 1) {
newAlert.setStatus(CommonConstants.ALERT_STATUS_FIRING);
@@ -395,7 +411,7 @@ public class RealTimeAlertCalculator {
// Update existing alert
existingAlert.setTriggerTimes(existingAlert.getTriggerTimes() + 1);
existingAlert.setActiveAt(currentTimeMilli);
// Check if required trigger times reached
if (existingAlert.getStatus().equals(CommonConstants.ALERT_STATUS_PENDING) && existingAlert.getTriggerTimes() >= requiredTimes) {
// Reached trigger times threshold, change to firing status
@@ -407,35 +423,6 @@ public class RealTimeAlertCalculator {
}
}
private boolean execAlertExpression(Map<String, Object> fieldValueMap, String expr, boolean ignoreJexlException) {
Boolean match;
JexlExpression expression;
try {
expression = JexlExpressionRunner.compile(expr);
} catch (JexlException jexlException) {
log.warn("Alarm Rule: {} Compile Error: {}.", expr, jexlException.getMessage());
throw jexlException;
} catch (Exception e) {
log.error("Alarm Rule: {} Unknown Error: {}.", expr, e.getMessage());
throw e;
}
try {
match = (Boolean) JexlExpressionRunner.evaluate(expression, fieldValueMap);
} catch (JexlException jexlException) {
if (ignoreJexlException) {
log.debug("Alarm Rule: {} Run Error: {}.", expr, jexlException.getMessage());
} else {
log.error("Alarm Rule: {} Run Error: {}.", expr, jexlException.getMessage());
}
throw jexlException;
} catch (Exception e) {
log.error("Alarm Rule: {} Unknown Error: {}.", expr, e.getMessage());
throw e;
}
return match != null && match;
}
private Set<String> kvLabelsToKvStringSet(Map<String, String> labels) {
if (labels == null || labels.isEmpty()) {
return Collections.singleton("");
@@ -0,0 +1,140 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.calculate.realtime;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.calculate.realtime.window.LogWorker;
import org.apache.hertzbeat.alert.calculate.realtime.window.TimeService;
import org.apache.hertzbeat.common.entity.log.LogEntry;
import org.apache.hertzbeat.common.queue.CommonDataQueue;
import org.springframework.stereotype.Component;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* WindowedLogRealTimeAlertCalculator - Single entry point for log stream processing
* Responsible for:
* 1. Reading from original log stream
* 2. Extracting event timestamps
* 3. Maintaining maxTimestamp for watermark generation
* 4. Distributing logs to workers
*/
@Component
@Slf4j
public class WindowedLogRealTimeAlertCalculator implements Runnable {
private static final int CALCULATE_THREADS = 3;
private final CommonDataQueue dataQueue;
private final TimeService timeService;
private ThreadPoolExecutor dispatcherExecutor;
private final LogWorker logWorker;
public WindowedLogRealTimeAlertCalculator(CommonDataQueue dataQueue, TimeService timeService, LogWorker logWorker) {
this.dataQueue = dataQueue;
this.timeService = timeService;
this.logWorker = logWorker;
}
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
LogEntry logEntry = dataQueue.pollLogEntry();
if (logEntry != null) {
processLogEntry(logEntry);
dataQueue.sendLogEntryToStorage(logEntry);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (Exception e) {
log.error("Error in log dispatch loop: {}", e.getMessage(), e);
}
}
}
private void processLogEntry(LogEntry logEntry) {
// Extract event timestamp
long eventTimestamp = extractEventTimestamp(logEntry);
// Check if timestamp is within reasonable range
if (!timeService.isValidTimestamp(eventTimestamp)) {
log.warn("Dropping log with invalid timestamp: {}", eventTimestamp);
return;
}
// Check if this is late data based on current watermark
if (timeService.isLateData(eventTimestamp)) {
log.warn("Dropping late data, timestamp: {}, watermark: {}",
eventTimestamp, timeService.getCurrentWatermark());
return;
}
// Update max timestamp (only validated timestamps can update watermark)
timeService.updateMaxTimestamp(eventTimestamp);
logWorker.reduceAndSendLogTask(logEntry);
}
private long extractEventTimestamp(LogEntry logEntry) {
if (logEntry.getTimeUnixNano() != null && logEntry.getTimeUnixNano() != 0) {
return logEntry.getTimeUnixNano() / 1_000_000; // Convert to milliseconds
}
if (logEntry.getObservedTimeUnixNano() != null && logEntry.getObservedTimeUnixNano() != 0) {
return logEntry.getObservedTimeUnixNano() / 1_000_000; // Convert to milliseconds
}
return System.currentTimeMillis();
}
@PostConstruct
public void start() {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("Alerter workerExecutor has uncaughtException.");
log.error(throwable.getMessage(), throwable);
})
.setDaemon(true)
.setNameFormat("log-dispatcher-%d")
.build();
// Create dispatcher thread executor
this.dispatcherExecutor = new ThreadPoolExecutor(
CALCULATE_THREADS,
CALCULATE_THREADS,
10,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
threadFactory,
new ThreadPoolExecutor.AbortPolicy()
);
for (int i = 0; i < CALCULATE_THREADS; i++) {
dispatcherExecutor.execute(this);
}
}
@PreDestroy
public void stop() {
if (dispatcherExecutor != null) {
dispatcherExecutor.shutdownNow();
}
}
}
@@ -0,0 +1,317 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.calculate.realtime.window;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.springframework.stereotype.Component;
import org.apache.hertzbeat.alert.util.AlertTemplateUtil;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.apache.hertzbeat.common.entity.log.LogEntry;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* Alarm Evaluator - Final alarm logic trigger
* Responsible for:
* 1. Receiving closed window data from WindowAggregator
* 2. Applying alert logic (count threshold, alert mode)
* 3. Generating individual alerts or alert groups
* 4. Sending alerts to AlarmCommonReduce
*/
@Component
@Slf4j
public class AlarmEvaluator {
private static final String WINDOW_START_TIME = "window_start_time";
private static final String WINDOW_END_TIME = "window_end_time";
private static final String MATCHING_LOGS_COUNT = "matching_logs_count";
private final AlarmCommonReduce alarmCommonReduce;
private ThreadPoolExecutor workerExecutor;
public AlarmEvaluator(AlarmCommonReduce alarmCommonReduce) {
this.alarmCommonReduce = alarmCommonReduce;
initAlarmEvaluator();
}
public void initAlarmEvaluator() {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("alerter-reduce-worker has uncaughtException.");
log.error(throwable.getMessage(), throwable);
})
.setDaemon(true)
.setNameFormat("alerter-reduce-worker-%d")
.build();
workerExecutor = new ThreadPoolExecutor(2,
10,
10,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
threadFactory,
new ThreadPoolExecutor.AbortPolicy());
}
public void sendAndProcessWindowData(WindowAggregator.WindowData windowData) {
workerExecutor.execute(processWindowData(windowData));
}
private Runnable processWindowData(WindowAggregator.WindowData windowData) {
return () -> {
AlertDefine alertDefine = windowData.getAlertDefine();
List<MatchingLogEvent> matchingLogs = windowData.getMatchingLogs();
if (matchingLogs.isEmpty()) {
return;
}
// Check if count threshold is met
int requiredTimes = alertDefine.getTimes() != null ? alertDefine.getTimes() : 1;
if (matchingLogs.size() < requiredTimes) {
log.debug("Window {} has {} matching logs, but requires {} times",
windowData.getWindowKey(), matchingLogs.size(), requiredTimes);
return;
}
// Determine alert mode from configuration
String alertMode = getAlertMode(alertDefine);
long currentTime = System.currentTimeMillis();
switch (alertMode) {
case CommonConstants.ALERT_MODE_INDIVIDUAL:
// Generate individual alerts for each matching log
for (MatchingLogEvent matchingLog : matchingLogs) {
generateIndividualAlert(matchingLog, currentTime);
}
break;
case CommonConstants.ALERT_MODE_GROUP:
// Generate a single alert group for all matching logs
generateGroupAlert(windowData, matchingLogs, currentTime);
break;
default:
log.warn("Unknown alert mode for define {}: {}", alertDefine.getName(), alertMode);
}
};
}
private void generateIndividualAlert(MatchingLogEvent matchingLog, long currentTime) {
AlertDefine define = matchingLog.getAlertDefine();
LogEntry logEntry = matchingLog.getLogEntry();
Map<String, String> alertLabels = new HashMap<>(8);
// Create fingerprints for group alert
Map<String, String> commonFingerPrints = createCommonFingerprints(define);
alertLabels.putAll(commonFingerPrints);
// add the log data to commonFingerPrints
addLogEntryToMap(logEntry, alertLabels);
Map<String, Object> fieldValueMap = createFieldValueMap(logEntry, define);
Map<String, String> alertAnnotations = createAlertAnnotations(define, fieldValueMap);
// Create and send group alert
SingleAlert alert = SingleAlert.builder()
.labels(alertLabels)
.annotations(alertAnnotations)
.content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap))
.status(CommonConstants.ALERT_STATUS_FIRING)
.triggerTimes(1)
.startAt(currentTime)
.activeAt(currentTime)
.build();
alarmCommonReduce.reduceAndSendAlarm(alert.clone());
log.debug("Generated individual alert for define: {}", define.getName());
}
private void generateGroupAlert(WindowAggregator.WindowData windowData,
List<MatchingLogEvent> matchingLogs, long currentTime) {
List<SingleAlert> alerts = new ArrayList<>(matchingLogs.size());
AlertDefine define = windowData.getAlertDefine();
// Create fingerprints for group alert
Map<String, String> commonFingerPrints = createCommonFingerprints(define);
// Add window information to fingerprints
commonFingerPrints.put(WINDOW_START_TIME, String.valueOf(windowData.getStartTime()));
commonFingerPrints.put(WINDOW_END_TIME, String.valueOf(windowData.getEndTime()));
commonFingerPrints.put(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_GROUP);
commonFingerPrints.put(MATCHING_LOGS_COUNT, String.valueOf(matchingLogs.size()));
for (MatchingLogEvent event: matchingLogs) {
LogEntry logEntry = event.getLogEntry();
Map<String, String> alertLabels = new HashMap<>(8);
alertLabels.putAll(commonFingerPrints);
// add the log data to commonFingerPrints
addLogEntryToMap(logEntry, alertLabels);
Map<String, Object> fieldValueMap = createFieldValueMap(logEntry, define);
Map<String, String> alertAnnotations = createAlertAnnotations(define, fieldValueMap);
// Create and send group alert
SingleAlert alert = SingleAlert.builder()
.labels(alertLabels)
.annotations(alertAnnotations)
.content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap))
.status(CommonConstants.ALERT_STATUS_FIRING)
.triggerTimes(matchingLogs.size())
.startAt(currentTime)
.activeAt(currentTime)
.build();
alerts.add(alert.clone());
}
alarmCommonReduce.reduceAndSendAlarmGroup(commonFingerPrints, alerts);
log.debug("Generated group alert for define: {} with {} matching logs",
define.getName(), matchingLogs.size());
}
private String getAlertMode(AlertDefine alertDefine) {
String mode = null;
if (alertDefine.getLabels() != null) {
mode = alertDefine.getLabels().get(CommonConstants.ALERT_MODE_LABEL);
}
if (mode == null || mode.isEmpty()) {
return CommonConstants.ALERT_MODE_GROUP; // Default to group mode if not specified
} else {
return mode;
}
}
private Map<String, String> createCommonFingerprints(AlertDefine define) {
Map<String, String> fingerprints = new HashMap<>(8);
fingerprints.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
fingerprints.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(define.getId()));
if (define.getLabels() != null) {
fingerprints.putAll(define.getLabels());
}
return fingerprints;
}
private Map<String, Object> createFieldValueMap(LogEntry logEntry, AlertDefine define) {
Map<String, Object> fieldValueMap = new HashMap<>(8);
fieldValueMap.put("log", logEntry);
if (define.getLabels() != null) {
fieldValueMap.putAll(define.getLabels());
}
return fieldValueMap;
}
private Map<String, String> createAlertAnnotations(AlertDefine define, Map<String, Object> fieldValueMap) {
Map<String, String> annotations = new HashMap<>(8);
if (define.getAnnotations() != null) {
for (Map.Entry<String, String> entry : define.getAnnotations().entrySet()) {
annotations.put(entry.getKey(),
AlertTemplateUtil.render(entry.getValue(), fieldValueMap));
}
}
return annotations;
}
/**
* Add the content from LogEntry object (except timestamp) to commonFingerPrints
*
* @param logEntry log entry object
* @param context context
*/
private void addLogEntryToMap(LogEntry logEntry, Map<String, String> context) {
// Add basic fields
if (logEntry.getSeverityNumber() != null) {
context.put("severityNumber", String.valueOf(logEntry.getSeverityNumber()));
}
if (logEntry.getSeverityText() != null) {
context.put("severityText", logEntry.getSeverityText());
}
if (logEntry.getBody() != null) {
context.put("body", String.valueOf(logEntry.getBody()));
}
if (logEntry.getDroppedAttributesCount() != null) {
context.put("droppedAttributesCount", String.valueOf(logEntry.getDroppedAttributesCount()));
}
if (logEntry.getTraceId() != null) {
context.put("traceId", logEntry.getTraceId());
}
if (logEntry.getSpanId() != null) {
context.put("spanId", logEntry.getSpanId());
}
if (logEntry.getTraceFlags() != null) {
context.put("traceFlags", String.valueOf(logEntry.getTraceFlags()));
}
// Add attributes
if (logEntry.getAttributes() != null && !logEntry.getAttributes().isEmpty()) {
for (Map.Entry<String, Object> entry : logEntry.getAttributes().entrySet()) {
if (entry.getValue() != null) {
context.put("attr_" + entry.getKey(), String.valueOf(entry.getValue()));
}
}
}
// Add resource
if (logEntry.getResource() != null && !logEntry.getResource().isEmpty()) {
for (Map.Entry<String, Object> entry : logEntry.getResource().entrySet()) {
if (entry.getValue() != null) {
context.put("resource_" + entry.getKey(), String.valueOf(entry.getValue()));
}
}
}
// Add instrumentationScope
if (logEntry.getInstrumentationScope() != null) {
LogEntry.InstrumentationScope scope = logEntry.getInstrumentationScope();
if (scope.getName() != null) {
context.put("scope_name", scope.getName());
}
if (scope.getVersion() != null) {
context.put("scope_version", scope.getVersion());
}
if (scope.getDroppedAttributesCount() != null) {
context.put("scope_droppedAttributesCount", String.valueOf(scope.getDroppedAttributesCount()));
}
if (scope.getAttributes() != null && !scope.getAttributes().isEmpty()) {
for (Map.Entry<String, Object> entry : scope.getAttributes().entrySet()) {
if (entry.getValue() != null) {
context.put("scope_attr_" + entry.getKey(), String.valueOf(entry.getValue()));
}
}
}
}
}
}
@@ -0,0 +1,123 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.calculate.realtime.window;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.AlerterWorkerPool;
import org.apache.hertzbeat.alert.calculate.JexlExprCalculator;
import org.apache.hertzbeat.alert.service.AlertDefineService;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.entity.log.LogEntry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* LogWorker Responsible for:
* 1. Receiving logs from WindowedLogRealTimeAlertCalculator
* 2. Evaluating alert expressions against logs
* 3. Sending matching logs to WindowAggregator
*/
@Slf4j
@Component
public class LogWorker {
private static final String LOG_PREFIX = "log";
private final AlertDefineService alertDefineService;
private final JexlExprCalculator jexlExprCalculator;
private final WindowAggregator windowAggregator;
private final AlerterWorkerPool workerPool;
@Autowired
public LogWorker(AlertDefineService alertDefineService,
JexlExprCalculator jexlExprCalculator, WindowAggregator windowAggregator,
AlerterWorkerPool workerPool) {
this.alertDefineService = alertDefineService;
this.jexlExprCalculator = jexlExprCalculator;
this.windowAggregator = windowAggregator;
this.workerPool = workerPool;
}
public void reduceAndSendLogTask(LogEntry logEntry) {
workerPool.executeLogJob(reduceLogTask(logEntry));
}
public Runnable reduceLogTask(LogEntry logEntry) {
return () -> {
try {
processLogEntry(logEntry);
} catch (Exception e) {
log.error("Error processing log entry in worker: {}", e.getMessage(), e);
}
};
}
private void processLogEntry(LogEntry logEntry) {
// Get all log alert definitions
List<AlertDefine> alertDefines = alertDefineService.getLogRealTimeAlertDefines();
// Create context for expression evaluation
Map<String, Object> context = new HashMap<>(8);
context.put(LOG_PREFIX, logEntry);
// Process each alert definition
for (AlertDefine define : alertDefines) {
if (define.getExpr() == null || define.getExpr().trim().isEmpty()) {
continue;
}
try {
// Evaluate alert expression
boolean match = jexlExprCalculator.execAlertExpression(context, define.getExpr(), false);
if (match) {
// Create matching log event and send to WindowAggregator
MatchingLogEvent event = createMatchingLogEvent(logEntry, define);
windowAggregator.addMatchingLog(event);
}
} catch (Exception e) {
log.warn("Error evaluating expression for alert define {}: {}", define.getName(), e.getMessage());
}
}
}
private MatchingLogEvent createMatchingLogEvent(LogEntry logEntry, AlertDefine define) {
long eventTimestamp = extractEventTimestamp(logEntry);
return MatchingLogEvent.builder()
.logEntry(logEntry)
.alertDefine(define)
.eventTimestamp(eventTimestamp)
.workerTimestamp(System.currentTimeMillis())
.build();
}
private long extractEventTimestamp(LogEntry logEntry) {
if (logEntry.getTimeUnixNano() != null && logEntry.getTimeUnixNano() != 0) {
return logEntry.getTimeUnixNano() / 1_000_000; // Convert to milliseconds
}
if (logEntry.getObservedTimeUnixNano() != null && logEntry.getObservedTimeUnixNano() != 0) {
return logEntry.getObservedTimeUnixNano() / 1_000_000; // Convert to milliseconds
}
return System.currentTimeMillis();
}
}
@@ -0,0 +1,40 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.calculate.realtime.window;
import lombok.Builder;
import lombok.Data;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.entity.log.LogEntry;
/**
* Represents a log entry that matched an alert expression
* Used for communication between LogWorker and WindowAggregator
*/
@Data
@Builder
public class MatchingLogEvent {
private LogEntry logEntry;
private AlertDefine alertDefine;
private long eventTimestamp;
private long workerTimestamp;
}
@@ -0,0 +1,213 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.calculate.realtime.window;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
/**
* Watermark Generator responsible for:
* 1. Receiving maxTimestamp updates from WindowedLogRealTimeAlertCalculator
* 2. Calculating watermarks based on configurable delay
* 3. Broadcasting watermarks to all subscribers (WindowAggregator)
*/
@Component
@Slf4j
public class TimeService {
static final long DEFAULT_WATERMARK_DELAY_MS = 30_000; // 30 seconds
private static final long WATERMARK_BROADCAST_INTERVAL_MS = 5_000; // 5 seconds
// Define acceptable timestamp range to filter abnormal timestamps
private static final long MAX_FUTURE_TIME_MS = 60_000; // Allow 1 minute future time
private static final long MAX_PAST_TIME_MS = 24 * 60 * 60 * 1000; // Allow 24 hours past time
private final AtomicLong maxTimestamp = new AtomicLong(0);
private final AtomicLong currentWatermark = new AtomicLong(0);
private final CopyOnWriteArrayList<WatermarkListener> listeners = new CopyOnWriteArrayList<>();
private ScheduledExecutorService scheduler;
public TimeService(List<WatermarkListener> initialListeners) {
listeners.addAll(initialListeners);
}
@PostConstruct
public void start() {
// Create internal scheduled executor
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("TimeService scheduler has uncaughtException.");
log.error(throwable.getMessage(), throwable);
})
.setDaemon(true)
.setNameFormat("timeservice-scheduler-%d")
.build();
this.scheduler = Executors.newSingleThreadScheduledExecutor(threadFactory);
// Start watermark broadcast scheduler
scheduler.scheduleAtFixedRate(
this::broadcastWatermark,
0,
WATERMARK_BROADCAST_INTERVAL_MS,
TimeUnit.MILLISECONDS
);
log.info("TimeService started with watermark delay: {}ms", DEFAULT_WATERMARK_DELAY_MS);
}
@PreDestroy
public void stop() {
if (scheduler != null && !scheduler.isShutdown()) {
log.info("Shutting down TimeService scheduler...");
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(10, TimeUnit.SECONDS)) {
log.warn("TimeService scheduler did not terminate within 10 seconds, forcing shutdown");
scheduler.shutdownNow();
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
log.error("TimeService scheduler did not terminate");
}
}
} catch (InterruptedException e) {
log.warn("Interrupted while waiting for TimeService scheduler to terminate");
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
}
log.info("TimeService stopped");
}
/**
* Check if timestamp is within acceptable range
*/
public boolean isValidTimestamp(long timestamp) {
long currentTime = System.currentTimeMillis();
return timestamp >= (currentTime - MAX_PAST_TIME_MS) && timestamp <= (currentTime + MAX_FUTURE_TIME_MS);
}
/**
* Check if data is late based on current watermark
*/
public boolean isLateData(long timestamp) {
return timestamp < getCurrentWatermark();
}
/**
* Update max timestamp from WindowedLogRealTimeAlertCalculator
*/
public void updateMaxTimestamp(long timestamp) {
maxTimestamp.getAndUpdate(current -> Math.max(current, timestamp));
}
/**
* Add watermark listener
*/
public void addWatermarkListener(WatermarkListener listener) {
listeners.add(listener);
}
/**
* Remove watermark listener
*/
public void removeWatermarkListener(WatermarkListener listener) {
listeners.remove(listener);
}
/**
* Get current watermark
*/
public long getCurrentWatermark() {
return currentWatermark.get();
}
/**
* Get max timestamp
*/
public long getMaxTimestamp() {
return maxTimestamp.get();
}
/**
* Calculate and broadcast watermark
*/
private void broadcastWatermark() {
try {
long maxTs = maxTimestamp.get();
if (maxTs <= 0) {
return;
}
// Calculate watermark: maxTimestamp - delay
long newWatermark = maxTs - DEFAULT_WATERMARK_DELAY_MS;
long currentWm = currentWatermark.get();
// Only advance watermark (monotonic property)
if (newWatermark <= currentWm) {
return;
}
if (currentWatermark.compareAndSet(currentWm, newWatermark)) {
// Broadcast to all listeners
Watermark watermark = new Watermark(newWatermark);
for (WatermarkListener listener : listeners) {
try {
listener.onWatermark(watermark);
} catch (Exception e) {
log.error("Error notifying watermark listener: {}", e.getMessage(), e);
}
}
log.debug("Broadcast watermark: {} (maxTimestamp: {})", newWatermark, maxTs);
}
} catch (Exception e) {
log.error("Error in watermark broadcast: {}", e.getMessage(), e);
}
}
/**
* Watermark data class
*/
@Data
@AllArgsConstructor
@Getter
public static class Watermark {
private final long timestamp;
}
/**
* Interface for watermark listeners
*/
public interface WatermarkListener {
void onWatermark(Watermark watermark);
}
}
@@ -0,0 +1,251 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.calculate.realtime.window;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
/**
* Window Aggregator Responsible for:
* 1. Managing window data structure for all active windows
* 2. Receiving matching logs from Workers
* 3. Receiving watermarks from TimeService
* 4. Sending closed windows to AlarmEvaluator
*/
@Component
@Slf4j
public class WindowAggregator implements TimeService.WatermarkListener, Runnable {
private static final long DEFAULT_WINDOW_SIZE_MS = 1 * 60 * 1000; // 1 minutes
private final AlarmEvaluator alarmEvaluator;
private final BlockingQueue<MatchingLogEvent> eventQueue = new LinkedBlockingQueue<>();
private final Map<WindowKey, WindowData> activeWindows = new HashMap<>();
private final Object windowLock = new Object();
private ExecutorService aggregatorExecutor;
public WindowAggregator(AlarmEvaluator alarmEvaluator) {
this.alarmEvaluator = alarmEvaluator;
}
public void addMatchingLog(MatchingLogEvent event) {
try {
eventQueue.put(event);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("Interrupted while adding matching log to aggregator");
}
}
@Override
public void onWatermark(TimeService.Watermark watermark) {
List<WindowData> closedWindows;
synchronized (windowLock) {
closedWindows = new ArrayList<>();
// Find windows that should be closed based on watermark
Iterator<Map.Entry<WindowKey, WindowData>> iterator = activeWindows.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<WindowKey, WindowData> entry = iterator.next();
WindowData windowData = entry.getValue();
// Close window if its end time <= watermark timestamp
if (windowData.getEndTime() <= watermark.getTimestamp()) {
closedWindows.add(windowData);
iterator.remove();
log.debug("Closing window: {} with {} matching logs",
entry.getKey(), windowData.getMatchingLogs().size());
}
}
}
for (WindowData windowData : closedWindows) {
alarmEvaluator.sendAndProcessWindowData(windowData);
}
}
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
MatchingLogEvent event = eventQueue.take();
processMatchingLogEvent(event);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (Exception e) {
log.error("Error processing matching log event: {}", e.getMessage(), e);
}
}
}
private void processMatchingLogEvent(MatchingLogEvent event) {
// Determine window size from alert define (if specified) or use default
long windowSizeMs = getWindowSize(event.getAlertDefine());
// Calculate window boundaries
long eventTime = event.getEventTimestamp();
long windowStart = (eventTime / windowSizeMs) * windowSizeMs;
long windowEnd = windowStart + windowSizeMs;
// Create window key
WindowKey windowKey = new WindowKey(
event.getAlertDefine().getId(),
windowStart,
windowEnd
);
synchronized (windowLock) {
// Get or create window data
WindowData windowData = activeWindows.computeIfAbsent(windowKey,
key -> new WindowData(key, event.getAlertDefine()));
// Add matching log to window
windowData.addMatchingLog(event);
log.debug("Added matching log to window: {} (total logs: {})",
windowKey, windowData.getMatchingLogs().size());
}
}
private long getWindowSize(AlertDefine alertDefine) {
// Check if alert define has custom window size configuration
if (alertDefine.getPeriod() != null) {
return alertDefine.getPeriod() * 1000; // Convert seconds to milliseconds
}
log.info("Using default window size of {} ms for alert define: {}",
DEFAULT_WINDOW_SIZE_MS, alertDefine.getName());
return DEFAULT_WINDOW_SIZE_MS;
}
@PostConstruct
public void start() {
// Create internal executor
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("WindowAggregator executor has uncaughtException.");
log.error(throwable.getMessage(), throwable);
})
.setDaemon(true)
.setNameFormat("window-aggregator-%d")
.build();
aggregatorExecutor = Executors.newSingleThreadExecutor(threadFactory);
// Submit aggregation task
aggregatorExecutor.submit(this);
log.info("WindowAggregator started");
}
@PreDestroy
public void stop() {
if (aggregatorExecutor != null && !aggregatorExecutor.isShutdown()) {
log.info("Shutting down WindowAggregator executor...");
aggregatorExecutor.shutdown();
try {
if (!aggregatorExecutor.awaitTermination(10, TimeUnit.SECONDS)) {
log.warn("WindowAggregator executor did not terminate within 10 seconds, forcing shutdown");
aggregatorExecutor.shutdownNow();
if (!aggregatorExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
log.error("WindowAggregator executor did not terminate");
}
}
} catch (InterruptedException e) {
log.warn("Interrupted while waiting for WindowAggregator executor to terminate");
aggregatorExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
}
log.info("WindowAggregator stopped");
}
/**
* Window key for identifying unique windows
*/
@Data
@Getter
@AllArgsConstructor
public static class WindowKey {
private final long alertDefineId;
private final long startTime;
private final long endTime;
}
/**
* Window data container
*/
public static class WindowData {
@Getter
private final WindowKey windowKey;
@Getter
private final AlertDefine alertDefine;
private final List<MatchingLogEvent> matchingLogs = new ArrayList<>();
@Getter
private final long createdTime;
public WindowData(WindowKey windowKey, AlertDefine alertDefine) {
this.windowKey = windowKey;
this.alertDefine = alertDefine;
this.createdTime = System.currentTimeMillis();
}
public void addMatchingLog(MatchingLogEvent event) {
matchingLogs.add(event);
}
public List<MatchingLogEvent> getMatchingLogs() {
return new ArrayList<>(matchingLogs);
}
/**
* Get start time of the window
* @return start time
*/
public long getStartTime() { return windowKey.getStartTime(); }
/**
* Get end time of the window
* @return end time
*/
public long getEndTime() { return windowKey.getEndTime(); }
}
}
@@ -24,7 +24,7 @@ import java.util.Properties;
import java.util.ResourceBundle;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.dto.MailServerConfig;
import org.apache.hertzbeat.common.entity.dto.MailServerConfig;
import org.apache.hertzbeat.alert.notice.AlertNoticeException;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.manager.GeneralConfig;
@@ -18,6 +18,8 @@
package org.apache.hertzbeat.alert.reduce;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.List;
import java.util.Map;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
@@ -66,6 +68,22 @@ public class AlarmCommonReduce {
public void reduceAndSendAlarm(SingleAlert alert) {
workerExecutor.execute(reduceAlarmTask(alert));
}
public void reduceAndSendAlarmGroup(Map<String, String> groupLabels, List<SingleAlert> alerts) {
workerExecutor.execute(() -> {
try {
// Generate alert fingerprint
for (SingleAlert alert : alerts) {
String fingerprint = generateAlertFingerprint(alert.getLabels());
alert.setFingerprint(fingerprint);
}
// Process the group alert
alarmGroupReduce.processGroupAlert(groupLabels, alerts);
} catch (Exception e) {
log.error("Reduce alarm group failed: {}", e.getMessage());
}
});
}
Runnable reduceAlarmTask(SingleAlert alert) {
return () -> {
@@ -159,7 +159,20 @@ public class AlarmGroupReduce {
sendSingleAlert(alert);
}
}
public void processGroupAlert(Map<String, String> groupLabels, List<SingleAlert> alertList) {
GroupAlert groupAlert = GroupAlert.builder()
.groupKey(generateGroupKey(groupLabels))
.groupLabels(groupLabels)
.commonLabels(extractCommonLabels(alertList))
.commonAnnotations(extractCommonAnnotations(alertList))
.alerts(alertList)
.status(CommonConstants.ALERT_STATUS_FIRING)
.build();
alarmInhibitReduce.inhibitAlarm(groupAlert);
}
private boolean hasRequiredLabels(Map<String, String> labels, List<String> requiredLabels) {
return requiredLabels.stream().allMatch(labels::containsKey);
}
@@ -22,6 +22,7 @@ import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.support.exception.AlertExpressionException;
import org.springframework.data.domain.Page;
import org.springframework.web.multipart.MultipartFile;
import org.apache.hertzbeat.common.constants.CommonConstants;
import java.util.List;
import java.util.Map;
@@ -106,10 +107,23 @@ public interface AlertDefineService {
void importConfig(MultipartFile file) throws Exception;
/**
* Get the real-time alarm definition list
* @return Real-time alarm definition list
* Get the real-time metrics alarm definition list
* @return Real-time metrics alarm definition list
*/
List<AlertDefine> getRealTimeAlertDefines();
List<AlertDefine> getMetricsRealTimeAlertDefines();
/**
* Get the real-time log alarm definition list
* @return Real-time log alarm definition list
*/
List<AlertDefine> getLogRealTimeAlertDefines();
/**
* Get the alarm definition list by type
* @param type Alarm definition type, must be one of the constants defined in {@link CommonConstants}
* @return List of enabled alarm definitions matching the specified type, empty list if none found
*/
List<AlertDefine> getAlertDefinesByType(String type);
/**
* Get define preview
@@ -32,4 +32,12 @@ public interface DataSourceService {
* @return result
*/
List<Map<String, Object>> calculate(String datasource, String expr);
/**
* query result set from db
* @param datasource sql or promql
* @param expr query expr
* @return result
*/
List<Map<String, Object>> query(String datasource, String expr);
}
@@ -19,7 +19,7 @@ package org.apache.hertzbeat.alert.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.config.SmsConfig;
import org.apache.hertzbeat.common.entity.dto.sms.SmsConfig;
import org.apache.hertzbeat.alert.service.impl.SmsLocalSmsClientImpl;
import org.apache.hertzbeat.alert.service.impl.AwsSmsClientImpl;
import org.apache.hertzbeat.alert.service.impl.TencentSmsClientImpl;
@@ -151,4 +151,4 @@ public class SmsClientFactory {
break;
}
}
}
}
@@ -24,7 +24,7 @@ import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.Predicate;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.calculate.PeriodicAlertRuleScheduler;
import org.apache.hertzbeat.alert.calculate.periodic.PeriodicAlertRuleScheduler;
import org.apache.hertzbeat.alert.dao.AlertDefineDao;
import org.apache.hertzbeat.alert.service.AlertDefineImExportService;
import org.apache.hertzbeat.alert.service.AlertDefineService;
@@ -87,7 +87,8 @@ public class AlertDefineServiceImpl implements AlertDefineService {
@Override
public void validate(AlertDefine alertDefine, boolean isModify) throws IllegalArgumentException {
if (StringUtils.hasText(alertDefine.getExpr())) {
if (CommonConstants.ALERT_THRESHOLD_TYPE_REALTIME.equals(alertDefine.getType())) {
if (CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_REALTIME.equals(alertDefine.getType())
|| CommonConstants.LOG_ALERT_THRESHOLD_TYPE_REALTIME.equals(alertDefine.getType())) {
try {
JexlExpressionRunner.compile(alertDefine.getExpr());
} catch (Exception e) {
@@ -214,11 +215,21 @@ public class AlertDefineServiceImpl implements AlertDefineService {
}
@Override
public List<AlertDefine> getRealTimeAlertDefines() {
List<AlertDefine> alertDefines = CacheFactory.getAlertDefineCache();
public List<AlertDefine> getMetricsRealTimeAlertDefines() {
List<AlertDefine> alertDefines = CacheFactory.getMetricsAlertDefineCache();
if (alertDefines == null) {
alertDefines = alertDefineDao.findAlertDefinesByTypeAndEnableTrue(CommonConstants.ALERT_THRESHOLD_TYPE_REALTIME);
CacheFactory.setAlertDefineCache(alertDefines);
alertDefines = alertDefineDao.findAlertDefinesByTypeAndEnableTrue(CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_REALTIME);
CacheFactory.setMetricsAlertDefineCache(alertDefines);
}
return alertDefines;
}
@Override
public List<AlertDefine> getLogRealTimeAlertDefines() {
List<AlertDefine> alertDefines = CacheFactory.getLogAlertDefineCache();
if (alertDefines == null) {
alertDefines = alertDefineDao.findAlertDefinesByTypeAndEnableTrue(CommonConstants.LOG_ALERT_THRESHOLD_TYPE_REALTIME);
CacheFactory.setLogAlertDefineCache(alertDefines);
}
return alertDefines;
}
@@ -229,11 +240,35 @@ public class AlertDefineServiceImpl implements AlertDefineService {
return Collections.emptyList();
}
switch (type) {
case CommonConstants.ALERT_THRESHOLD_TYPE_PERIODIC:
case CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_PERIODIC:
return dataSourceService.calculate(datasource, expr);
case CommonConstants.LOG_ALERT_THRESHOLD_TYPE_PERIODIC:
// todo support alert expr preview
return dataSourceService.query(datasource, expr);
default:
log.error("Get define preview unsupported type: {}", type);
return Collections.emptyList();
}
}
@Override
public List<AlertDefine> getAlertDefinesByType(String type) {
if (!StringUtils.hasText(type)) {
throw new IllegalArgumentException("Alert definition type cannot be null or empty");
}
switch (type) {
case CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_REALTIME:
case CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_PERIODIC:
case CommonConstants.LOG_ALERT_THRESHOLD_TYPE_REALTIME:
case CommonConstants.LOG_ALERT_THRESHOLD_TYPE_PERIODIC:
// Valid type, proceed with query
break;
default:
throw new IllegalArgumentException("Unsupported alert definition type: " + type);
}
// Query enabled alert definitions by type
return alertDefineDao.findAlertDefinesByTypeAndEnableTrue(type);
}
}
@@ -17,7 +17,9 @@
package org.apache.hertzbeat.alert.service.impl;
import com.fasterxml.jackson.core.type.TypeReference;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.alert.dto.AlibabaCloudSlsExternAlert;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
@@ -33,7 +35,9 @@ import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -46,19 +50,32 @@ public class AlibabaCloudSlsExternAlertService implements ExternAlertService {
private final AlarmCommonReduce alarmCommonReduce;
private static final AlibabaCloudSlsConverter CONVERTER = new AlibabaCloudSlsConverter();
public AlibabaCloudSlsExternAlertService(AlarmCommonReduce alarmCommonReduce) {
this.alarmCommonReduce = alarmCommonReduce;
}
@Override
public void addExternAlert(String content) {
AlibabaCloudSlsExternAlert externAlert = JsonUtil.fromJson(content, AlibabaCloudSlsExternAlert.class);
if (externAlert == null) {
List<AlibabaCloudSlsExternAlert> externAlerts = new ArrayList<>();
if (BooleanUtils.isTrue(JsonUtil.isArray(content))) {
TypeReference<List<AlibabaCloudSlsExternAlert>> typeReference = new TypeReference<>() {};
externAlerts = JsonUtil.fromJson(content, typeReference);
} else {
AlibabaCloudSlsExternAlert externAlert = JsonUtil.fromJson(content, AlibabaCloudSlsExternAlert.class);
if (null != externAlert) {
externAlerts.add(externAlert);
}
}
if (null == externAlerts || externAlerts.isEmpty()) {
log.warn("Failure to parse external alert content. content: {}", content);
return;
}
SingleAlert singleAlert = new AlibabaCloudSlsConverter().convert(externAlert);
alarmCommonReduce.reduceAndSendAlarm(singleAlert);
for (AlibabaCloudSlsExternAlert externAlert : externAlerts) {
SingleAlert singleAlert = CONVERTER.convert(externAlert);
alarmCommonReduce.reduceAndSendAlarm(singleAlert);
}
}
@Override
@@ -19,7 +19,7 @@ package org.apache.hertzbeat.alert.service.impl;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.config.AlibabaSmsProperties;
import org.apache.hertzbeat.common.entity.dto.sms.AlibabaSmsProperties;
import org.apache.hertzbeat.alert.service.SmsClient;
import org.apache.hertzbeat.alert.util.CryptoUtils;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
@@ -236,4 +236,4 @@ public class AlibabaSmsClientImpl implements SmsClient {
public boolean checkConfig() {
return !(accessKeyId.isBlank() || accessKeySecret.isBlank() || signName.isBlank() || templateCode.isBlank());
}
}
}
@@ -20,7 +20,7 @@ package org.apache.hertzbeat.alert.service.impl;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.config.AwsSmsProperties;
import org.apache.hertzbeat.common.entity.dto.sms.AwsSmsProperties;
import org.apache.hertzbeat.alert.service.SmsClient;
import org.apache.hertzbeat.alert.util.CryptoUtils;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
@@ -95,6 +95,29 @@ public class DataSourceServiceImpl implements DataSourceService {
}
}
@Override
public List<Map<String, Object>> query(String datasource, String expr) {
if (!StringUtils.hasText(expr)) {
throw new IllegalArgumentException("Empty expression");
}
if (executors == null || executors.isEmpty()) {
throw new IllegalArgumentException(bundle.getString("alerter.datasource.executor.not.found"));
}
QueryExecutor executor = executors.stream().filter(e -> e.support(datasource)).findFirst().orElse(null);
if (executor == null) {
throw new IllegalArgumentException("Unsupported datasource: " + datasource);
}
// replace all white space
expr = expr.replaceAll("\\s+", " ");
try {
return executor.execute(expr);
} catch (Exception e) {
log.error("Error executing query on datasource {}: {}", datasource, e.getMessage());
throw new RuntimeException("Query execution failed", e);
}
}
private List<Map<String, Object>> evaluate(String expr, QueryExecutor executor) {
CommonTokenStream tokens = tokenStreamCache.get(expr, this::createTokenStream);
AlertExpressionParser parser = new AlertExpressionParser(tokens);
@@ -21,7 +21,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.config.SmslocalSmsProperties;
import org.apache.hertzbeat.common.entity.dto.sms.SmslocalSmsProperties;
import org.apache.hertzbeat.alert.service.SmsClient;
import org.apache.hertzbeat.common.constants.SmsConstants;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
@@ -19,7 +19,7 @@ package org.apache.hertzbeat.alert.service.impl;
import jakarta.xml.bind.DatatypeConverter;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.config.TencentSmsProperties;
import org.apache.hertzbeat.common.entity.dto.sms.TencentSmsProperties;
import org.apache.hertzbeat.alert.service.SmsClient;
import org.apache.hertzbeat.alert.util.CryptoUtils;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
@@ -19,7 +19,7 @@ package org.apache.hertzbeat.alert.service.impl;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.config.TwilioSmsProperties;
import org.apache.hertzbeat.common.entity.dto.sms.TwilioSmsProperties;
import org.apache.hertzbeat.alert.service.SmsClient;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
@@ -168,4 +168,4 @@ public class TwilioSmsClientImpl implements SmsClient {
public boolean checkConfig() {
return !(accountSid.isBlank() || authToken.isBlank() || twilioPhoneNumber.isBlank());
}
}
}
@@ -18,7 +18,7 @@
package org.apache.hertzbeat.alert.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.config.UniSmsProperties;
import org.apache.hertzbeat.common.entity.dto.sms.UniSmsProperties;
import org.apache.hertzbeat.alert.service.SmsClient;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
@@ -190,4 +190,4 @@ public class UniSmsClientImpl implements SmsClient {
return true;
}
}
}
@@ -17,6 +17,8 @@
package org.apache.hertzbeat.alert.util;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -33,10 +35,11 @@ public final class AlertTemplateUtil {
}
/**
* Match the variable ${key}
* Match the variable ${key} or ${key.property.subproperty}
* eg: Alert, the instance: ${instance} metrics: ${metrics} is over flow.
* eg: Log alert: ${log.attributes.level} - ${log.message}
*/
private static final Pattern PATTERN = Pattern.compile("\\$\\{(\\w+)\\}");
private static final Pattern PATTERN = Pattern.compile("\\$\\{([\\w.]+)}");
public static String render(String template, Map<String, Object> replaceData) {
if (!StringUtils.hasText(template)) {
@@ -50,7 +53,8 @@ public final class AlertTemplateUtil {
Matcher matcher = PATTERN.matcher(template);
StringBuilder builder = new StringBuilder();
while (matcher.find()) {
Object objectValue = replaceData.getOrDefault(matcher.group(1), "NullValue");
String propertyPath = matcher.group(1);
Object objectValue = getNestedProperty(replaceData, propertyPath);
String value = objectValue != null ? objectValue.toString() : "NullValue";
matcher.appendReplacement(builder, Matcher.quoteReplacement(value));
}
@@ -61,4 +65,116 @@ public final class AlertTemplateUtil {
return template;
}
}
/**
* Get nested property value from object using property path like "log.attributes.demo"
* @param replaceData the root data map
* @param propertyPath the property path, e.g., "log.attributes.demo"
* @return the property value or null if not found
*/
private static Object getNestedProperty(Map<String, Object> replaceData, String propertyPath) {
if (propertyPath == null || propertyPath.isEmpty()) {
return null;
}
String[] parts = propertyPath.split("\\.");
Object current = replaceData.get(parts[0]);
if (current == null) {
return null;
}
// If only one part, return the direct value
if (parts.length == 1) {
return current;
}
// Navigate through the property path
for (int i = 1; i < parts.length; i++) {
current = getPropertyValue(current, parts[i]);
if (current == null) {
return null;
}
}
return current;
}
/**
* Get property value from an object using reflection
* @param obj the object
* @param propertyName the property name
* @return the property value or null if not found
*/
private static Object getPropertyValue(Object obj, String propertyName) {
if (obj == null || propertyName == null) {
return null;
}
try {
// Handle Map objects
if (obj instanceof Map) {
return ((Map<?, ?>) obj).get(propertyName);
}
Class<?> clazz = obj.getClass();
// Try getter method first (getPropertyName or isPropertyName for boolean)
String getterName = "get" + capitalize(propertyName);
String booleanGetterName = "is" + capitalize(propertyName);
try {
Method getter = clazz.getMethod(getterName);
return getter.invoke(obj);
} catch (NoSuchMethodException e) {
// Try boolean getter
try {
Method booleanGetter = clazz.getMethod(booleanGetterName);
return booleanGetter.invoke(obj);
} catch (NoSuchMethodException ignored) {
// Continue to field access
}
}
// Try direct field access
try {
Field field = clazz.getDeclaredField(propertyName);
field.setAccessible(true);
return field.get(obj);
} catch (NoSuchFieldException ignored) {
// Field not found
}
// Try parent classes
Class<?> superClass = clazz.getSuperclass();
while (superClass != null && !superClass.equals(Object.class)) {
try {
Field field = superClass.getDeclaredField(propertyName);
field.setAccessible(true);
return field.get(obj);
} catch (NoSuchFieldException ignored) {
// Continue with parent class
}
superClass = superClass.getSuperclass();
}
} catch (Exception e) {
log.debug("Failed to access property '{}' on object of type {}: {}",
propertyName, obj.getClass().getSimpleName(), e.getMessage());
}
return null;
}
/**
* Capitalize the first letter of a string
* @param str the input string
* @return the capitalized string
*/
private static String capitalize(String str) {
if (str == null || str.isEmpty()) {
return str;
}
return str.substring(0, 1).toUpperCase() + str.substring(1);
}
}
@@ -52,4 +52,36 @@ class AlerterWorkerPoolTest {
assertEquals(NUMBER_OF_THREADS, counter.get());
}
@Test
void executeNotify() throws InterruptedException {
counter = new AtomicInteger();
latch = new CountDownLatch(NUMBER_OF_THREADS);
for (int i = 0; i < NUMBER_OF_THREADS; i++) {
pool.executeNotify(() -> {
counter.incrementAndGet();
latch.countDown();
});
}
latch.await();
assertEquals(NUMBER_OF_THREADS, counter.get());
}
@Test
void executeLogJob() throws InterruptedException {
counter = new AtomicInteger();
latch = new CountDownLatch(NUMBER_OF_THREADS);
for (int i = 0; i < NUMBER_OF_THREADS; i++) {
pool.executeLogJob(() -> {
counter.incrementAndGet();
latch.countDown();
});
}
latch.await();
assertEquals(NUMBER_OF_THREADS, counter.get());
}
}
@@ -0,0 +1,252 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.calculate.periodic;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.alert.service.DataSourceService;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
/**
* Test case for {@link LogPeriodicAlertCalculator}
*/
@ExtendWith(MockitoExtension.class)
class LogPeriodicAlertCalculatorTest {
@Mock
private DataSourceService dataSourceService;
@Mock
private AlarmCommonReduce alarmCommonReduce;
@InjectMocks
private LogPeriodicAlertCalculator calculator;
private AlertDefine groupAlertDefine;
private AlertDefine individualAlertDefine;
@BeforeEach
void setUp() {
groupAlertDefine = AlertDefine.builder()
.id(1L)
.name("log_error_alert")
.type("periodic_log")
.expr("SELECT * FROM hertzbeat_logs WHERE severity_text = 'ERROR' AND time_unix_nano >= ? AND time_unix_nano <= ?")
.labels(Map.of(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_WARNING, "team", "backend", CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_GROUP))
.annotations(Map.of("summary", "Error logs detected", "description", "Multiple error logs found"))
.template("Found ${severity_text} log: ${body} from ${service_name}")
.datasource("sql")
.enable(true)
.period(300)
.build();
individualAlertDefine = AlertDefine.builder()
.id(1L)
.name("log_error_alert")
.type("periodic_log")
.expr("SELECT * FROM hertzbeat_logs WHERE severity_text = 'ERROR' AND time_unix_nano >= ? AND time_unix_nano <= ?")
.labels(Map.of(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_WARNING, "team", "backend",
CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_INDIVIDUAL))
.annotations(Map.of("summary", "Error logs detected", "description", "Multiple error logs found"))
.template("Found ${severity_text} log: ${body} from ${service_name}")
.datasource("sql")
.enable(true)
.period(300)
.build();
}
@Test
void testCalculateWithEnabledRuleAndValidExpression() {
// Given
List<Map<String, Object>> queryResults = createSampleLogResults(3);
when(dataSourceService.query(anyString(), anyString())).thenReturn(queryResults);
// When
calculator.calculate(groupAlertDefine);
// Then
verify(dataSourceService).query("sql", "SELECT * FROM hertzbeat_logs WHERE severity_text = 'ERROR' AND time_unix_nano >= ? AND time_unix_nano <= ?");
verify(alarmCommonReduce).reduceAndSendAlarmGroup(any(), any());
}
@Test
void testCalculateWithEmptyQueryResults() {
// Given
when(dataSourceService.query(anyString(), anyString())).thenReturn(new ArrayList<>());
// When
calculator.calculate(groupAlertDefine);
// Then
verify(dataSourceService).query(anyString(), anyString());
verifyNoInteractions(alarmCommonReduce);
}
@Test
void testCalculateWithNullQueryResults() {
// Given
when(dataSourceService.query(anyString(), anyString())).thenReturn(null);
// When
calculator.calculate(groupAlertDefine);
// Then
verify(dataSourceService).query(anyString(), anyString());
verifyNoInteractions(alarmCommonReduce);
}
@Test
void testCalculateWithQueryException() {
// Given
when(dataSourceService.query(anyString(), anyString())).thenThrow(new RuntimeException("Database connection failed"));
// When
calculator.calculate(groupAlertDefine);
// Then
verify(dataSourceService).query(anyString(), anyString());
verifyNoInteractions(alarmCommonReduce);
}
@Test
void testIndividualAlertGeneration() {
// Given
Map<String, Object> logContext = createSingleLogResult("Error in payment service", "ERROR");
List<Map<String, Object>> queryResults = List.of(logContext);
when(dataSourceService.query(anyString(), anyString())).thenReturn(queryResults);
try (MockedStatic<org.apache.hertzbeat.alert.util.AlertTemplateUtil> mockedTemplateUtil =
Mockito.mockStatic(org.apache.hertzbeat.alert.util.AlertTemplateUtil.class)) {
mockedTemplateUtil.when(() -> org.apache.hertzbeat.alert.util.AlertTemplateUtil.render(anyString(), any()))
.thenReturn("Rendered alert content");
// When
calculator.calculate(individualAlertDefine);
// Then
ArgumentCaptor<SingleAlert> alertCaptor = ArgumentCaptor.forClass(SingleAlert.class);
verify(alarmCommonReduce).reduceAndSendAlarm(alertCaptor.capture());
SingleAlert capturedAlert = alertCaptor.getValue();
assertAll(
() -> assertEquals(CommonConstants.ALERT_STATUS_FIRING, capturedAlert.getStatus()),
() -> assertEquals(1, capturedAlert.getTriggerTimes()),
() -> assertEquals("Rendered alert content", capturedAlert.getContent()),
() -> assertNotNull(capturedAlert.getStartAt()),
() -> assertNotNull(capturedAlert.getActiveAt()),
() -> assertTrue(capturedAlert.getLabels().containsKey("alertname")),
() -> assertTrue(capturedAlert.getLabels().containsKey("defineid")),
() -> assertEquals("log_error_alert", capturedAlert.getLabels().get("alertname")),
() -> assertEquals("1", capturedAlert.getLabels().get("defineid"))
);
}
}
@Test
void testGroupAlertGeneration() {
// Given
List<Map<String, Object>> queryResults = createSampleLogResults(2);
when(dataSourceService.query(anyString(), anyString())).thenReturn(queryResults);
try (MockedStatic<org.apache.hertzbeat.alert.util.AlertTemplateUtil> mockedTemplateUtil =
Mockito.mockStatic(org.apache.hertzbeat.alert.util.AlertTemplateUtil.class)) {
mockedTemplateUtil.when(() -> org.apache.hertzbeat.alert.util.AlertTemplateUtil.render(anyString(), any()))
.thenReturn("Rendered alert content");
// When
calculator.calculate(groupAlertDefine);
// Then
@SuppressWarnings("unchecked")
ArgumentCaptor<Map<String, String>> groupLabelsCaptor = ArgumentCaptor.forClass(Map.class);
@SuppressWarnings("unchecked")
ArgumentCaptor<List<SingleAlert>> alertsCaptor = ArgumentCaptor.forClass(List.class);
verify(alarmCommonReduce).reduceAndSendAlarmGroup(groupLabelsCaptor.capture(), alertsCaptor.capture());
Map<String, String> groupLabels = groupLabelsCaptor.getValue();
List<SingleAlert> alerts = alertsCaptor.getValue();
assertAll(
() -> assertEquals("2", groupLabels.get("__rows__")),
() -> assertEquals(CommonConstants.ALERT_MODE_GROUP, groupLabels.get(CommonConstants.ALERT_MODE_LABEL)),
() -> assertEquals("log_error_alert", groupLabels.get("alertname")),
() -> assertEquals("1", groupLabels.get("defineid")),
() -> assertEquals(2, alerts.size()),
() -> alerts.forEach(alert -> {
assertEquals(CommonConstants.ALERT_STATUS_FIRING, alert.getStatus());
assertEquals(2, alert.getTriggerTimes());
assertNotNull(alert.getStartAt());
assertNotNull(alert.getActiveAt());
})
);
}
}
/**
* Create sample log results with simplified structure - only key fields
*/
private List<Map<String, Object>> createSampleLogResults(int count) {
List<Map<String, Object>> results = new ArrayList<>();
for (int i = 0; i < count; i++) {
results.add(createSingleLogResult("Error message " + i, "ERROR"));
}
return results;
}
/**
* Create a single log result with simplified structure
*/
private Map<String, Object> createSingleLogResult(String message, String severityText) {
Map<String, Object> logEntry = new HashMap<>();
// Only essential fields for testing
logEntry.put("time_unix_nano", System.nanoTime());
logEntry.put("severity_text", severityText);
logEntry.put("body", message);
logEntry.put("service_name", "payment-service");
return logEntry;
}
}
@@ -15,8 +15,9 @@
* limitations under the License.
*/
package org.apache.hertzbeat.alert.calculate;
package org.apache.hertzbeat.alert.calculate.periodic;
import org.apache.hertzbeat.alert.calculate.AlarmCacheManager;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.alert.service.DataSourceService;
import org.apache.hertzbeat.common.constants.CommonConstants;
@@ -50,10 +51,10 @@ import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
/**
* Test case for {@link PeriodicAlertCalculator}
* Test case for {@link MetricsPeriodicAlertCalculator}
*/
@ExtendWith(MockitoExtension.class)
class PeriodicAlertCalculatorTest {
class MetricsPeriodicAlertCalculatorTest {
@Mock
private DataSourceService dataSourceService;
@@ -65,7 +66,7 @@ class PeriodicAlertCalculatorTest {
private AlarmCacheManager alarmCacheManager;
@InjectMocks
private PeriodicAlertCalculator periodicAlertCalculator;
private MetricsPeriodicAlertCalculator periodicAlertCalculator;
private AlertDefine rule;
@@ -15,10 +15,12 @@
* limitations under the License.
*/
package org.apache.hertzbeat.alert.calculate;
package org.apache.hertzbeat.alert.calculate.realtime;
import com.google.common.collect.Lists;
import org.apache.hertzbeat.alert.AlerterWorkerPool;
import org.apache.hertzbeat.alert.calculate.AlarmCacheManager;
import org.apache.hertzbeat.alert.calculate.JexlExprCalculator;
import org.apache.hertzbeat.alert.dao.SingleAlertDao;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.alert.service.AlertDefineService;
@@ -48,7 +50,7 @@ import static org.mockito.Mockito.when;
/**
*
*/
public class RealTimeAlertCalculatorMatchTest {
public class MetricsRealTimeAlertCalculatorMatchTest {
private final AlerterWorkerPool workerPool = new AlerterWorkerPool();
@@ -67,19 +69,20 @@ public class RealTimeAlertCalculatorMatchTest {
@Mock
private AlarmCacheManager alarmCacheManager;
private RealTimeAlertCalculator realTimeAlertCalculator;
private MetricsRealTimeAlertCalculator metricsRealTimeAlertCalculator;
@BeforeEach
public void setUp() {
MockitoAnnotations.openMocks(this);
when(singleAlertDao.querySingleAlertsByStatus(any())).thenReturn(new ArrayList<>());
realTimeAlertCalculator = new RealTimeAlertCalculator(
metricsRealTimeAlertCalculator = new MetricsRealTimeAlertCalculator(
workerPool,
dataQueue,
alertDefineService,
singleAlertDao,
alarmCommonReduce,
alarmCacheManager,
new JexlExprCalculator(),
false
);
}
@@ -99,7 +102,7 @@ public class RealTimeAlertCalculatorMatchTest {
List<AlertDefine> allDefines = Collections.singletonList(matchDefine);
List<AlertDefine> filtered = realTimeAlertCalculator.filterThresholdsByAppAndMetrics(allDefines, app, "", Map.of(), instanceId, priority);
List<AlertDefine> filtered = metricsRealTimeAlertCalculator.filterThresholdsByAppAndMetrics(allDefines, app, "", Map.of(), instanceId, priority);
// It should filter out 999999999.
assertEquals(1, filtered.size());
@@ -145,10 +148,10 @@ public class RealTimeAlertCalculatorMatchTest {
List<AlertDefine> allDefines = Collections.singletonList(matchDefine);
when(alertDefineService.getRealTimeAlertDefines()).thenReturn(allDefines);
when(alertDefineService.getMetricsRealTimeAlertDefines()).thenReturn(allDefines);
when(dataQueue.pollMetricsDataToAlerter()).thenReturn(metricsData).thenThrow(new InterruptedException());
realTimeAlertCalculator.startCalculate();
metricsRealTimeAlertCalculator.startCalculate();
Thread.sleep(3000);
@@ -189,10 +192,10 @@ public class RealTimeAlertCalculatorMatchTest {
List<AlertDefine> allDefines = Collections.singletonList(matchDefine);
when(alertDefineService.getRealTimeAlertDefines()).thenReturn(allDefines);
when(alertDefineService.getMetricsRealTimeAlertDefines()).thenReturn(allDefines);
when(dataQueue.pollMetricsDataToAlerter()).thenReturn(metricsData).thenThrow(new InterruptedException());
realTimeAlertCalculator.startCalculate();
metricsRealTimeAlertCalculator.startCalculate();
Thread.sleep(3000);
@@ -239,10 +242,10 @@ public class RealTimeAlertCalculatorMatchTest {
List<AlertDefine> allDefines = Collections.singletonList(matchDefine);
when(alertDefineService.getRealTimeAlertDefines()).thenReturn(allDefines);
when(alertDefineService.getMetricsRealTimeAlertDefines()).thenReturn(allDefines);
when(dataQueue.pollMetricsDataToAlerter()).thenReturn(metricsData).thenThrow(new InterruptedException());
realTimeAlertCalculator.startCalculate();
metricsRealTimeAlertCalculator.startCalculate();
Thread.sleep(3000);
@@ -15,11 +15,13 @@
* limitations under the License.
*/
package org.apache.hertzbeat.alert.calculate;
package org.apache.hertzbeat.alert.calculate.realtime;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.apache.hertzbeat.alert.AlerterWorkerPool;
import org.apache.hertzbeat.alert.calculate.AlarmCacheManager;
import org.apache.hertzbeat.alert.calculate.JexlExprCalculator;
import org.apache.hertzbeat.alert.dao.SingleAlertDao;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.alert.service.AlertDefineService;
@@ -35,9 +37,9 @@ import java.util.List;
import java.util.Map;
class RealTimeAlertCalculatorTest {
class MetricsRealTimeAlertCalculatorTest {
private RealTimeAlertCalculator calculator;
private MetricsRealTimeAlertCalculator calculator;
@BeforeEach
void setUp() {
@@ -51,7 +53,8 @@ class RealTimeAlertCalculatorTest {
Mockito.when(mockDao.querySingleAlertsByStatus(Mockito.anyString()))
.thenReturn(Collections.emptyList());
calculator = new RealTimeAlertCalculator(mockPool, mockQueue, mockAlertDefineService, mockDao, mockReduce, alarmCacheManager, false);
calculator = new MetricsRealTimeAlertCalculator(mockPool, mockQueue, mockAlertDefineService
, mockDao, mockReduce, alarmCacheManager, new JexlExprCalculator(), false);
}
@Test
@@ -0,0 +1,189 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.calculate.realtime;
import org.apache.hertzbeat.alert.calculate.realtime.window.LogWorker;
import org.apache.hertzbeat.alert.calculate.realtime.window.TimeService;
import org.apache.hertzbeat.common.entity.log.LogEntry;
import org.apache.hertzbeat.common.queue.CommonDataQueue;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Test case for {@link WindowedLogRealTimeAlertCalculator}
*/
@ExtendWith(MockitoExtension.class)
class WindowedLogRealTimeAlertCalculatorTest {
@Mock
private CommonDataQueue dataQueue;
@Mock
private TimeService timeService;
@Mock
private LogWorker logWorker;
@InjectMocks
private WindowedLogRealTimeAlertCalculator calculator;
private LogEntry validLogEntry;
private LogEntry invalidTimestampLogEntry;
private LogEntry lateDataLogEntry;
@BeforeEach
void setUp() {
// Create valid log entry with current timestamp
long currentTimeNano = System.currentTimeMillis() * 1_000_000L;
validLogEntry = LogEntry.builder()
.timeUnixNano(currentTimeNano)
.severityText("ERROR")
.body("Test error message")
.build();
// Create log entry with invalid timestamp (far future)
long invalidTimeNano = (System.currentTimeMillis() + 2 * 60 * 60 * 1000L) * 1_000_000L; // 2 hours in future
invalidTimestampLogEntry = LogEntry.builder()
.timeUnixNano(invalidTimeNano)
.severityText("ERROR")
.body("Test error message with invalid timestamp")
.build();
// Create late data log entry
long lateTimeNano = (System.currentTimeMillis() - 60 * 60 * 1000L) * 1_000_000L; // 1 hour in past
lateDataLogEntry = LogEntry.builder()
.timeUnixNano(lateTimeNano)
.severityText("ERROR")
.body("Test late data message")
.build();
}
@Test
void testRunWithValidLogEntry() throws InterruptedException {
// Given
when(dataQueue.pollLogEntry()).thenReturn(validLogEntry, (LogEntry) null);
when(timeService.isValidTimestamp(anyLong())).thenReturn(true);
when(timeService.isLateData(anyLong())).thenReturn(false);
// Create a thread to run the calculator
Thread calculatorThread = new Thread(calculator);
// When
calculatorThread.start();
// Give some time for processing
Thread.sleep(100);
calculatorThread.interrupt();
calculatorThread.join(1000);
// Then
verify(dataQueue, atLeastOnce()).pollLogEntry();
verify(timeService).isValidTimestamp(anyLong());
verify(timeService).isLateData(anyLong());
verify(timeService).updateMaxTimestamp(anyLong());
verify(logWorker).reduceAndSendLogTask(validLogEntry);
verify(dataQueue).sendLogEntryToStorage(validLogEntry);
}
@Test
void testRunWithInvalidTimestamp() throws InterruptedException {
// Given
when(dataQueue.pollLogEntry()).thenReturn(invalidTimestampLogEntry, (LogEntry) null);
when(timeService.isValidTimestamp(anyLong())).thenReturn(false);
// Create a thread to run the calculator
Thread calculatorThread = new Thread(calculator);
// When
calculatorThread.start();
// Give some time for processing
Thread.sleep(100);
calculatorThread.interrupt();
calculatorThread.join(1000);
// Then
verify(dataQueue, atLeastOnce()).pollLogEntry();
verify(timeService).isValidTimestamp(anyLong());
verify(timeService, never()).isLateData(anyLong());
verify(timeService, never()).updateMaxTimestamp(anyLong());
verify(logWorker, never()).reduceAndSendLogTask(any());
verify(dataQueue).sendLogEntryToStorage(invalidTimestampLogEntry);
}
@Test
void testRunWithLateData() throws InterruptedException {
// Given
when(dataQueue.pollLogEntry()).thenReturn(lateDataLogEntry, (LogEntry) null);
when(timeService.isValidTimestamp(anyLong())).thenReturn(true);
when(timeService.isLateData(anyLong())).thenReturn(true);
when(timeService.getCurrentWatermark()).thenReturn(System.currentTimeMillis() - 30 * 60 * 1000L); // 30 minutes ago
// Create a thread to run the calculator
Thread calculatorThread = new Thread(calculator);
// When
calculatorThread.start();
// Give some time for processing
Thread.sleep(100);
calculatorThread.interrupt();
calculatorThread.join(1000);
// Then
verify(dataQueue, atLeastOnce()).pollLogEntry();
verify(timeService).isValidTimestamp(anyLong());
verify(timeService).isLateData(anyLong());
verify(timeService, never()).updateMaxTimestamp(anyLong());
verify(logWorker, never()).reduceAndSendLogTask(any());
verify(dataQueue).sendLogEntryToStorage(lateDataLogEntry);
}
@Test
void testRunWithGeneralException() throws InterruptedException {
// Given
when(dataQueue.pollLogEntry()).thenThrow(new RuntimeException("Test runtime exception"))
.thenReturn(null); // Return null to exit the loop
// Create a thread to run the calculator
Thread calculatorThread = new Thread(calculator);
// When
calculatorThread.start();
// Give some time for processing
Thread.sleep(100);
calculatorThread.interrupt();
calculatorThread.join(1000);
// Then
verify(dataQueue, atLeastOnce()).pollLogEntry();
}
}
@@ -0,0 +1,346 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.calculate.realtime.window;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.apache.hertzbeat.common.entity.log.LogEntry;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
/**
* Test case for {@link AlarmEvaluator}
*/
@ExtendWith(MockitoExtension.class)
class AlarmEvaluatorTest {
@Mock
private AlarmCommonReduce alarmCommonReduce;
@InjectMocks
private AlarmEvaluator alarmEvaluator;
private AlertDefine alertDefine;
private WindowAggregator.WindowData windowData;
@BeforeEach
void setUp() {
// Create test log entry
LogEntry logEntry = LogEntry.builder()
.timeUnixNano(System.currentTimeMillis() * 1_000_000L)
.severityText("ERROR")
.body("Test error message")
.attributes(Map.of("service", "test-service", "pod", "test-pod"))
.resource(Map.of("cluster", "test-cluster"))
.build();
// Create test alert define for individual mode
alertDefine = AlertDefine.builder()
.id(1L)
.name("test_log_alert")
.type("realtime_log")
.expr("log.severityText == 'ERROR'")
.times(1)
.template("Alert: {{log.body}}")
.labels(Map.of(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_INDIVIDUAL, CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_CRITICAL))
.annotations(Map.of("summary", "Test alert summary"))
.enable(true)
.build();
// Create matching log events
MatchingLogEvent matchingEvent = MatchingLogEvent.builder()
.logEntry(logEntry)
.alertDefine(alertDefine)
.eventTimestamp(System.currentTimeMillis())
.workerTimestamp(System.currentTimeMillis())
.build();
// Create window data
WindowAggregator.WindowKey windowKey = new WindowAggregator.WindowKey(1L,
System.currentTimeMillis() - 60000, System.currentTimeMillis());
windowData = new WindowAggregator.WindowData(windowKey, alertDefine);
windowData.addMatchingLog(matchingEvent);
}
@Test
void testProcessWindowDataWithIndividualMode() throws InterruptedException {
// Given - alert define with individual mode
alertDefine.setLabels(Map.of(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_INDIVIDUAL));
// When
alarmEvaluator.sendAndProcessWindowData(windowData);
// Wait for async processing
Thread.sleep(2000);
// Then
verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any(SingleAlert.class));
verify(alarmCommonReduce, never()).reduceAndSendAlarmGroup(anyMap(), anyList());
}
@Test
void testProcessWindowDataWithGroupMode() throws InterruptedException {
// Given - alert define with group mode
alertDefine.setLabels(Map.of(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_GROUP));
// When
alarmEvaluator.sendAndProcessWindowData(windowData);
// Wait for async processing
Thread.sleep(2000);
// Then
verify(alarmCommonReduce, never()).reduceAndSendAlarm(any(SingleAlert.class));
verify(alarmCommonReduce, times(1)).reduceAndSendAlarmGroup(anyMap(), anyList());
}
@Test
void testProcessWindowDataWithEmptyLogs() throws InterruptedException {
// Given - window data with no matching logs
WindowAggregator.WindowKey emptyWindowKey = new WindowAggregator.WindowKey(1L,
System.currentTimeMillis() - 60000, System.currentTimeMillis());
WindowAggregator.WindowData emptyWindowData = new WindowAggregator.WindowData(emptyWindowKey, alertDefine);
// When
alarmEvaluator.sendAndProcessWindowData(emptyWindowData);
// Wait for async processing
Thread.sleep(2000);
// Then - should not process any alerts
verify(alarmCommonReduce, never()).reduceAndSendAlarm(any(SingleAlert.class));
verify(alarmCommonReduce, never()).reduceAndSendAlarmGroup(anyMap(), anyList());
}
@Test
void testProcessWindowDataWithInsufficientTimes() throws InterruptedException {
// Given - alert define requiring 3 times but only 1 matching log
alertDefine.setTimes(3);
// When
alarmEvaluator.sendAndProcessWindowData(windowData);
// Wait for async processing
Thread.sleep(2000);
// Then - should not process any alerts due to insufficient count
verify(alarmCommonReduce, never()).reduceAndSendAlarm(any(SingleAlert.class));
verify(alarmCommonReduce, never()).reduceAndSendAlarmGroup(anyMap(), anyList());
}
@Test
void testIndividualAlertGeneration() throws InterruptedException {
// Given
alertDefine.setLabels(Map.of(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_INDIVIDUAL, CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_CRITICAL));
alertDefine.setAnnotations(Map.of("summary", "Test summary", "description", "Test description"));
// When
alarmEvaluator.sendAndProcessWindowData(windowData);
// Wait for async processing
Thread.sleep(2000);
// Then
ArgumentCaptor<SingleAlert> alertCaptor = ArgumentCaptor.forClass(SingleAlert.class);
verify(alarmCommonReduce).reduceAndSendAlarm(alertCaptor.capture());
SingleAlert capturedAlert = alertCaptor.getValue();
assertAll(
() -> assertNotNull(capturedAlert),
() -> assertEquals(CommonConstants.ALERT_STATUS_FIRING, capturedAlert.getStatus()),
() -> assertEquals(1, capturedAlert.getTriggerTimes()),
() -> assertNotNull(capturedAlert.getContent()),
() -> assertNotNull(capturedAlert.getLabels()),
() -> assertNotNull(capturedAlert.getAnnotations()),
() -> assertTrue(capturedAlert.getStartAt() > 0),
() -> assertTrue(capturedAlert.getActiveAt() > 0)
);
// Verify labels contain alert define info and log entry data
Map<String, String> labels = capturedAlert.getLabels();
assertTrue(labels.containsKey(CommonConstants.LABEL_ALERT_SEVERITY));
assertTrue(labels.containsKey("severityText"));
assertTrue(labels.containsKey("body"));
}
@Test
void testGroupAlertGeneration() throws InterruptedException {
// Given
alertDefine.setLabels(Map.of(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_GROUP, CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_WARNING));
// Add more matching logs to test group functionality
MatchingLogEvent secondEvent = MatchingLogEvent.builder()
.logEntry(LogEntry.builder()
.timeUnixNano((System.currentTimeMillis() + 1000) * 1_000_000L)
.severityText("ERROR")
.body("Second error message")
.build())
.alertDefine(alertDefine)
.eventTimestamp(System.currentTimeMillis() + 1000)
.workerTimestamp(System.currentTimeMillis() + 1000)
.build();
windowData.addMatchingLog(secondEvent);
// When
alarmEvaluator.sendAndProcessWindowData(windowData);
// Wait for async processing
Thread.sleep(2000);
// Then
@SuppressWarnings("unchecked")
ArgumentCaptor<Map<String, String>> labelsCaptor = ArgumentCaptor.forClass(Map.class);
@SuppressWarnings("unchecked")
ArgumentCaptor<List<SingleAlert>> alertsCaptor = ArgumentCaptor.forClass(List.class);
verify(alarmCommonReduce).reduceAndSendAlarmGroup(labelsCaptor.capture(), alertsCaptor.capture());
Map<String, String> groupLabels = labelsCaptor.getValue();
List<SingleAlert> alerts = alertsCaptor.getValue();
assertAll(
() -> assertNotNull(groupLabels),
() -> assertNotNull(alerts),
() -> assertEquals(2, alerts.size()),
() -> assertTrue(groupLabels.containsKey("window_start_time")),
() -> assertTrue(groupLabels.containsKey("window_end_time")),
() -> assertTrue(groupLabels.containsKey("matching_logs_count")),
() -> assertEquals(CommonConstants.ALERT_MODE_GROUP, groupLabels.get(CommonConstants.ALERT_MODE_LABEL)),
() -> assertEquals("2", groupLabels.get("matching_logs_count"))
);
// Verify each alert in the group
for (SingleAlert alert : alerts) {
assertEquals(CommonConstants.ALERT_STATUS_FIRING, alert.getStatus());
assertEquals(2, alert.getTriggerTimes()); // Should equal the count of matching logs
}
}
@Test
void testLogEntryDataMapping() throws InterruptedException {
// Given - log entry with comprehensive data
LogEntry detailedLogEntry = LogEntry.builder()
.timeUnixNano(System.currentTimeMillis() * 1_000_000L)
.severityNumber(17) // ERROR level
.severityText("ERROR")
.body("Detailed error message")
.attributes(Map.of("service", "api-service", "version", "1.0.0"))
.resource(Map.of("cluster", "prod-cluster", "namespace", "default"))
.droppedAttributesCount(0)
.traceId("trace123")
.spanId("span456")
.traceFlags(1)
.build();
MatchingLogEvent detailedEvent = MatchingLogEvent.builder()
.logEntry(detailedLogEntry)
.alertDefine(alertDefine)
.eventTimestamp(System.currentTimeMillis())
.workerTimestamp(System.currentTimeMillis())
.build();
WindowAggregator.WindowData detailedWindowData = new WindowAggregator.WindowData(
new WindowAggregator.WindowKey(1L, System.currentTimeMillis() - 60000, System.currentTimeMillis()),
alertDefine);
detailedWindowData.addMatchingLog(detailedEvent);
alertDefine.setLabels(Map.of(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_INDIVIDUAL));
// When
alarmEvaluator.sendAndProcessWindowData(detailedWindowData);
Thread.sleep(2000);
// Then
ArgumentCaptor<SingleAlert> alertCaptor = ArgumentCaptor.forClass(SingleAlert.class);
verify(alarmCommonReduce).reduceAndSendAlarm(alertCaptor.capture());
SingleAlert alert = alertCaptor.getValue();
Map<String, String> labels = alert.getLabels();
// Verify log entry fields are mapped to labels
assertAll(
() -> assertEquals("17", labels.get("severityNumber")),
() -> assertEquals("ERROR", labels.get("severityText")),
() -> assertEquals("Detailed error message", labels.get("body")),
() -> assertEquals("0", labels.get("droppedAttributesCount")),
() -> assertEquals("trace123", labels.get("traceId")),
() -> assertEquals("span456", labels.get("spanId")),
() -> assertEquals("1", labels.get("traceFlags")),
() -> assertEquals("api-service", labels.get("attr_service")),
() -> assertEquals("1.0.0", labels.get("attr_version")),
() -> assertEquals("prod-cluster", labels.get("resource_cluster")),
() -> assertEquals("default", labels.get("resource_namespace"))
);
}
@Test
void testMultipleMatchingLogsInWindow() throws InterruptedException {
// Given - multiple matching logs in the same window
alertDefine.setTimes(2); // Require at least 2 occurrences
alertDefine.setLabels(Map.of(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_GROUP));
// Add second matching log
MatchingLogEvent secondEvent = MatchingLogEvent.builder()
.logEntry(LogEntry.builder()
.severityText("ERROR")
.body("Another error")
.build())
.alertDefine(alertDefine)
.eventTimestamp(System.currentTimeMillis() + 5000)
.workerTimestamp(System.currentTimeMillis() + 5000)
.build();
windowData.addMatchingLog(secondEvent);
// When
alarmEvaluator.sendAndProcessWindowData(windowData);
Thread.sleep(2000);
// Then - should trigger group alert since threshold is met
verify(alarmCommonReduce).reduceAndSendAlarmGroup(anyMap(), anyList());
@SuppressWarnings("unchecked")
ArgumentCaptor<List<SingleAlert>> alertsCaptor = ArgumentCaptor.forClass(List.class);
verify(alarmCommonReduce).reduceAndSendAlarmGroup(anyMap(), alertsCaptor.capture());
List<SingleAlert> alerts = alertsCaptor.getValue();
assertEquals(2, alerts.size());
assertEquals(2, alerts.get(0).getTriggerTimes()); // Each alert should have trigger times = total count
}
}
@@ -0,0 +1,239 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.calculate.realtime.window;
import org.apache.hertzbeat.alert.AlerterWorkerPool;
import org.apache.hertzbeat.alert.calculate.JexlExprCalculator;
import org.apache.hertzbeat.alert.service.AlertDefineService;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.entity.log.LogEntry;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Test case for {@link LogWorker}
*/
@ExtendWith(MockitoExtension.class)
class LogWorkerTest {
@Mock
private AlertDefineService alertDefineService;
@Mock
private JexlExprCalculator jexlExprCalculator;
@Mock
private WindowAggregator windowAggregator;
@Mock
private AlerterWorkerPool workerPool;
@InjectMocks
private LogWorker logWorker;
private LogEntry logEntry;
private AlertDefine alertDefine;
private List<AlertDefine> alertDefines;
@BeforeEach
void setUp() {
// Create test log entry
logEntry = LogEntry.builder()
.timeUnixNano(System.currentTimeMillis() * 1_000_000L)
.severityText("ERROR")
.body("Test error message")
.build();
// Create test alert define
alertDefine = AlertDefine.builder()
.id(1L)
.name("test_log_alert")
.type("realtime_log")
.expr("log.severityText == 'ERROR'")
.enable(true)
.build();
alertDefines = new ArrayList<>();
alertDefines.add(alertDefine);
}
@Test
void testReduceAndSendLogTask() {
// When
logWorker.reduceAndSendLogTask(logEntry);
// Then
verify(workerPool).executeLogJob(any(Runnable.class));
}
@Test
void testReduceLogTask() {
// Given
when(alertDefineService.getLogRealTimeAlertDefines()).thenReturn(alertDefines);
when(jexlExprCalculator.execAlertExpression(anyMap(), anyString(), eq(false))).thenReturn(true);
// When
Runnable task = logWorker.reduceLogTask(logEntry);
// Execute the task
task.run();
// Then
verify(alertDefineService).getLogRealTimeAlertDefines();
verify(jexlExprCalculator).execAlertExpression(anyMap(), eq("log.severityText == 'ERROR'"), eq(false));
verify(windowAggregator).addMatchingLog(any(MatchingLogEvent.class));
}
@Test
void testProcessLogEntryWithMatchingExpression() {
// Given
when(alertDefineService.getLogRealTimeAlertDefines()).thenReturn(alertDefines);
when(jexlExprCalculator.execAlertExpression(anyMap(), anyString(), eq(false))).thenReturn(true);
// When
Runnable task = logWorker.reduceLogTask(logEntry);
task.run();
// Then
@SuppressWarnings("unchecked")
ArgumentCaptor<Map<String, Object>> contextCaptor = ArgumentCaptor.forClass(Map.class);
verify(jexlExprCalculator).execAlertExpression(contextCaptor.capture(), eq("log.severityText == 'ERROR'"), eq(false));
Map<String, Object> context = contextCaptor.getValue();
assertEquals(logEntry, context.get("log"));
ArgumentCaptor<MatchingLogEvent> eventCaptor = ArgumentCaptor.forClass(MatchingLogEvent.class);
verify(windowAggregator).addMatchingLog(eventCaptor.capture());
MatchingLogEvent capturedEvent = eventCaptor.getValue();
assertEquals(logEntry, capturedEvent.getLogEntry());
assertEquals(alertDefine, capturedEvent.getAlertDefine());
assertTrue(capturedEvent.getEventTimestamp() > 0);
assertTrue(capturedEvent.getWorkerTimestamp() > 0);
}
@Test
void testProcessLogEntryWithNonMatchingExpression() {
// Given
when(alertDefineService.getLogRealTimeAlertDefines()).thenReturn(alertDefines);
when(jexlExprCalculator.execAlertExpression(anyMap(), anyString(), eq(false))).thenReturn(false);
// When
Runnable task = logWorker.reduceLogTask(logEntry);
task.run();
// Then
verify(jexlExprCalculator).execAlertExpression(anyMap(), eq("log.severityText == 'ERROR'"), eq(false));
verify(windowAggregator, never()).addMatchingLog(any(MatchingLogEvent.class));
}
@Test
void testProcessLogEntryWithMultipleAlertDefines() {
// Given
AlertDefine secondDefine = AlertDefine.builder()
.id(2L)
.name("second_alert")
.type("realtime_log")
.expr("log.body != null")
.enable(true)
.build();
List<AlertDefine> multipleDefines = List.of(alertDefine, secondDefine);
when(alertDefineService.getLogRealTimeAlertDefines()).thenReturn(multipleDefines);
when(jexlExprCalculator.execAlertExpression(anyMap(), eq("log.severityText == 'ERROR'"), eq(false))).thenReturn(true);
when(jexlExprCalculator.execAlertExpression(anyMap(), eq("log.body != null"), eq(false))).thenReturn(true);
// When
Runnable task = logWorker.reduceLogTask(logEntry);
task.run();
// Then
verify(jexlExprCalculator).execAlertExpression(anyMap(), eq("log.severityText == 'ERROR'"), eq(false));
verify(jexlExprCalculator).execAlertExpression(anyMap(), eq("log.body != null"), eq(false));
verify(windowAggregator, times(2)).addMatchingLog(any(MatchingLogEvent.class));
}
@Test
void testCreateMatchingLogEventStructure() {
// Given
when(alertDefineService.getLogRealTimeAlertDefines()).thenReturn(alertDefines);
when(jexlExprCalculator.execAlertExpression(anyMap(), anyString(), eq(false))).thenReturn(true);
// When
Runnable task = logWorker.reduceLogTask(logEntry);
task.run();
// Then
ArgumentCaptor<MatchingLogEvent> eventCaptor = ArgumentCaptor.forClass(MatchingLogEvent.class);
verify(windowAggregator).addMatchingLog(eventCaptor.capture());
MatchingLogEvent event = eventCaptor.getValue();
assertAll(
() -> assertNotNull(event.getLogEntry()),
() -> assertNotNull(event.getAlertDefine()),
() -> assertTrue(event.getEventTimestamp() > 0),
() -> assertTrue(event.getWorkerTimestamp() > 0),
() -> assertEquals(logEntry, event.getLogEntry()),
() -> assertEquals(alertDefine, event.getAlertDefine()),
() -> assertTrue(event.getWorkerTimestamp() >= event.getEventTimestamp() - 1000)
);
}
@Test
void testLogPrefixInContext() {
// Given
when(alertDefineService.getLogRealTimeAlertDefines()).thenReturn(alertDefines);
when(jexlExprCalculator.execAlertExpression(anyMap(), anyString(), eq(false))).thenReturn(false);
// When
Runnable task = logWorker.reduceLogTask(logEntry);
task.run();
// Then
@SuppressWarnings("unchecked")
ArgumentCaptor<Map<String, Object>> contextCaptor = ArgumentCaptor.forClass(Map.class);
verify(jexlExprCalculator).execAlertExpression(contextCaptor.capture(), anyString(), eq(false));
Map<String, Object> context = contextCaptor.getValue();
assertTrue(context.containsKey("log"));
assertEquals(logEntry, context.get("log"));
}
}
@@ -0,0 +1,291 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.calculate.realtime.window;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Test case for {@link TimeService}
*/
@ExtendWith(MockitoExtension.class)
class TimeServiceTest {
@Mock
private TimeService.WatermarkListener mockListener1;
@Mock
private TimeService.WatermarkListener mockListener2;
private TimeService timeService;
@BeforeEach
void setUp() {
timeService = new TimeService(Collections.emptyList());
}
@AfterEach
void tearDown() {
if (timeService != null) {
timeService.stop();
}
}
@Test
void testIsValidTimestamp() {
// Given
long currentTime = System.currentTimeMillis();
long validFutureTime = currentTime + 30_000; // 30 seconds in future
long validPastTime = currentTime - 60_000; // 1 minute in past
long invalidFutureTime = currentTime + 120_000; // 2 minutes in future (exceeds 1 minute limit)
long invalidPastTime = currentTime - 25 * 60 * 60 * 1000; // 25 hours in past (exceeds 24 hours limit)
// When & Then
assertAll(
() -> assertTrue(timeService.isValidTimestamp(currentTime)),
() -> assertTrue(timeService.isValidTimestamp(validFutureTime)),
() -> assertTrue(timeService.isValidTimestamp(validPastTime)),
() -> assertFalse(timeService.isValidTimestamp(invalidFutureTime)),
() -> assertFalse(timeService.isValidTimestamp(invalidPastTime))
);
}
@Test
void testIsLateData() {
// Given
long currentTime = System.currentTimeMillis();
timeService.updateMaxTimestamp(currentTime);
timeService.start();
// Wait for watermark to be set
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// When & Then
long currentWatermark = timeService.getCurrentWatermark();
if (currentWatermark > 0) {
assertTrue(timeService.isLateData(currentWatermark - 1000));
assertFalse(timeService.isLateData(currentWatermark + 1000));
}
}
@Test
void testUpdateMaxTimestamp() {
// Given
long timestamp1 = 1000L;
long timestamp2 = 2000L;
long timestamp3 = 1500L;
// When
timeService.updateMaxTimestamp(timestamp1);
timeService.updateMaxTimestamp(timestamp2);
timeService.updateMaxTimestamp(timestamp3);
timeService.start();
// Wait for potential watermark broadcast
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// Then - The max timestamp should be the highest value (timestamp2)
assertEquals(timestamp2, timeService.getMaxTimestamp());
}
@Test
void testWatermarkBroadcastWithRealTimestamps() throws InterruptedException {
// Given
CountDownLatch latch = new CountDownLatch(1);
AtomicLong receivedWatermark = new AtomicLong();
TimeService.WatermarkListener testListener = watermark -> {
receivedWatermark.set(watermark.getTimestamp());
latch.countDown();
};
timeService.addWatermarkListener(testListener);
timeService.start();
// When
long testTimestamp = System.currentTimeMillis();
timeService.updateMaxTimestamp(testTimestamp);
// Then
assertTrue(latch.await(10, TimeUnit.SECONDS), "Should receive watermark within 10 seconds");
long watermark = receivedWatermark.get();
long expectedWatermark = testTimestamp - TimeService.DEFAULT_WATERMARK_DELAY_MS; // DEFAULT_WATERMARK_DELAY_MS
assertEquals(expectedWatermark, watermark);
}
@Test
void testWatermarkMonotonicProperty() throws InterruptedException {
// Given
List<Long> receivedWatermarks = Collections.synchronizedList(new ArrayList<>());
CountDownLatch latch = new CountDownLatch(2);
TimeService.WatermarkListener testListener = watermark -> {
receivedWatermarks.add(watermark.getTimestamp());
latch.countDown();
};
timeService.addWatermarkListener(testListener);
timeService.start();
// When - Update with increasing timestamps
long baseTime = System.currentTimeMillis();
timeService.updateMaxTimestamp(baseTime);
Thread.sleep(6000); // Wait for first watermark
timeService.updateMaxTimestamp(baseTime + 40_000); // Increase by 40 seconds
// Then
assertTrue(latch.await(15, TimeUnit.SECONDS), "Should receive at least 2 watermarks");
// Verify watermarks are monotonic (non-decreasing)
for (int i = 1; i < receivedWatermarks.size(); i++) {
assertTrue(receivedWatermarks.get(i) >= receivedWatermarks.get(i - 1),
"Watermarks should be monotonic: " + receivedWatermarks);
}
}
@Test
void testListenerExceptionHandling() throws InterruptedException {
// Given
AtomicInteger successfulCalls = new AtomicInteger(0);
CountDownLatch latch = new CountDownLatch(1);
// Mock listener that throws exception
TimeService.WatermarkListener faultyListener = watermark -> {
throw new RuntimeException("Test exception");
};
// Mock listener that works normally
TimeService.WatermarkListener normalListener = watermark -> {
successfulCalls.incrementAndGet();
latch.countDown();
};
timeService.addWatermarkListener(faultyListener);
timeService.addWatermarkListener(normalListener);
timeService.start();
// When
timeService.updateMaxTimestamp(System.currentTimeMillis());
// Then - Normal listener should still work despite faulty listener
assertTrue(latch.await(10, TimeUnit.SECONDS), "Normal listener should receive watermark");
assertTrue(successfulCalls.get() > 0, "Normal listener should be called successfully");
}
@Test
void testGetCurrentWatermark() {
// Given
timeService.start();
// Initially watermark should be 0
assertEquals(0, timeService.getCurrentWatermark());
// When
long testTimestamp = System.currentTimeMillis();
timeService.updateMaxTimestamp(testTimestamp);
// Wait for watermark update
try {
Thread.sleep(6000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// Then
long watermark = timeService.getCurrentWatermark();
assertEquals(testTimestamp - TimeService.DEFAULT_WATERMARK_DELAY_MS, watermark);
}
@Test
void testZeroTimestampHandling() throws InterruptedException {
// Given
CountDownLatch latch = new CountDownLatch(1);
AtomicInteger callCount = new AtomicInteger(0);
TimeService.WatermarkListener testListener = watermark -> {
callCount.incrementAndGet();
latch.countDown();
};
timeService.addWatermarkListener(testListener);
timeService.start();
// When - Update with zero timestamp
timeService.updateMaxTimestamp(0);
// Wait for potential watermark broadcast
boolean received = latch.await(8, TimeUnit.SECONDS);
// Then - No watermark should be broadcast for zero timestamp
assertFalse(received, "No watermark should be broadcast for zero timestamp");
assertEquals(0, callCount.get());
}
@Test
void testNegativeTimestampHandling() throws InterruptedException {
// Given
CountDownLatch latch = new CountDownLatch(1);
AtomicInteger callCount = new AtomicInteger(0);
TimeService.WatermarkListener testListener = watermark -> {
callCount.incrementAndGet();
latch.countDown();
};
timeService.addWatermarkListener(testListener);
timeService.start();
// When - Update with negative timestamp
timeService.updateMaxTimestamp(-1000L);
// Wait for potential watermark broadcast
boolean received = latch.await(8, TimeUnit.SECONDS);
// Then - No watermark should be broadcast for negative timestamp
assertFalse(received, "No watermark should be broadcast for negative timestamp");
assertEquals(0, callCount.get());
}
}
@@ -0,0 +1,239 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.calculate.realtime.window;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.entity.log.LogEntry;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
/**
* Test case for {@link WindowAggregator}
*/
@ExtendWith(MockitoExtension.class)
class WindowAggregatorTest {
@Mock
private AlarmEvaluator alarmEvaluator;
@InjectMocks
private WindowAggregator windowAggregator;
private AlertDefine alertDefine;
private LogEntry logEntry;
@BeforeEach
void setUp() {
// Create test log entry
logEntry = LogEntry.builder()
.timeUnixNano(System.currentTimeMillis() * 1_000_000L)
.severityText("ERROR")
.body("Test error message")
.attributes(Map.of("service", "test-service"))
.build();
// Create test alert define
alertDefine = AlertDefine.builder()
.id(1L)
.name("test_log_alert")
.type("realtime_log")
.expr("log.severityText == 'ERROR'")
.period(60) // 60 seconds window
.times(1)
.enable(true)
.build();
// Stop the default aggregator to avoid conflicts
windowAggregator.stop();
}
@AfterEach
void tearDown() {
if (windowAggregator != null) {
windowAggregator.stop();
}
}
@Test
void testWatermarkProcessing() throws InterruptedException {
// Given - add a matching log event first
long eventTime = System.currentTimeMillis();
long windowSize = 60 * 1000; // 60 seconds
long windowStart = (eventTime / windowSize) * windowSize;
long windowEnd = windowStart + windowSize;
MatchingLogEvent testEvent = MatchingLogEvent.builder()
.logEntry(logEntry)
.alertDefine(alertDefine)
.eventTimestamp(eventTime)
.workerTimestamp(eventTime)
.build();
windowAggregator.start();
windowAggregator.addMatchingLog(testEvent);
Thread.sleep(100); // Allow processing
// When - send watermark that should close the window
TimeService.Watermark watermark = new TimeService.Watermark(windowEnd + 1000);
windowAggregator.onWatermark(watermark);
// Then - the window should be closed and sent to AlarmEvaluator
verify(alarmEvaluator, timeout(1000)).sendAndProcessWindowData(any(WindowAggregator.WindowData.class));
}
@Test
void testWatermarkNotClosingActiveWindow() throws InterruptedException {
// Given - add a matching log event
long eventTime = System.currentTimeMillis();
MatchingLogEvent testEvent = MatchingLogEvent.builder()
.logEntry(logEntry)
.alertDefine(alertDefine)
.eventTimestamp(eventTime)
.workerTimestamp(eventTime)
.build();
windowAggregator.start();
windowAggregator.addMatchingLog(testEvent);
Thread.sleep(100);
// When - send watermark that should NOT close the window (watermark before window end)
TimeService.Watermark earlyWatermark = new TimeService.Watermark(eventTime - 1000);
windowAggregator.onWatermark(earlyWatermark);
// Give some time for potential processing
Thread.sleep(100);
// Then - no window should be closed
verify(alarmEvaluator, never()).sendAndProcessWindowData(any(WindowAggregator.WindowData.class));
}
@Test
void testMultipleWindowsWithDifferentAlertDefines() throws InterruptedException {
// Given - two different alert defines
AlertDefine alertDefine2 = AlertDefine.builder()
.id(2L)
.name("second_alert")
.type("realtime_log")
.expr("log.severityText == 'WARN'")
.period(60)
.times(1)
.enable(true)
.build();
long eventTime = System.currentTimeMillis();
MatchingLogEvent event1 = MatchingLogEvent.builder()
.logEntry(logEntry)
.alertDefine(alertDefine)
.eventTimestamp(eventTime)
.workerTimestamp(eventTime)
.build();
MatchingLogEvent event2 = MatchingLogEvent.builder()
.logEntry(logEntry)
.alertDefine(alertDefine2)
.eventTimestamp(eventTime)
.workerTimestamp(eventTime)
.build();
// When
windowAggregator.start();
windowAggregator.addMatchingLog(event1);
windowAggregator.addMatchingLog(event2);
Thread.sleep(100);
// Send watermark to close both windows
long windowSize = 60 * 1000;
long windowStart = (eventTime / windowSize) * windowSize;
long windowEnd = windowStart + windowSize;
TimeService.Watermark watermark = new TimeService.Watermark(windowEnd + 1000);
windowAggregator.onWatermark(watermark);
// Then - both windows should be closed
verify(alarmEvaluator, timeout(1000).times(2)).sendAndProcessWindowData(any(WindowAggregator.WindowData.class));
}
@Test
void testMultipleStartCalls() {
// When - call start multiple times
windowAggregator.start(); // First call (already called in setUp)
windowAggregator.start(); // Second call
// Then - should handle multiple start calls gracefully
assertNotNull(windowAggregator);
}
@Test
void testWatermarkWithMultipleWindows() throws InterruptedException {
// Given - multiple windows at different times
long baseTime = System.currentTimeMillis();
long windowSize = 60 * 1000; // 60 seconds
// Event in first window
MatchingLogEvent event1 = MatchingLogEvent.builder()
.logEntry(logEntry)
.alertDefine(alertDefine)
.eventTimestamp(baseTime)
.workerTimestamp(baseTime)
.build();
// Event in second window (1 minute later)
MatchingLogEvent event2 = MatchingLogEvent.builder()
.logEntry(logEntry)
.alertDefine(alertDefine)
.eventTimestamp(baseTime + windowSize + 5000)
.workerTimestamp(baseTime + windowSize + 5000)
.build();
windowAggregator.start();
windowAggregator.addMatchingLog(event1);
windowAggregator.addMatchingLog(event2);
Thread.sleep(100);
// When - send watermark that closes only the first window
long firstWindowStart = (baseTime / windowSize) * windowSize;
long firstWindowEnd = firstWindowStart + windowSize;
TimeService.Watermark watermark = new TimeService.Watermark(firstWindowEnd + 1000);
windowAggregator.onWatermark(watermark);
// Then - only first window should be closed
verify(alarmEvaluator, timeout(1000).times(1)).sendAndProcessWindowData(any(WindowAggregator.WindowData.class));
// When - send watermark that closes the second window
long secondWindowStart = ((baseTime + windowSize + 5000) / windowSize) * windowSize;
long secondWindowEnd = secondWindowStart + windowSize;
TimeService.Watermark secondWatermark = new TimeService.Watermark(secondWindowEnd + 1000);
windowAggregator.onWatermark(secondWatermark);
// Then - second window should also be closed (total 2 windows)
verify(alarmEvaluator, timeout(1000).times(2)).sendAndProcessWindowData(any(WindowAggregator.WindowData.class));
}
}
@@ -27,7 +27,7 @@ import static org.mockito.Mockito.when;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Properties;
import org.apache.hertzbeat.alert.dto.MailServerConfig;
import org.apache.hertzbeat.common.entity.dto.MailServerConfig;
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
@@ -18,15 +18,18 @@
package org.apache.hertzbeat.alert.service;
import com.google.common.collect.Lists;
import org.apache.hertzbeat.alert.calculate.PeriodicAlertRuleScheduler;
import org.apache.hertzbeat.alert.calculate.periodic.PeriodicAlertRuleScheduler;
import org.apache.hertzbeat.alert.dao.AlertDefineDao;
import org.apache.hertzbeat.alert.service.impl.AlertDefineServiceImpl;
import org.apache.hertzbeat.common.cache.CacheFactory;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
@@ -39,8 +42,10 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.apache.hertzbeat.common.constants.CommonConstants.ALERT_THRESHOLD_TYPE_PERIODIC;
import static org.apache.hertzbeat.common.constants.CommonConstants.ALERT_THRESHOLD_TYPE_REALTIME;
import static org.apache.hertzbeat.common.constants.CommonConstants.LOG_ALERT_THRESHOLD_TYPE_PERIODIC;
import static org.apache.hertzbeat.common.constants.CommonConstants.LOG_ALERT_THRESHOLD_TYPE_REALTIME;
import static org.apache.hertzbeat.common.constants.CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_PERIODIC;
import static org.apache.hertzbeat.common.constants.CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_REALTIME;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -100,19 +105,19 @@ class AlertDefineServiceTest {
@Test
void addAlertDefine() {
assertDoesNotThrow(() -> alertDefineService.addAlertDefine(alertDefine));
assertDoesNotThrow(() -> this.alertDefineService.addAlertDefine(alertDefine));
when(alertDefineDao.saveAndFlush(alertDefine)).thenThrow(new RuntimeException());
assertThrows(RuntimeException.class, () -> alertDefineService.addAlertDefine(alertDefine));
assertThrows(RuntimeException.class, () -> this.alertDefineService.addAlertDefine(alertDefine));
}
@Test
void modifyAlertDefine() {
AlertDefine alertDefine = AlertDefine.builder().id(1L).build();
when(alertDefineDao.saveAndFlush(alertDefine)).thenReturn(alertDefine);
assertDoesNotThrow(() -> alertDefineService.modifyAlertDefine(alertDefine));
assertDoesNotThrow(() -> this.alertDefineService.modifyAlertDefine(alertDefine));
reset();
when(alertDefineDao.saveAndFlush(alertDefine)).thenThrow(new RuntimeException());
assertThrows(RuntimeException.class, () -> alertDefineService.modifyAlertDefine(alertDefine));
assertThrows(RuntimeException.class, () -> this.alertDefineService.modifyAlertDefine(alertDefine));
}
@Test
@@ -164,15 +169,171 @@ class AlertDefineServiceTest {
}
};
when(dataSourceService.calculate(eq("promql"), eq(expr))).thenReturn(Lists.newArrayList(countValue1));
List<Map<String, Object>> result = alertDefineService.getDefinePreview("promql", ALERT_THRESHOLD_TYPE_PERIODIC, expr);
List<Map<String, Object>> result = alertDefineService.getDefinePreview("promql", METRIC_ALERT_THRESHOLD_TYPE_PERIODIC, expr);
assertNotNull(result);
assertEquals(1307, result.get(0).get("__value__"));
result = alertDefineService.getDefinePreview("promql", ALERT_THRESHOLD_TYPE_PERIODIC, null);
result = alertDefineService.getDefinePreview("promql", METRIC_ALERT_THRESHOLD_TYPE_PERIODIC, null);
assertEquals(0, result.size());
result = alertDefineService.getDefinePreview("promql", ALERT_THRESHOLD_TYPE_REALTIME, null);
result = alertDefineService.getDefinePreview("promql", METRIC_ALERT_THRESHOLD_TYPE_REALTIME, null);
assertEquals(0, result.size());
}
@Test
void getMetricsRealTimeAlertDefines() {
// Test cache hit scenario
List<AlertDefine> cachedAlertDefines = Lists.newArrayList(
AlertDefine.builder().id(1L).type(METRIC_ALERT_THRESHOLD_TYPE_REALTIME).enable(true).build(),
AlertDefine.builder().id(2L).type(METRIC_ALERT_THRESHOLD_TYPE_REALTIME).enable(true).build()
);
try (MockedStatic<CacheFactory> cacheFactoryMock = Mockito.mockStatic(CacheFactory.class)) {
// Mock cache hit
cacheFactoryMock.when(CacheFactory::getMetricsAlertDefineCache).thenReturn(cachedAlertDefines);
List<AlertDefine> result = alertDefineService.getMetricsRealTimeAlertDefines();
assertNotNull(result);
assertEquals(2, result.size());
assertEquals(1L, result.get(0).getId());
assertEquals(2L, result.get(1).getId());
// Verify no database query was called
verify(alertDefineDao, times(0)).findAlertDefinesByTypeAndEnableTrue(any());
cacheFactoryMock.verify(() -> CacheFactory.setMetricsAlertDefineCache(any()), times(0));
}
// Test cache miss scenario
List<AlertDefine> dbAlertDefines = Lists.newArrayList(
AlertDefine.builder().id(3L).type(METRIC_ALERT_THRESHOLD_TYPE_REALTIME).enable(true).build()
);
try (MockedStatic<CacheFactory> cacheFactoryMock = Mockito.mockStatic(CacheFactory.class)) {
// Mock cache miss
cacheFactoryMock.when(CacheFactory::getMetricsAlertDefineCache).thenReturn(null);
when(alertDefineDao.findAlertDefinesByTypeAndEnableTrue(METRIC_ALERT_THRESHOLD_TYPE_REALTIME))
.thenReturn(dbAlertDefines);
List<AlertDefine> result = alertDefineService.getMetricsRealTimeAlertDefines();
assertNotNull(result);
assertEquals(1, result.size());
assertEquals(3L, result.get(0).getId());
// Verify database query and cache setting were called
verify(alertDefineDao, times(1)).findAlertDefinesByTypeAndEnableTrue(METRIC_ALERT_THRESHOLD_TYPE_REALTIME);
cacheFactoryMock.verify(() -> CacheFactory.setMetricsAlertDefineCache(dbAlertDefines), times(1));
}
}
@Test
void getLogRealTimeAlertDefines() {
// Test cache hit scenario
List<AlertDefine> cachedAlertDefines = Lists.newArrayList(
AlertDefine.builder().id(4L).type(LOG_ALERT_THRESHOLD_TYPE_REALTIME).enable(true).build(),
AlertDefine.builder().id(5L).type(LOG_ALERT_THRESHOLD_TYPE_REALTIME).enable(true).build()
);
try (MockedStatic<CacheFactory> cacheFactoryMock = Mockito.mockStatic(CacheFactory.class)) {
// Mock cache hit
cacheFactoryMock.when(CacheFactory::getLogAlertDefineCache).thenReturn(cachedAlertDefines);
List<AlertDefine> result = alertDefineService.getLogRealTimeAlertDefines();
assertNotNull(result);
assertEquals(2, result.size());
assertEquals(4L, result.get(0).getId());
assertEquals(5L, result.get(1).getId());
// Verify no database query was called
verify(alertDefineDao, times(0)).findAlertDefinesByTypeAndEnableTrue(any());
cacheFactoryMock.verify(() -> CacheFactory.setLogAlertDefineCache(any()), times(0));
}
// Test cache miss scenario
List<AlertDefine> dbAlertDefines = Lists.newArrayList(
AlertDefine.builder().id(6L).type(LOG_ALERT_THRESHOLD_TYPE_REALTIME).enable(true).build()
);
try (MockedStatic<CacheFactory> cacheFactoryMock = Mockito.mockStatic(CacheFactory.class)) {
// Mock cache miss
cacheFactoryMock.when(CacheFactory::getLogAlertDefineCache).thenReturn(null);
when(alertDefineDao.findAlertDefinesByTypeAndEnableTrue(LOG_ALERT_THRESHOLD_TYPE_REALTIME))
.thenReturn(dbAlertDefines);
List<AlertDefine> result = alertDefineService.getLogRealTimeAlertDefines();
assertNotNull(result);
assertEquals(1, result.size());
assertEquals(6L, result.get(0).getId());
// Verify database query and cache setting were called
verify(alertDefineDao, times(1)).findAlertDefinesByTypeAndEnableTrue(LOG_ALERT_THRESHOLD_TYPE_REALTIME);
cacheFactoryMock.verify(() -> CacheFactory.setLogAlertDefineCache(dbAlertDefines), times(1));
}
}
@Test
void getAlertDefinesByType() {
List<AlertDefine> mockAlertDefines = Lists.newArrayList(
AlertDefine.builder().id(7L).type(METRIC_ALERT_THRESHOLD_TYPE_REALTIME).enable(true).build(),
AlertDefine.builder().id(8L).type(METRIC_ALERT_THRESHOLD_TYPE_REALTIME).enable(true).build()
);
// Test valid metric realtime alert type
when(alertDefineDao.findAlertDefinesByTypeAndEnableTrue(METRIC_ALERT_THRESHOLD_TYPE_REALTIME))
.thenReturn(mockAlertDefines);
List<AlertDefine> result = alertDefineService.getAlertDefinesByType(METRIC_ALERT_THRESHOLD_TYPE_REALTIME);
assertNotNull(result);
assertEquals(2, result.size());
assertEquals(7L, result.get(0).getId());
assertEquals(8L, result.get(1).getId());
verify(alertDefineDao, times(1)).findAlertDefinesByTypeAndEnableTrue(METRIC_ALERT_THRESHOLD_TYPE_REALTIME);
// Test valid metric periodic alert type
reset(alertDefineDao);
when(alertDefineDao.findAlertDefinesByTypeAndEnableTrue(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC))
.thenReturn(Lists.newArrayList());
result = alertDefineService.getAlertDefinesByType(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC);
assertNotNull(result);
assertEquals(0, result.size());
verify(alertDefineDao, times(1)).findAlertDefinesByTypeAndEnableTrue(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC);
// Test valid log realtime alert type
reset(alertDefineDao);
when(alertDefineDao.findAlertDefinesByTypeAndEnableTrue(LOG_ALERT_THRESHOLD_TYPE_REALTIME))
.thenReturn(mockAlertDefines);
result = alertDefineService.getAlertDefinesByType(LOG_ALERT_THRESHOLD_TYPE_REALTIME);
assertNotNull(result);
assertEquals(2, result.size());
verify(alertDefineDao, times(1)).findAlertDefinesByTypeAndEnableTrue(LOG_ALERT_THRESHOLD_TYPE_REALTIME);
// Test valid log periodic alert type
reset(alertDefineDao);
when(alertDefineDao.findAlertDefinesByTypeAndEnableTrue(LOG_ALERT_THRESHOLD_TYPE_PERIODIC))
.thenReturn(Lists.newArrayList());
result = alertDefineService.getAlertDefinesByType(LOG_ALERT_THRESHOLD_TYPE_PERIODIC);
assertNotNull(result);
assertEquals(0, result.size());
verify(alertDefineDao, times(1)).findAlertDefinesByTypeAndEnableTrue(LOG_ALERT_THRESHOLD_TYPE_PERIODIC);
// Test empty string type
assertThrows(IllegalArgumentException.class, () -> alertDefineService.getAlertDefinesByType(""));
// Test null type
assertThrows(IllegalArgumentException.class, () -> alertDefineService.getAlertDefinesByType(null));
// Test invalid type
assertThrows(IllegalArgumentException.class, () -> alertDefineService.getAlertDefinesByType("invalid_type"));
}
}
@@ -28,9 +28,12 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.BeanUtils;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -147,4 +150,36 @@ public class AlibabaCloudSlsExternAlertServiceTest {
verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any(SingleAlert.class));
}
@Test
void testAddMergeExternAlert() {
String source = externAlertService.supportSource();
assertEquals("alibabacloud-sls", source);
AlibabaCloudSlsExternAlert externAlert = new AlibabaCloudSlsExternAlert();
externAlert.setAlertName("Test SLS alert");
externAlert.setFireTime((int) Instant.now().getEpochSecond());
externAlert.setAlertTime((int) Instant.now().getEpochSecond());
externAlert.setRegion("cn-hangzhou");
externAlert.setProject("project");
externAlert.setStatus("firing");
externAlert.setSeverity(AlibabaCloudSlsExternAlert.Severity.HIGH.getStatus());
Map<String, String> labels = new HashMap<>();
labels.put("labels-k", "labels-v");
externAlert.setLabels(labels);
Map<String, String> annotations = new HashMap<>();
annotations.put("annotations-k", "annotations-v");
externAlert.setAnnotations(annotations);
AlibabaCloudSlsExternAlert externAlert1 = new AlibabaCloudSlsExternAlert();
BeanUtils.copyProperties(externAlert, externAlert1);
List<AlibabaCloudSlsExternAlert> externAlerts = new ArrayList<>();
externAlerts.add(externAlert);
externAlerts.add(externAlert1);
externAlertService.addExternAlert(JsonUtil.toJson(externAlerts));
verify(alarmCommonReduce, times(2)).reduceAndSendAlarm(any(SingleAlert.class));
}
}
@@ -17,7 +17,7 @@
package org.apache.hertzbeat.alert.service;
import org.apache.hertzbeat.alert.config.AwsSmsProperties;
import org.apache.hertzbeat.common.entity.dto.sms.AwsSmsProperties;
import org.apache.hertzbeat.alert.service.impl.AwsSmsClientImpl;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
@@ -19,8 +19,8 @@ package org.apache.hertzbeat.alert.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.hertzbeat.alert.config.SmsConfig;
import org.apache.hertzbeat.alert.config.SmslocalSmsProperties;
import org.apache.hertzbeat.common.entity.dto.sms.SmsConfig;
import org.apache.hertzbeat.common.entity.dto.sms.SmslocalSmsProperties;
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
import org.apache.hertzbeat.common.constants.GeneralConfigTypeEnum;
import org.apache.hertzbeat.common.entity.manager.GeneralConfig;
@@ -89,4 +89,4 @@ public class SmsClientFactoryTest {
assertNull(smsClientFactory.getSmsClient());
}
}
}
@@ -17,7 +17,7 @@
package org.apache.hertzbeat.alert.service;
import org.apache.hertzbeat.alert.config.TwilioSmsProperties;
import org.apache.hertzbeat.common.entity.dto.sms.TwilioSmsProperties;
import org.apache.hertzbeat.alert.service.impl.TwilioSmsClientImpl;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
@@ -61,4 +61,4 @@ class TwilioSmsClientImplTest {
assertEquals(expected, twilioSmsClient.checkConfig());
}
}
}
@@ -18,7 +18,7 @@
package org.apache.hertzbeat.alert.service.impl;
import com.google.common.collect.Lists;
import org.apache.hertzbeat.alert.config.SmslocalSmsProperties;
import org.apache.hertzbeat.common.entity.dto.sms.SmslocalSmsProperties;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
@@ -70,4 +70,4 @@ public class SmsLocalSmsClientImplTest {
() -> smsLocalSmsClient.sendMessage(noticeReceiver, null, groupAlert));
}
}
}
@@ -17,7 +17,7 @@
package org.apache.hertzbeat.alert.service.impl;
import org.apache.hertzbeat.alert.config.UniSmsProperties;
import org.apache.hertzbeat.common.entity.dto.sms.UniSmsProperties;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
import org.apache.hertzbeat.common.support.exception.SendMessageException;
@@ -82,4 +82,4 @@ public class UniSmsClientImplTest {
}
}
}
@@ -87,4 +87,199 @@ class AlertTemplateUtilTest {
String expectedResult = "The price is $100 and the path is C:\\Users";
assertEquals(expectedResult, AlertTemplateUtil.render(template, param));
}
@Test
void renderNestedMapProperties() {
Map<String, Object> param = new HashMap<>();
Map<String, Object> log = new HashMap<>();
Map<String, Object> attributes = new HashMap<>();
attributes.put("level", "ERROR");
attributes.put("thread", "main");
log.put("attributes", attributes);
log.put("message", "Connection failed");
log.put("timestamp", "2024-01-01T10:00:00Z");
param.put("log", log);
param.put("instance", "server-01");
String template = "Log alert: ${log.attributes.level} - ${log.message} on ${instance}";
String expectedResult = "Log alert: ERROR - Connection failed on server-01";
assertEquals(expectedResult, AlertTemplateUtil.render(template, param));
}
@Test
void renderMultiLevelNestedProperties() {
Map<String, Object> param = new HashMap<>();
Map<String, Object> config = new HashMap<>();
Map<String, Object> database = new HashMap<>();
Map<String, Object> connection = new HashMap<>();
connection.put("host", "localhost");
connection.put("port", 3306);
database.put("connection", connection);
database.put("name", "hertzbeat");
config.put("database", database);
param.put("config", config);
String template = "Database: ${config.database.name} at ${config.database.connection.host}:${config.database.connection.port}";
String expectedResult = "Database: hertzbeat at localhost:3306";
assertEquals(expectedResult, AlertTemplateUtil.render(template, param));
}
@Test
void renderNonExistentNestedProperties() {
Map<String, Object> param = new HashMap<>();
Map<String, Object> log = new HashMap<>();
log.put("message", "Test message");
param.put("log", log);
// Test non-existent nested property
String template = "Log alert: ${log.nonexistent.property} - ${log.message}";
String expectedResult = "Log alert: NullValue - Test message";
assertEquals(expectedResult, AlertTemplateUtil.render(template, param));
}
@Test
void renderNonExistentTopLevelProperty() {
Map<String, Object> param = new HashMap<>();
param.put("existing", "value");
String template = "Value: ${nonexistent.property}";
String expectedResult = "Value: NullValue";
assertEquals(expectedResult, AlertTemplateUtil.render(template, param));
}
@Test
void renderNestedPropertiesWithNullValues() {
Map<String, Object> param = new HashMap<>();
Map<String, Object> data = new HashMap<>();
data.put("value", null);
data.put("description", "Test data");
param.put("data", data);
String template = "Data: ${data.value} - ${data.description}";
String expectedResult = "Data: NullValue - Test data";
assertEquals(expectedResult, AlertTemplateUtil.render(template, param));
}
@Test
void renderComplexNestedStructure() {
Map<String, Object> param = new HashMap<>();
Map<String, Object> alert = new HashMap<>();
Map<String, Object> monitor = new HashMap<>();
Map<String, Object> target = new HashMap<>();
Map<String, Object> metadata = new HashMap<>();
metadata.put("version", "1.0");
metadata.put("author", "system");
target.put("host", "192.168.1.100");
target.put("port", 8080);
target.put("metadata", metadata);
monitor.put("name", "HTTP Monitor");
monitor.put("target", target);
alert.put("monitor", monitor);
alert.put("severity", "CRITICAL");
param.put("alert", alert);
String template = "Alert: ${alert.severity} from ${alert.monitor.name} targeting ${alert.monitor.target.host}:${alert.monitor.target.port} (v${alert.monitor.target.metadata.version})";
String expectedResult = "Alert: CRITICAL from HTTP Monitor targeting 192.168.1.100:8080 (v1.0)";
assertEquals(expectedResult, AlertTemplateUtil.render(template, param));
}
@Test
void renderWithDifferentDataTypes() {
Map<String, Object> param = new HashMap<>();
Map<String, Object> metrics = new HashMap<>();
metrics.put("cpu", 85.5);
metrics.put("memory", 1024);
metrics.put("active", true);
metrics.put("count", 42L);
param.put("metrics", metrics);
String template = "CPU: ${metrics.cpu}%, Memory: ${metrics.memory}MB, Active: ${metrics.active}, Count: ${metrics.count}";
String expectedResult = "CPU: 85.5%, Memory: 1024MB, Active: true, Count: 42";
assertEquals(expectedResult, AlertTemplateUtil.render(template, param));
}
// Helper classes for testing object property access
public static class TestObject {
private String name;
private int value;
private TestNestedObject nested;
public TestObject(String name, int value) {
this.name = name;
this.value = value;
}
public String getName() {
return name;
}
public int getValue() {
return value;
}
public TestNestedObject getNested() {
return nested;
}
public void setNested(TestNestedObject nested) {
this.nested = nested;
}
}
public static class TestNestedObject {
private String description;
private boolean enabled;
public TestNestedObject(String description, boolean enabled) {
this.description = description;
this.enabled = enabled;
}
public String getDescription() {
return description;
}
public boolean isEnabled() {
return enabled;
}
}
@Test
void renderWithObjectProperties() {
Map<String, Object> param = new HashMap<>();
TestNestedObject nested = new TestNestedObject("Test description", true);
TestObject obj = new TestObject("TestName", 123);
obj.setNested(nested);
param.put("object", obj);
String template = "Object: ${object.name} (value: ${object.value}) - ${object.nested.description}, enabled: ${object.nested.enabled}";
String expectedResult = "Object: TestName (value: 123) - Test description, enabled: true";
assertEquals(expectedResult, AlertTemplateUtil.render(template, param));
}
@Test
void renderWithMixedMapAndObjectProperties() {
Map<String, Object> param = new HashMap<>();
Map<String, Object> config = new HashMap<>();
TestObject obj = new TestObject("ConfigObject", 456);
config.put("object", obj);
config.put("type", "mixed");
param.put("config", config);
String template = "Config type: ${config.type}, object name: ${config.object.name}, value: ${config.object.value}";
String expectedResult = "Config type: mixed, object name: ConfigObject, value: 456";
assertEquals(expectedResult, AlertTemplateUtil.render(template, param));
}
}
@@ -158,7 +158,7 @@ public class DnsCollectImpl extends AbstractCollect {
responseTimeStopWatch.start();
Name name = Name.fromString(dns.getAddress(), Name.root);
Message query = Message.newQuery(Record.newRecord(name, Type.ANY, DClass.value(dns.getQueryClass())));
Message query = Message.newQuery(Record.newRecord(name, Type.value(dns.getRecordType()), DClass.value(dns.getQueryClass())));
Resolver res = new SimpleResolver(dns.getDnsServerIP());
res.setTimeout(Duration.of(Long.parseLong(dns.getTimeout()), ChronoUnit.MILLIS));
res.setTCP(Boolean.parseBoolean(dns.getTcp()));

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