mirror of
https://github.com/dromara/hertzbeat.git
synced 2026-09-17 18:19:02 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2bfa4fc5d | ||
|
|
bc865d971e | ||
|
|
070ab41966 |
+9
-75
@@ -6,7 +6,8 @@
|
||||
"files": [
|
||||
"README.md",
|
||||
"README_CN.md",
|
||||
"README_JP.md"
|
||||
"README_JP.md",
|
||||
"home/src/pages/team/index.jsx"
|
||||
],
|
||||
"imageSize": 100,
|
||||
"commit": true,
|
||||
@@ -1274,7 +1275,11 @@
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/105542329?v=4",
|
||||
"profile": "https://github.com/lynx009",
|
||||
"contributions": [
|
||||
"doc"
|
||||
"code",
|
||||
"doc",
|
||||
"blog",
|
||||
"bug",
|
||||
"design"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -2584,7 +2589,8 @@
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/95274591?v=4",
|
||||
"profile": "https://github.com/tuzuy",
|
||||
"contributions": [
|
||||
"code"
|
||||
"code",
|
||||
"design"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -2595,78 +2601,6 @@
|
||||
"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,
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"settings": {},
|
||||
// same extensions as Gitpod, should match /.gitpod.yml
|
||||
"extensions": [
|
||||
"vscjava.vscode-java-pack",
|
||||
"editorconfig.editorconfig",
|
||||
|
||||
@@ -35,12 +35,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
- name: Install
|
||||
- name: Yarn Install
|
||||
working-directory: web-app
|
||||
run: pnpm install
|
||||
run: yarn
|
||||
- name: EsLint Test
|
||||
working-directory: web-app
|
||||
run: pnpm lint:ts
|
||||
run: yarn lint:ts
|
||||
|
||||
@@ -31,15 +31,12 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./script/ci/github-actions/setup-deps
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Build the Frontend
|
||||
run: |
|
||||
cd web-app
|
||||
pnpm install
|
||||
pnpm package
|
||||
yarn install
|
||||
yarn package
|
||||
|
||||
- name: Build the Backend
|
||||
run: |
|
||||
|
||||
@@ -66,7 +66,6 @@ header:
|
||||
- '.gitpod.yml'
|
||||
- 'karma.conf.js'
|
||||
- 'proxy.conf.js'
|
||||
- 'pnpm-lock.yaml'
|
||||
- '.helmignore'
|
||||
- 'script/helm/**'
|
||||
- 'web-app/src/app/core/**'
|
||||
|
||||
+14
-10
@@ -44,17 +44,19 @@ Even small corrections to typos are very welcome :)
|
||||
|
||||
#### Frontend start
|
||||
|
||||
1. Need `Node pnpm` Environment, Make sure `Node.js >= 18`
|
||||
1. Need `Node Yarn` Environment, Make sure `Node.js >= 18`
|
||||
|
||||
2. Cd to the `web-app` directory: `cd web-app`
|
||||
|
||||
3. Install pnpm if not existed `npm install -g pnpm`
|
||||
3. Install yarn if not existed `npm install -g yarn`
|
||||
|
||||
4. Install Dependencies: `pnpm install` or `pnpm install --registry=https://registry.npmmirror.com` in `web-app`
|
||||
4. Install Dependencies: `yarn install` or `yarn install --registry=https://registry.npmmirror.com` in `web-app`
|
||||
|
||||
5. After the local backend is started, start the local frontend in the web-app directory: `pnpm start`
|
||||
5. Install angular-cli globally: `yarn global add @angular/cli@15` or `yarn global add @angular/cli@15 --registry=https://registry.npmmirror.com`
|
||||
|
||||
6. Browser access to localhost:4200 to start, default account/password is *admin/hertzbeat*
|
||||
6. After the local backend is started, start the local frontend in the web-app directory: `ng serve --open`
|
||||
|
||||
7. Browser access to localhost:4200 to start, default account/password is *admin/hertzbeat*
|
||||
|
||||
|
||||
### Find tasks
|
||||
@@ -196,17 +198,19 @@ Add WeChat account `ahertzbeat` to pull you into the WeChat group.
|
||||
|
||||
#### 前端启动
|
||||
|
||||
1. 需要 `nodejs pnpm` 环境, Make sure `Node.js >= 18`
|
||||
1. 需要 `nodejs yarn` 环境, Make sure `Node.js >= 18`
|
||||
|
||||
2. 进入 `web-app` 目录: `cd web-app`
|
||||
|
||||
3. 安装pnpm: `npm install -g pnpm`
|
||||
3. 安装yarn: `npm install -g yarn`
|
||||
|
||||
4. 在前端工程目录 `web-app` 下执行: `pnpm install` or `pnpm install --registry=https://registry.npmmirror.com` in `web-app`
|
||||
4. 在前端工程目录 `web-app` 下执行: `yarn install` or `yarn install --registry=https://registry.npmmirror.com` in `web-app`
|
||||
|
||||
5. 待本地后端启动后,在web-app目录下启动本地前端 `pnpm start`
|
||||
5. 全局安装 `angular-cli`: `yarn global add @angular/cli@15` or `yarn global add @angular/cli@15 --registry=https://registry.npmmirror.com`
|
||||
|
||||
6. 浏览器访问 localhost:4200 即可开始,默认账号密码 *admin/hertzbeat*
|
||||
6. 待本地后端启动后,在web-app目录下启动本地前端 `ng serve --open`
|
||||
|
||||
7. 浏览器访问 localhost:4200 即可开始,默认账号密码 *admin/hertzbeat*
|
||||
|
||||
### 寻找任务
|
||||
|
||||
|
||||
@@ -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/lynx009"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="lynx009"/><br /><sub><b>lynx009</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lynx009" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/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="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=lynx009" title="Documentation">📖</a> <a href="#blog-lynx009" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Alynx009" title="Bug reports">🐛</a> <a href="#design-lynx009" title="Design">🎨</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/littlezhongzer"><img src="https://avatars.githubusercontent.com/u/33685289?v=4?s=100" width="100px;" alt="littlezhongzer"/><br /><sub><b>littlezhongzer</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=littlezhongzer" title="Code">💻</a></td>
|
||||
@@ -537,18 +537,8 @@ Thanks to these wonderful people, welcome to join us:
|
||||
<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/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> <a href="#design-tuzuy" title="Design">🎨</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>
|
||||
|
||||
+2
-12
@@ -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/lynx009"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="lynx009"/><br /><sub><b>lynx009</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lynx009" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/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="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=lynx009" title="Documentation">📖</a> <a href="#blog-lynx009" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Alynx009" title="Bug reports">🐛</a> <a href="#design-lynx009" title="Design">🎨</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/littlezhongzer"><img src="https://avatars.githubusercontent.com/u/33685289?v=4?s=100" width="100px;" alt="littlezhongzer"/><br /><sub><b>littlezhongzer</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=littlezhongzer" title="Code">💻</a></td>
|
||||
@@ -535,18 +535,8 @@ Thanks these wonderful people, welcome to join us:
|
||||
<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/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> <a href="#design-tuzuy" title="Design">🎨</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>
|
||||
|
||||
+2
-12
@@ -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/lynx009"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="lynx009"/><br /><sub><b>lynx009</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lynx009" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/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="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=lynx009" title="Documentation">📖</a> <a href="#blog-lynx009" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Alynx009" title="Bug reports">🐛</a> <a href="#design-lynx009" title="Design">🎨</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/littlezhongzer"><img src="https://avatars.githubusercontent.com/u/33685289?v=4?s=100" width="100px;" alt="littlezhongzer"/><br /><sub><b>littlezhongzer</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=littlezhongzer" title="Code">💻</a></td>
|
||||
@@ -539,18 +539,8 @@ Thanks these wonderful people, welcome to join us:
|
||||
<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/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> <a href="#design-tuzuy" title="Design">🎨</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>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<artifactId>hertzbeat-ai-agent</artifactId>
|
||||
<version>${hertzbeat.version}</version>
|
||||
<properties>
|
||||
<spring-ai.version>1.0.3</spring-ai.version>
|
||||
<spring-ai.version>1.0.1</spring-ai.version>
|
||||
<java.version>17</java.version>
|
||||
</properties>
|
||||
|
||||
@@ -51,22 +51,12 @@
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-openai</artifactId>
|
||||
<artifactId>spring-ai-starter-model-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>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.hertzbeat</groupId>
|
||||
<artifactId>hertzbeat-alerter</artifactId>
|
||||
@@ -100,4 +90,4 @@
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
</project>
|
||||
+22
-21
@@ -18,39 +18,40 @@
|
||||
package org.apache.hertzbeat.ai.agent.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.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.jetbrains.annotations.NotNull;
|
||||
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 LLM Provider API Key implementation that retrieves the API key
|
||||
* 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 GeneralConfigDao generalConfigDao;
|
||||
|
||||
public DynamicOpenAiApiKey(GeneralConfigDao generalConfigDao) {
|
||||
this.generalConfigDao = generalConfigDao;
|
||||
private final OpenAiConfigService openAiConfigService;
|
||||
|
||||
public DynamicOpenAiApiKey(OpenAiConfigService openAiConfigService) {
|
||||
this.openAiConfigService = openAiConfigService;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String getValue() {
|
||||
GeneralConfig providerConfig = generalConfigDao.findByType("provider");
|
||||
ModelProviderConfig modelProviderConfig = JsonUtil.fromJson(providerConfig.getContent(), ModelProviderConfig.class);
|
||||
|
||||
if (modelProviderConfig != null && modelProviderConfig.isEnable() && modelProviderConfig.isStatus()) {
|
||||
log.debug("Retrieved {} API key from configuration service", modelProviderConfig.getCode());
|
||||
return modelProviderConfig.getApiKey();
|
||||
} else {
|
||||
log.warn("No valid LLM Provider API key found in configuration");
|
||||
return "";
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
-112
@@ -18,144 +18,59 @@
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.common.support.event.AiProviderConfigChangeEvent;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.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.beans.factory.annotation.Value;
|
||||
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;
|
||||
}
|
||||
@Value("${spring.ai.openai.chat.options.model}")
|
||||
private String model;
|
||||
|
||||
/**
|
||||
* Create ChatClient bean with all dependencies created internally
|
||||
* Create OpenAI API instance with dynamic API key
|
||||
*/
|
||||
@Bean
|
||||
public ChatClient openAiChatClient() {
|
||||
return createChatClient();
|
||||
public OpenAiApi openAiApi(DynamicOpenAiApiKey dynamicApiKey) {
|
||||
return OpenAiApi.builder()
|
||||
.apiKey(dynamicApiKey)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create ChatClient with all necessary components
|
||||
* Create OpenAI Chat Options with custom settings
|
||||
*/
|
||||
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.isEnable() || !modelProviderConfig.isStatus()) {
|
||||
log.warn("LLM Provider is not enabled or status is not valid, ChatClient bean will not be created");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (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())
|
||||
@Bean
|
||||
public OpenAiChatOptions openAiChatOptions() {
|
||||
return OpenAiChatOptions.builder()
|
||||
.model(model)
|
||||
.temperature(0.3)
|
||||
.build();
|
||||
|
||||
// Create Chat Model
|
||||
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
|
||||
.openAiApi(builder.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();
|
||||
|
||||
// Create and return ChatClient
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ChatClient openAiChatClient(OpenAiChatModel openAiChatModel) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+19
-16
@@ -15,26 +15,29 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.calculate.realtime.window;
|
||||
package org.apache.hertzbeat.ai.agent.config;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
|
||||
import org.apache.hertzbeat.common.entity.log.LogEntry;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Represents a log entry that matched an alert expression
|
||||
* Used for communication between LogWorker and WindowAggregator
|
||||
* OpenAI YAML Configuration - reads from spring.ai.openai.api-key
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
public class MatchingLogEvent {
|
||||
|
||||
private LogEntry logEntry;
|
||||
|
||||
private AlertDefine alertDefine;
|
||||
|
||||
private long eventTimestamp;
|
||||
|
||||
private long workerTimestamp;
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "spring.ai.openai")
|
||||
public class OpenAiYamlConfig {
|
||||
|
||||
/**
|
||||
* OpenAI API key from spring.ai.openai.api-key
|
||||
*/
|
||||
private String apiKey;
|
||||
|
||||
/**
|
||||
* Check if OpenAI is enabled (has API key)
|
||||
*/
|
||||
public boolean isEnable() {
|
||||
return apiKey != null && !apiKey.trim().isEmpty();
|
||||
}
|
||||
}
|
||||
+160
@@ -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.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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+15
-19
@@ -15,27 +15,23 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.log.service;
|
||||
package org.apache.hertzbeat.ai.agent.dao;
|
||||
|
||||
import org.apache.hertzbeat.ai.agent.entity.OpenAiConfig;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/**
|
||||
* Adapter interface for ingesting logs pushed via different protocols
|
||||
* (e.g. OTLP, Loki, Filebeat, Vector).
|
||||
* Implementations should:
|
||||
* 1. Parse raw HTTP payload of their protocol.
|
||||
* 2. Convert data to LogEntry.
|
||||
* 3. Forward / persist it to downstream pipeline.
|
||||
* OpenAI Agent Configuration Dao
|
||||
*/
|
||||
public interface LogProtocolAdapter {
|
||||
|
||||
@Repository
|
||||
public interface OpenAiConfigDao extends JpaRepository<OpenAiConfig, String>, JpaSpecificationExecutor<OpenAiConfig> {
|
||||
|
||||
/**
|
||||
* Ingest raw log payload pushed from external system.
|
||||
*
|
||||
* @param content raw request body string
|
||||
* Query by type
|
||||
* @param type type
|
||||
* @return Return the queried configuration information
|
||||
*/
|
||||
void ingest(String content);
|
||||
|
||||
/**
|
||||
* Identifier of the protocol this adapter supports ("otlp", "vector", etc.)
|
||||
*/
|
||||
String supportProtocol();
|
||||
}
|
||||
OpenAiConfig findByType(String type);
|
||||
}
|
||||
+5
-5
@@ -15,17 +15,17 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.common.support.event;
|
||||
package org.apache.hertzbeat.ai.agent.event;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
|
||||
/**
|
||||
* Ai Provider configuration change event
|
||||
* OpenAI configuration change event
|
||||
*/
|
||||
public class AiProviderConfigChangeEvent extends ApplicationEvent {
|
||||
public class OpenAiConfigChangeEvent extends ApplicationEvent {
|
||||
|
||||
public AiProviderConfigChangeEvent(ApplicationContext source) {
|
||||
public OpenAiConfigChangeEvent(ApplicationContext source) {
|
||||
super(source);
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-26
@@ -24,36 +24,24 @@ import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Model Provider Configuration
|
||||
* OpenAI Configuration DTO - simplified to handle only API key
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Schema(description = "LLM Model Provider configuration")
|
||||
public class ModelProviderConfig {
|
||||
|
||||
@Schema(title = "Enable Provider", description = "Whether Provider is enabled", example = "true")
|
||||
@Schema(description = "OpenAI configuration")
|
||||
public class OpenAiConfigDto {
|
||||
|
||||
/**
|
||||
* Whether to enable OpenAI, default is false
|
||||
*/
|
||||
@Schema(title = "Enable OpenAI", description = "Whether OpenAI is enabled", example = "true")
|
||||
private boolean enable = false;
|
||||
|
||||
@Schema(title = "Check the provider available status")
|
||||
private boolean status = false;
|
||||
|
||||
@Schema(title = "The error message when provider status check failed")
|
||||
private String error;
|
||||
|
||||
@Schema(title = "Model type, text-generate, vision")
|
||||
private String type;
|
||||
|
||||
@Schema(title = "Model Provider code, like openai, zai, bigmodel")
|
||||
private String code;
|
||||
|
||||
@Schema(title = "custom the provider server base url")
|
||||
private String baseUrl;
|
||||
|
||||
@Schema(title = "use the model id name, eg: gpt-5, glm-4.6")
|
||||
private String model;
|
||||
|
||||
@Schema(title = "API Key", description = "API key", example = "sk-...")
|
||||
|
||||
/**
|
||||
* OpenAI API key
|
||||
*/
|
||||
@Schema(title = "API Key", description = "OpenAI API key", example = "sk-...")
|
||||
@NotBlank(message = "API Key cannot be empty when enabled")
|
||||
private String apiKey;
|
||||
}
|
||||
}
|
||||
+1
-7
@@ -33,10 +33,4 @@ 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();
|
||||
}
|
||||
}
|
||||
+43
-3
@@ -17,10 +17,38 @@
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.service;
|
||||
|
||||
import org.apache.hertzbeat.ai.agent.event.OpenAiConfigChangeEvent;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.OpenAiConfigDto;
|
||||
|
||||
/**
|
||||
* Ai Configuration Service
|
||||
* OpenAI Configuration Service
|
||||
* Consolidated service for OpenAI configuration, validation, and client factory management
|
||||
*/
|
||||
public interface AiConfigService {
|
||||
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
|
||||
@@ -29,6 +57,18 @@ public interface AiConfigService {
|
||||
*/
|
||||
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
|
||||
*/
|
||||
@@ -57,4 +97,4 @@ public interface AiConfigService {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-100
@@ -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.impl;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.agent.service.AiConfigService;
|
||||
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
|
||||
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.util.StringUtils;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* Ai Configuration Service Implementation
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AiConfigServiceImpl implements AiConfigService {
|
||||
|
||||
private static final String OPENAI_MODELS_ENDPOINT = "https://api.openai.com/v1/models";
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
private final GeneralConfigDao generalConfigDao;
|
||||
|
||||
public AiConfigServiceImpl(GeneralConfigDao generalConfigDao, RestTemplate restTemplate) {
|
||||
this.restTemplate = restTemplate;
|
||||
this.generalConfigDao = generalConfigDao;
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-30
@@ -21,11 +21,7 @@ package org.apache.hertzbeat.ai.agent.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.pojo.dto.ModelProviderConfig;
|
||||
import org.apache.hertzbeat.ai.agent.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.springframework.ai.chat.client.ChatClient;
|
||||
@@ -35,7 +31,6 @@ 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;
|
||||
@@ -50,25 +45,19 @@ import java.util.List;
|
||||
@Service
|
||||
public class ChatClientProviderServiceImpl implements ChatClientProviderService {
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
private final GeneralConfigDao generalConfigDao;
|
||||
private final ChatClient chatClient;
|
||||
|
||||
@Qualifier("hertzbeatTools")
|
||||
@Autowired
|
||||
private ToolCallbackProvider toolCallbackProvider;
|
||||
|
||||
private boolean isConfigured = false;
|
||||
|
||||
@Autowired
|
||||
public ChatClientProviderServiceImpl(ApplicationContext applicationContext, GeneralConfigDao generalConfigDao) {
|
||||
this.applicationContext = applicationContext;
|
||||
this.generalConfigDao = generalConfigDao;
|
||||
public ChatClientProviderServiceImpl(ChatClient openAiChatClient) {
|
||||
this.chatClient = openAiChatClient;
|
||||
}
|
||||
|
||||
public String complete(String message) {
|
||||
ChatClient chatClient = applicationContext.getBean("openAiChatClient", ChatClient.class);
|
||||
return chatClient.prompt()
|
||||
return this.chatClient.prompt()
|
||||
.user(message)
|
||||
.call()
|
||||
.content();
|
||||
@@ -77,9 +66,6 @@ public class ChatClientProviderServiceImpl implements ChatClientProviderService
|
||||
@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
|
||||
@@ -97,7 +83,7 @@ public class ChatClientProviderServiceImpl implements ChatClientProviderService
|
||||
|
||||
log.info("Starting streaming chat for conversation: {}", context.getConversationId());
|
||||
|
||||
return chatClient.prompt()
|
||||
return this.chatClient.prompt()
|
||||
.messages(messages)
|
||||
.system(PromptProvider.HERTZBEAT_SYSTEM_PROMPT)
|
||||
.toolCallbacks(toolCallbackProvider)
|
||||
@@ -111,14 +97,4 @@ 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.isStatus();
|
||||
}
|
||||
return isConfigured;
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-4
@@ -24,6 +24,7 @@ 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;
|
||||
@@ -50,7 +51,10 @@ public class ConversationServiceImpl implements ConversationService {
|
||||
|
||||
@Autowired
|
||||
private ChatClientProviderService chatClientProviderService;
|
||||
|
||||
|
||||
@Autowired
|
||||
private OpenAiConfigService openAiConfigService;
|
||||
|
||||
@Override
|
||||
public ConversationDto createConversation() {
|
||||
String conversationId = createNewConversation();
|
||||
@@ -71,10 +75,10 @@ public class ConversationServiceImpl implements ConversationService {
|
||||
}
|
||||
|
||||
// Check if OpenAI is properly configured
|
||||
if (!chatClientProviderService.isConfigured()) {
|
||||
if (!openAiConfigService.isConfigured()) {
|
||||
ChatResponseDto errorResponse = ChatResponseDto.builder()
|
||||
.conversationId(conversationId)
|
||||
.response("Provider is not configured. Please configure your AI Provider.")
|
||||
.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")
|
||||
@@ -269,4 +273,4 @@ public class ConversationServiceImpl implements ConversationService {
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.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();
|
||||
}
|
||||
}
|
||||
@@ -35,12 +35,10 @@ public class AlerterWorkerPool {
|
||||
|
||||
private ThreadPoolExecutor workerExecutor;
|
||||
private ThreadPoolExecutor notifyExecutor;
|
||||
private ThreadPoolExecutor logWorkerExecutor;
|
||||
|
||||
public AlerterWorkerPool() {
|
||||
initWorkExecutor();
|
||||
initNotifyExecutor();
|
||||
initLogWorkerExecutor();
|
||||
}
|
||||
|
||||
private void initWorkExecutor() {
|
||||
@@ -79,21 +77,6 @@ public class AlerterWorkerPool {
|
||||
new ThreadPoolExecutor.AbortPolicy());
|
||||
}
|
||||
|
||||
private void initLogWorkerExecutor() {
|
||||
ThreadFactory threadFactory = new ThreadFactoryBuilder()
|
||||
.setUncaughtExceptionHandler((thread, throwable) -> {
|
||||
log.error("Alerter logWorkerExecutor has uncaughtException.");
|
||||
log.error(throwable.getMessage(), throwable);
|
||||
})
|
||||
.setDaemon(true)
|
||||
.setNameFormat("log-worker-%d")
|
||||
.build();
|
||||
logWorkerExecutor = new ThreadPoolExecutor(10, 10, 10, TimeUnit.SECONDS,
|
||||
new LinkedBlockingQueue<>(1000),
|
||||
threadFactory,
|
||||
new ThreadPoolExecutor.AbortPolicy());
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the alerter task
|
||||
* @param runnable task
|
||||
@@ -113,13 +96,4 @@ public class AlerterWorkerPool {
|
||||
notifyExecutor.execute(runnable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the given runnable task using the logWorkerExecutor.
|
||||
*
|
||||
* @param runnable the task to be executed
|
||||
* @throws RejectedExecutionException if the task cannot be accepted for execution
|
||||
*/
|
||||
public void executeLogJob(Runnable runnable) throws RejectedExecutionException {
|
||||
logWorkerExecutor.execute(runnable);
|
||||
}
|
||||
}
|
||||
|
||||
-72
@@ -1,72 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.calculate;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.jexl3.JexlException;
|
||||
import org.apache.commons.jexl3.JexlExpression;
|
||||
import org.apache.hertzbeat.common.util.JexlExpressionRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* JexlExprCalculator is a utility class for evaluating JEXL expressions
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class JexlExprCalculator {
|
||||
/**
|
||||
* Execute an alert expression
|
||||
* @param fieldValueMap The field value map for expression evaluation
|
||||
* @param expr The expression to evaluate
|
||||
* @param ignoreJexlException Whether to ignore JEXL exceptions
|
||||
* @return true if the expression matches, false otherwise
|
||||
*/
|
||||
public boolean execAlertExpression(Map<String, Object> fieldValueMap, String expr, boolean ignoreJexlException) {
|
||||
Boolean match;
|
||||
JexlExpression expression;
|
||||
try {
|
||||
expression = JexlExpressionRunner.compile(expr);
|
||||
} catch (JexlException jexlException) {
|
||||
log.warn("Alarm Rule: {} Compile Error: {}.", expr, jexlException.getMessage());
|
||||
throw jexlException;
|
||||
} catch (Exception e) {
|
||||
log.error("Alarm Rule: {} Unknown Error: {}.", expr, e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
|
||||
try {
|
||||
match = (Boolean) JexlExpressionRunner.evaluate(expression, fieldValueMap);
|
||||
} catch (JexlException jexlException) {
|
||||
if (ignoreJexlException) {
|
||||
log.debug("Alarm Rule: {} Run Error: {}.", expr, jexlException.getMessage());
|
||||
} else {
|
||||
log.error("Alarm Rule: {} Run Error: {}.", expr, jexlException.getMessage());
|
||||
}
|
||||
throw jexlException;
|
||||
} catch (Exception e) {
|
||||
log.error("Alarm Rule: {} Unknown Error: {}.", expr, e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
return match != null && match;
|
||||
}
|
||||
|
||||
}
|
||||
+56
-68
@@ -15,12 +15,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.calculate.periodic;
|
||||
package org.apache.hertzbeat.alert.calculate;
|
||||
|
||||
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;
|
||||
@@ -35,11 +34,12 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Metrics Periodic Alert Calculator
|
||||
* Periodic Alert Calculator
|
||||
*/
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class MetricsPeriodicAlertCalculator {
|
||||
public class PeriodicAlertCalculator {
|
||||
|
||||
private static final String VALUE = "__value__";
|
||||
private static final String TIMESTAMP = "__timestamp__";
|
||||
@@ -48,82 +48,71 @@ public class MetricsPeriodicAlertCalculator {
|
||||
private final AlarmCommonReduce alarmCommonReduce;
|
||||
private final AlarmCacheManager alarmCacheManager;
|
||||
|
||||
public MetricsPeriodicAlertCalculator(DataSourceService dataSourceService, AlarmCommonReduce alarmCommonReduce,
|
||||
AlarmCacheManager alarmCacheManager) {
|
||||
public PeriodicAlertCalculator(DataSourceService dataSourceService, AlarmCommonReduce alarmCommonReduce,
|
||||
AlarmCacheManager alarmCacheManager) {
|
||||
this.dataSourceService = dataSourceService;
|
||||
this.alarmCommonReduce = alarmCommonReduce;
|
||||
this.alarmCacheManager = alarmCacheManager;
|
||||
this.dataSourceService = dataSourceService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate alerts for the given alert definition
|
||||
* @param define The alert definition to calculate
|
||||
*/
|
||||
|
||||
public void calculate(AlertDefine define) {
|
||||
if (!define.isEnable() || StringUtils.isEmpty(define.getExpr())) {
|
||||
log.error("Periodic define {} is disabled or expression is empty", define.getName());
|
||||
return;
|
||||
}
|
||||
|
||||
long currentTimeMilli = System.currentTimeMillis();
|
||||
try {
|
||||
doCalculate(define, currentTimeMilli);
|
||||
// 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;
|
||||
}
|
||||
} 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);
|
||||
@@ -135,6 +124,7 @@ public class MetricsPeriodicAlertCalculator {
|
||||
// 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)
|
||||
@@ -168,13 +158,11 @@ public class MetricsPeriodicAlertCalculator {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
@@ -183,4 +171,4 @@ public class MetricsPeriodicAlertCalculator {
|
||||
alarmCacheManager.removePending(defineId, fingerprint);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+8
-22
@@ -15,13 +15,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.calculate.periodic;
|
||||
package org.apache.hertzbeat.alert.calculate;
|
||||
|
||||
import static org.apache.hertzbeat.common.constants.CommonConstants.LOG_ALERT_THRESHOLD_TYPE_PERIODIC;
|
||||
import static org.apache.hertzbeat.common.constants.CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_PERIODIC;
|
||||
import static org.apache.hertzbeat.common.constants.CommonConstants.ALERT_THRESHOLD_TYPE_PERIODIC;
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -42,15 +39,13 @@ import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
|
||||
@Component
|
||||
public class PeriodicAlertRuleScheduler implements CommandLineRunner {
|
||||
|
||||
private final MetricsPeriodicAlertCalculator metricsCalculator;
|
||||
private final LogPeriodicAlertCalculator logCalculator;
|
||||
private final PeriodicAlertCalculator calculator;
|
||||
private final AlertDefineDao alertDefineDao;
|
||||
private final ScheduledExecutorService scheduledExecutor;
|
||||
private final Map<Long, ScheduledFuture<?>> scheduledFutures;
|
||||
|
||||
public PeriodicAlertRuleScheduler(MetricsPeriodicAlertCalculator metricsCalculator, LogPeriodicAlertCalculator logCalculator, AlertDefineDao alertDefineDao) {
|
||||
this.metricsCalculator = metricsCalculator;
|
||||
this.logCalculator = logCalculator;
|
||||
public PeriodicAlertRuleScheduler(PeriodicAlertCalculator calculator, AlertDefineDao alertDefineDao) {
|
||||
this.calculator = calculator;
|
||||
this.alertDefineDao = alertDefineDao;
|
||||
ThreadFactory threadFactory = new ThreadFactoryBuilder()
|
||||
.setUncaughtExceptionHandler((thread, throwable) -> {
|
||||
@@ -81,14 +76,9 @@ public class PeriodicAlertRuleScheduler implements CommandLineRunner {
|
||||
return;
|
||||
}
|
||||
cancelSchedule(rule.getId());
|
||||
if (rule.getType().equals(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC)
|
||||
|| rule.getType().equals(LOG_ALERT_THRESHOLD_TYPE_PERIODIC)) {
|
||||
if (rule.getType().equals(ALERT_THRESHOLD_TYPE_PERIODIC)) {
|
||||
ScheduledFuture<?> future = scheduledExecutor.scheduleAtFixedRate(() -> {
|
||||
if (rule.getType().equals(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC)) {
|
||||
metricsCalculator.calculate(rule);
|
||||
} else if (rule.getType().equals(LOG_ALERT_THRESHOLD_TYPE_PERIODIC)) {
|
||||
logCalculator.calculate(rule);
|
||||
}
|
||||
calculator.calculate(rule);
|
||||
}, 0, rule.getPeriod(), java.util.concurrent.TimeUnit.SECONDS);
|
||||
scheduledFutures.put(rule.getId(), future);
|
||||
}
|
||||
@@ -97,11 +87,7 @@ public class PeriodicAlertRuleScheduler implements CommandLineRunner {
|
||||
@Override
|
||||
public void run(String... args) throws Exception {
|
||||
log.info("Starting periodic alert rule scheduler...");
|
||||
List<AlertDefine> metricsPeriodicRules = alertDefineDao.findAlertDefinesByTypeAndEnableTrue(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC);
|
||||
List<AlertDefine> logPeriodicRules = alertDefineDao.findAlertDefinesByTypeAndEnableTrue(LOG_ALERT_THRESHOLD_TYPE_PERIODIC);
|
||||
List<AlertDefine> periodicRules = new ArrayList<>(metricsPeriodicRules.size() + logPeriodicRules.size());
|
||||
periodicRules.addAll(metricsPeriodicRules);
|
||||
periodicRules.addAll(logPeriodicRules);
|
||||
List<AlertDefine> periodicRules = alertDefineDao.findAlertDefinesByTypeAndEnableTrue(ALERT_THRESHOLD_TYPE_PERIODIC);
|
||||
for (AlertDefine rule : periodicRules) {
|
||||
updateSchedule(rule);
|
||||
}
|
||||
+55
-42
@@ -15,7 +15,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.calculate.realtime;
|
||||
package org.apache.hertzbeat.alert.calculate;
|
||||
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
@@ -27,10 +28,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;
|
||||
@@ -42,6 +43,7 @@ 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;
|
||||
@@ -51,10 +53,10 @@ import org.springframework.util.CollectionUtils;
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class MetricsRealTimeAlertCalculator {
|
||||
|
||||
public class RealTimeAlertCalculator {
|
||||
|
||||
private static final int CALCULATE_THREADS = 3;
|
||||
|
||||
|
||||
private static final String KEY_INSTANCE = "__instance__";
|
||||
private static final String KEY_INSTANCE_NAME = "__instancename__";
|
||||
private static final String KEY_INSTANCE_HOST = "__instancehost__";
|
||||
@@ -79,47 +81,38 @@ public class MetricsRealTimeAlertCalculator {
|
||||
private final AlertDefineService alertDefineService;
|
||||
private final AlarmCommonReduce alarmCommonReduce;
|
||||
private final AlarmCacheManager alarmCacheManager;
|
||||
private final JexlExprCalculator jexlExprCalculator;
|
||||
|
||||
|
||||
@Autowired
|
||||
public MetricsRealTimeAlertCalculator(AlerterWorkerPool workerPool, CommonDataQueue dataQueue,
|
||||
AlertDefineService alertDefineService, SingleAlertDao singleAlertDao,
|
||||
AlarmCommonReduce alarmCommonReduce, AlarmCacheManager alarmCacheManager,
|
||||
JexlExprCalculator jexlExprCalculator) {
|
||||
this(workerPool, dataQueue, alertDefineService, singleAlertDao, alarmCommonReduce, alarmCacheManager, jexlExprCalculator, true);
|
||||
public RealTimeAlertCalculator(AlerterWorkerPool workerPool, CommonDataQueue dataQueue,
|
||||
AlertDefineService alertDefineService, SingleAlertDao singleAlertDao,
|
||||
AlarmCommonReduce alarmCommonReduce, AlarmCacheManager alarmCacheManager) {
|
||||
this(workerPool, dataQueue, alertDefineService, singleAlertDao, alarmCommonReduce, alarmCacheManager, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for MetricsRealTimeAlertCalculator with a toggle to control whether to start alert calculation threads.
|
||||
* Constructor for RealTimeAlertCalculator with a toggle to control whether to start alert calculation threads.
|
||||
*
|
||||
* @param workerPool The worker pool used for concurrent alert calculation.
|
||||
* @param dataQueue The queue from which metric data is pulled and pushed.
|
||||
* @param alertDefineService The service providing alert definition rules.
|
||||
* @param singleAlertDao The DAO for fetching persisted alert states from storage.
|
||||
* @param alarmCommonReduce The component responsible for reducing and sending alerts.
|
||||
* @param alarmCacheManager The cache manager for managing alert states.
|
||||
* @param start If true, the alert calculation threads will start automatically;
|
||||
* set to false to disable thread start (useful for unit testing).
|
||||
*/
|
||||
public MetricsRealTimeAlertCalculator(AlerterWorkerPool workerPool, CommonDataQueue dataQueue,
|
||||
AlertDefineService alertDefineService, SingleAlertDao singleAlertDao,
|
||||
AlarmCommonReduce alarmCommonReduce, AlarmCacheManager alarmCacheManager,
|
||||
JexlExprCalculator jexlExprCalculator, boolean start) {
|
||||
public RealTimeAlertCalculator(AlerterWorkerPool workerPool, CommonDataQueue dataQueue,
|
||||
AlertDefineService alertDefineService, SingleAlertDao singleAlertDao,
|
||||
AlarmCommonReduce alarmCommonReduce, AlarmCacheManager alarmCacheManager, 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()) {
|
||||
@@ -139,7 +132,7 @@ public class MetricsRealTimeAlertCalculator {
|
||||
}
|
||||
}
|
||||
|
||||
protected void calculate(CollectRep.MetricsData metricsData) {
|
||||
private void calculate(CollectRep.MetricsData metricsData) {
|
||||
long currentTimeMilli = System.currentTimeMillis();
|
||||
String instance = String.valueOf(metricsData.getId());
|
||||
String instanceName = metricsData.getInstanceName();
|
||||
@@ -153,7 +146,7 @@ public class MetricsRealTimeAlertCalculator {
|
||||
int code = metricsData.getCode().getNumber();
|
||||
Map<String, String> labels = metricsData.getLabels();
|
||||
Map<String, String> annotations = metricsData.getAnnotations();
|
||||
List<AlertDefine> thresholds = this.alertDefineService.getMetricsRealTimeAlertDefines();
|
||||
List<AlertDefine> thresholds = this.alertDefineService.getRealTimeAlertDefines();
|
||||
// Filter thresholds by app, metrics, labels and instance
|
||||
thresholds = filterThresholdsByAppAndMetrics(thresholds, app, metrics, labels, instance, priority);
|
||||
if (thresholds.isEmpty()) {
|
||||
@@ -205,7 +198,7 @@ public class MetricsRealTimeAlertCalculator {
|
||||
{
|
||||
// trigger the expr before the metrics data, due the available up down or others
|
||||
try {
|
||||
boolean match = jexlExprCalculator.execAlertExpression(fieldValueMap, expr, true);
|
||||
boolean match = execAlertExpression(fieldValueMap, expr, true);
|
||||
try {
|
||||
if (match) {
|
||||
// If the threshold rule matches, the number of times the threshold has been triggered is determined and an alarm is triggered
|
||||
@@ -260,7 +253,7 @@ public class MetricsRealTimeAlertCalculator {
|
||||
}
|
||||
}
|
||||
try {
|
||||
boolean match = jexlExprCalculator.execAlertExpression(fieldValueMap, expr, false);
|
||||
boolean match = execAlertExpression(fieldValueMap, expr, false);
|
||||
try {
|
||||
if (match) {
|
||||
afterThresholdRuleMatch(defineId, currentTimeMilli, fingerPrints, fieldValueMap, define, annotations);
|
||||
@@ -281,9 +274,8 @@ public class MetricsRealTimeAlertCalculator {
|
||||
* @param thresholds Alert definitions to filter
|
||||
* @param app Current app name
|
||||
* @param metrics Current metrics name
|
||||
* @param labels Current labels
|
||||
* @param instance Current instance id
|
||||
* @param priority Current priority
|
||||
* @param priority Current priority
|
||||
* @return Filtered alert definitions
|
||||
*/
|
||||
public List<AlertDefine> filterThresholdsByAppAndMetrics(List<AlertDefine> thresholds, String app, String metrics, Map<String, String> labels, String instance, int priority) {
|
||||
@@ -299,7 +291,7 @@ public class MetricsRealTimeAlertCalculator {
|
||||
if (!appMatcher.find() || !app.equals(appMatcher.group(1))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Extract and check available - required
|
||||
if (priority != 0) {
|
||||
Matcher availableMatcher = AVAILABLE_PATTERN.matcher(expr);
|
||||
@@ -321,7 +313,7 @@ public class MetricsRealTimeAlertCalculator {
|
||||
if (!instanceMatcher.find() && !labelMatcher.find()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Reset matcher to check all instances
|
||||
instanceMatcher.reset();
|
||||
labelMatcher.reset();
|
||||
@@ -344,9 +336,6 @@ public class MetricsRealTimeAlertCalculator {
|
||||
.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);
|
||||
@@ -360,14 +349,9 @@ public class MetricsRealTimeAlertCalculator {
|
||||
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());
|
||||
@@ -393,11 +377,11 @@ public class MetricsRealTimeAlertCalculator {
|
||||
// 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);
|
||||
@@ -411,7 +395,7 @@ public class MetricsRealTimeAlertCalculator {
|
||||
// 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
|
||||
@@ -423,6 +407,35 @@ public class MetricsRealTimeAlertCalculator {
|
||||
}
|
||||
}
|
||||
|
||||
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("");
|
||||
-232
@@ -1,232 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.calculate.periodic;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
|
||||
import org.apache.hertzbeat.alert.service.DataSourceService;
|
||||
import org.apache.hertzbeat.alert.util.AlertTemplateUtil;
|
||||
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
|
||||
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/**
|
||||
* Log Periodic Alert Calculator
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class LogPeriodicAlertCalculator {
|
||||
|
||||
private static final String ROWS = "__rows__";
|
||||
|
||||
private final DataSourceService dataSourceService;
|
||||
private final AlarmCommonReduce alarmCommonReduce;
|
||||
|
||||
|
||||
public LogPeriodicAlertCalculator(DataSourceService dataSourceService, AlarmCommonReduce alarmCommonReduce) {
|
||||
this.alarmCommonReduce = alarmCommonReduce;
|
||||
this.dataSourceService = dataSourceService;
|
||||
}
|
||||
|
||||
public void calculate(AlertDefine define) {
|
||||
if (!define.isEnable() || StringUtils.isEmpty(define.getExpr())) {
|
||||
log.error("Log define {} is disabled or expression is empty", define.getName());
|
||||
return;
|
||||
}
|
||||
try {
|
||||
doCalculate(define);
|
||||
} catch (Exception e) {
|
||||
log.error("Calculate periodic define {} failed: {}", define.getName(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void doCalculate(AlertDefine define) {
|
||||
try {
|
||||
// Log-based queries are SQL queries with log-specific expressions
|
||||
List<Map<String, Object>> results = dataSourceService.query(define.getDatasource(), define.getExpr());
|
||||
if (CollectionUtils.isEmpty(results)) {
|
||||
return;
|
||||
}
|
||||
afterThresholdRuleMatch(results, define);
|
||||
} catch (Exception ignored) {
|
||||
// Ignore the query exception eg: no result, timeout, etc
|
||||
}
|
||||
}
|
||||
|
||||
String getAlertMode(AlertDefine alertDefine) {
|
||||
String mode = null;
|
||||
if (alertDefine.getLabels() != null) {
|
||||
mode = alertDefine.getLabels().get(CommonConstants.ALERT_MODE_LABEL);
|
||||
}
|
||||
if (mode == null || mode.isEmpty()) {
|
||||
return CommonConstants.ALERT_MODE_GROUP; // Default to group mode if not specified
|
||||
} else {
|
||||
return mode;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle alert after threshold rule match
|
||||
*/
|
||||
private void afterThresholdRuleMatch(List<Map<String, Object>> alertContext, AlertDefine define) {
|
||||
// Determine alert mode from configuration
|
||||
String alertMode = getAlertMode(define);
|
||||
|
||||
long currentTime = System.currentTimeMillis();
|
||||
|
||||
switch (alertMode) {
|
||||
case CommonConstants.ALERT_MODE_INDIVIDUAL:
|
||||
// Generate individual alerts for each matching log
|
||||
for (Map<String, Object> context : alertContext) {
|
||||
generateIndividualAlert(define, context, currentTime);
|
||||
}
|
||||
break;
|
||||
|
||||
case CommonConstants.ALERT_MODE_GROUP:
|
||||
// Generate a single alert group for all matching logs
|
||||
generateGroupAlert(define, alertContext, currentTime);
|
||||
break;
|
||||
default:
|
||||
log.warn("Unknown alert mode for define {}: {}", define.getName(), alertMode);
|
||||
}
|
||||
}
|
||||
|
||||
private void generateIndividualAlert(AlertDefine define, Map<String, Object> context, long currentTime) {
|
||||
|
||||
Map<String, String> alertLabels = new HashMap<>(8);
|
||||
|
||||
Map<String, String> commonFingerPrints = createCommonFingerprints(define);
|
||||
alertLabels.putAll(commonFingerPrints);
|
||||
addContextToMap(context, alertLabels);
|
||||
|
||||
Map<String, Object> fieldValueMap = createFieldValueMap(context, define);
|
||||
Map<String, String> alertAnnotations = createAlertAnnotations(define, fieldValueMap);
|
||||
// Create and send group alert
|
||||
SingleAlert alert = SingleAlert.builder()
|
||||
.labels(alertLabels)
|
||||
.annotations(alertAnnotations)
|
||||
.content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap))
|
||||
.status(CommonConstants.ALERT_STATUS_FIRING)
|
||||
.triggerTimes(1)
|
||||
.startAt(currentTime)
|
||||
.activeAt(currentTime)
|
||||
.build();
|
||||
|
||||
alarmCommonReduce.reduceAndSendAlarm(alert.clone());
|
||||
|
||||
log.debug("Generated individual alert for define: {}", define.getName());
|
||||
}
|
||||
|
||||
private void addContextToMap(Map<String, Object> context, Map<String, String> alertLabels) {
|
||||
for (Map.Entry<String, Object> entry : context.entrySet()) {
|
||||
if (entry.getValue() != null) {
|
||||
alertLabels.put(entry.getKey(), entry.getValue().toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void generateGroupAlert(AlertDefine define, List<Map<String, Object>> alertContext, long currentTime) {
|
||||
|
||||
List<SingleAlert> alerts = new ArrayList<>(alertContext.size());
|
||||
|
||||
// Create fingerprints for group alert
|
||||
Map<String, String> commonFingerPrints = createCommonFingerprints(define);
|
||||
|
||||
// Add context information to fingerprints
|
||||
commonFingerPrints.put(ROWS, String.valueOf(alertContext.size()));
|
||||
commonFingerPrints.put(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_GROUP);
|
||||
|
||||
for (Map<String, Object> context : alertContext) {
|
||||
|
||||
Map<String, String> alertLabels = new HashMap<>(8);
|
||||
|
||||
alertLabels.putAll(commonFingerPrints);
|
||||
// add the context to commonFingerPrints
|
||||
addContextToMap(context, alertLabels);
|
||||
|
||||
Map<String, Object> fieldValueMap = createFieldValueMap(context, define);
|
||||
Map<String, String> alertAnnotations = createAlertAnnotations(define, fieldValueMap);
|
||||
// Create and send group alert
|
||||
SingleAlert alert = SingleAlert.builder()
|
||||
.labels(alertLabels)
|
||||
.annotations(alertAnnotations)
|
||||
.content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap))
|
||||
.status(CommonConstants.ALERT_STATUS_FIRING)
|
||||
.triggerTimes(alertContext.size())
|
||||
.startAt(currentTime)
|
||||
.activeAt(currentTime)
|
||||
.build();
|
||||
alerts.add(alert.clone());
|
||||
}
|
||||
alarmCommonReduce.reduceAndSendAlarmGroup(commonFingerPrints, alerts);
|
||||
|
||||
log.debug("Generated group alert for define: {} with {} matching data",
|
||||
define.getName(), alertContext.size());
|
||||
}
|
||||
|
||||
|
||||
|
||||
private Map<String, String> createCommonFingerprints(AlertDefine define) {
|
||||
Map<String, String> fingerprints = new HashMap<>(8);
|
||||
fingerprints.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
|
||||
fingerprints.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(define.getId()));
|
||||
|
||||
if (define.getLabels() != null) {
|
||||
fingerprints.putAll(define.getLabels());
|
||||
}
|
||||
|
||||
return fingerprints;
|
||||
}
|
||||
|
||||
private Map<String, Object> createFieldValueMap(Map<String, Object> context, AlertDefine define) {
|
||||
Map<String, Object> fieldValueMap = new HashMap<>(8);
|
||||
for (Map.Entry<String, Object> entry : context.entrySet()) {
|
||||
if (entry.getValue() != null) {
|
||||
fieldValueMap.put(entry.getKey(), entry.getValue().toString());
|
||||
}
|
||||
}
|
||||
if (define.getLabels() != null) {
|
||||
fieldValueMap.putAll(define.getLabels());
|
||||
}
|
||||
|
||||
return fieldValueMap;
|
||||
}
|
||||
|
||||
private Map<String, String> createAlertAnnotations(AlertDefine define, Map<String, Object> fieldValueMap) {
|
||||
Map<String, String> annotations = new HashMap<>(8);
|
||||
|
||||
if (define.getAnnotations() != null) {
|
||||
for (Map.Entry<String, String> entry : define.getAnnotations().entrySet()) {
|
||||
annotations.put(entry.getKey(),
|
||||
AlertTemplateUtil.render(entry.getValue(), fieldValueMap));
|
||||
}
|
||||
}
|
||||
|
||||
return annotations;
|
||||
}
|
||||
|
||||
}
|
||||
-140
@@ -1,140 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.calculate.realtime;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.alert.calculate.realtime.window.LogWorker;
|
||||
import org.apache.hertzbeat.alert.calculate.realtime.window.TimeService;
|
||||
import org.apache.hertzbeat.common.entity.log.LogEntry;
|
||||
import org.apache.hertzbeat.common.queue.CommonDataQueue;
|
||||
import org.springframework.stereotype.Component;
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* WindowedLogRealTimeAlertCalculator - Single entry point for log stream processing
|
||||
* Responsible for:
|
||||
* 1. Reading from original log stream
|
||||
* 2. Extracting event timestamps
|
||||
* 3. Maintaining maxTimestamp for watermark generation
|
||||
* 4. Distributing logs to workers
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class WindowedLogRealTimeAlertCalculator implements Runnable {
|
||||
|
||||
private static final int CALCULATE_THREADS = 3;
|
||||
|
||||
private final CommonDataQueue dataQueue;
|
||||
private final TimeService timeService;
|
||||
private ThreadPoolExecutor dispatcherExecutor;
|
||||
private final LogWorker logWorker;
|
||||
|
||||
public WindowedLogRealTimeAlertCalculator(CommonDataQueue dataQueue, TimeService timeService, LogWorker logWorker) {
|
||||
this.dataQueue = dataQueue;
|
||||
this.timeService = timeService;
|
||||
this.logWorker = logWorker;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
try {
|
||||
LogEntry logEntry = dataQueue.pollLogEntry();
|
||||
if (logEntry != null) {
|
||||
processLogEntry(logEntry);
|
||||
dataQueue.sendLogEntryToStorage(logEntry);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
} catch (Exception e) {
|
||||
log.error("Error in log dispatch loop: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processLogEntry(LogEntry logEntry) {
|
||||
// Extract event timestamp
|
||||
long eventTimestamp = extractEventTimestamp(logEntry);
|
||||
|
||||
// Check if timestamp is within reasonable range
|
||||
if (!timeService.isValidTimestamp(eventTimestamp)) {
|
||||
log.warn("Dropping log with invalid timestamp: {}", eventTimestamp);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is late data based on current watermark
|
||||
if (timeService.isLateData(eventTimestamp)) {
|
||||
log.warn("Dropping late data, timestamp: {}, watermark: {}",
|
||||
eventTimestamp, timeService.getCurrentWatermark());
|
||||
return;
|
||||
}
|
||||
|
||||
// Update max timestamp (only validated timestamps can update watermark)
|
||||
timeService.updateMaxTimestamp(eventTimestamp);
|
||||
logWorker.reduceAndSendLogTask(logEntry);
|
||||
}
|
||||
|
||||
private long extractEventTimestamp(LogEntry logEntry) {
|
||||
if (logEntry.getTimeUnixNano() != null && logEntry.getTimeUnixNano() != 0) {
|
||||
return logEntry.getTimeUnixNano() / 1_000_000; // Convert to milliseconds
|
||||
}
|
||||
if (logEntry.getObservedTimeUnixNano() != null && logEntry.getObservedTimeUnixNano() != 0) {
|
||||
return logEntry.getObservedTimeUnixNano() / 1_000_000; // Convert to milliseconds
|
||||
}
|
||||
return System.currentTimeMillis();
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void start() {
|
||||
ThreadFactory threadFactory = new ThreadFactoryBuilder()
|
||||
.setUncaughtExceptionHandler((thread, throwable) -> {
|
||||
log.error("Alerter workerExecutor has uncaughtException.");
|
||||
log.error(throwable.getMessage(), throwable);
|
||||
})
|
||||
.setDaemon(true)
|
||||
.setNameFormat("log-dispatcher-%d")
|
||||
.build();
|
||||
// Create dispatcher thread executor
|
||||
this.dispatcherExecutor = new ThreadPoolExecutor(
|
||||
CALCULATE_THREADS,
|
||||
CALCULATE_THREADS,
|
||||
10,
|
||||
TimeUnit.SECONDS,
|
||||
new LinkedBlockingQueue<>(),
|
||||
threadFactory,
|
||||
new ThreadPoolExecutor.AbortPolicy()
|
||||
);
|
||||
for (int i = 0; i < CALCULATE_THREADS; i++) {
|
||||
dispatcherExecutor.execute(this);
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void stop() {
|
||||
if (dispatcherExecutor != null) {
|
||||
dispatcherExecutor.shutdownNow();
|
||||
}
|
||||
}
|
||||
}
|
||||
-317
@@ -1,317 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.calculate.realtime.window;
|
||||
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import org.apache.hertzbeat.alert.util.AlertTemplateUtil;
|
||||
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
|
||||
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
|
||||
import org.apache.hertzbeat.common.entity.log.LogEntry;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Alarm Evaluator - Final alarm logic trigger
|
||||
* Responsible for:
|
||||
* 1. Receiving closed window data from WindowAggregator
|
||||
* 2. Applying alert logic (count threshold, alert mode)
|
||||
* 3. Generating individual alerts or alert groups
|
||||
* 4. Sending alerts to AlarmCommonReduce
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class AlarmEvaluator {
|
||||
|
||||
private static final String WINDOW_START_TIME = "window_start_time";
|
||||
private static final String WINDOW_END_TIME = "window_end_time";
|
||||
private static final String MATCHING_LOGS_COUNT = "matching_logs_count";
|
||||
|
||||
private final AlarmCommonReduce alarmCommonReduce;
|
||||
private ThreadPoolExecutor workerExecutor;
|
||||
|
||||
public AlarmEvaluator(AlarmCommonReduce alarmCommonReduce) {
|
||||
this.alarmCommonReduce = alarmCommonReduce;
|
||||
initAlarmEvaluator();
|
||||
}
|
||||
|
||||
public void initAlarmEvaluator() {
|
||||
ThreadFactory threadFactory = new ThreadFactoryBuilder()
|
||||
.setUncaughtExceptionHandler((thread, throwable) -> {
|
||||
log.error("alerter-reduce-worker has uncaughtException.");
|
||||
log.error(throwable.getMessage(), throwable);
|
||||
})
|
||||
.setDaemon(true)
|
||||
.setNameFormat("alerter-reduce-worker-%d")
|
||||
.build();
|
||||
workerExecutor = new ThreadPoolExecutor(2,
|
||||
10,
|
||||
10,
|
||||
TimeUnit.SECONDS,
|
||||
new LinkedBlockingQueue<>(),
|
||||
threadFactory,
|
||||
new ThreadPoolExecutor.AbortPolicy());
|
||||
}
|
||||
|
||||
public void sendAndProcessWindowData(WindowAggregator.WindowData windowData) {
|
||||
workerExecutor.execute(processWindowData(windowData));
|
||||
}
|
||||
|
||||
private Runnable processWindowData(WindowAggregator.WindowData windowData) {
|
||||
return () -> {
|
||||
AlertDefine alertDefine = windowData.getAlertDefine();
|
||||
List<MatchingLogEvent> matchingLogs = windowData.getMatchingLogs();
|
||||
|
||||
if (matchingLogs.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if count threshold is met
|
||||
int requiredTimes = alertDefine.getTimes() != null ? alertDefine.getTimes() : 1;
|
||||
if (matchingLogs.size() < requiredTimes) {
|
||||
log.debug("Window {} has {} matching logs, but requires {} times",
|
||||
windowData.getWindowKey(), matchingLogs.size(), requiredTimes);
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine alert mode from configuration
|
||||
String alertMode = getAlertMode(alertDefine);
|
||||
|
||||
long currentTime = System.currentTimeMillis();
|
||||
|
||||
switch (alertMode) {
|
||||
case CommonConstants.ALERT_MODE_INDIVIDUAL:
|
||||
// Generate individual alerts for each matching log
|
||||
for (MatchingLogEvent matchingLog : matchingLogs) {
|
||||
generateIndividualAlert(matchingLog, currentTime);
|
||||
}
|
||||
break;
|
||||
|
||||
case CommonConstants.ALERT_MODE_GROUP:
|
||||
// Generate a single alert group for all matching logs
|
||||
generateGroupAlert(windowData, matchingLogs, currentTime);
|
||||
break;
|
||||
default:
|
||||
log.warn("Unknown alert mode for define {}: {}", alertDefine.getName(), alertMode);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void generateIndividualAlert(MatchingLogEvent matchingLog, long currentTime) {
|
||||
AlertDefine define = matchingLog.getAlertDefine();
|
||||
LogEntry logEntry = matchingLog.getLogEntry();
|
||||
|
||||
Map<String, String> alertLabels = new HashMap<>(8);
|
||||
|
||||
// Create fingerprints for group alert
|
||||
Map<String, String> commonFingerPrints = createCommonFingerprints(define);
|
||||
alertLabels.putAll(commonFingerPrints);
|
||||
// add the log data to commonFingerPrints
|
||||
addLogEntryToMap(logEntry, alertLabels);
|
||||
|
||||
Map<String, Object> fieldValueMap = createFieldValueMap(logEntry, define);
|
||||
Map<String, String> alertAnnotations = createAlertAnnotations(define, fieldValueMap);
|
||||
// Create and send group alert
|
||||
SingleAlert alert = SingleAlert.builder()
|
||||
.labels(alertLabels)
|
||||
.annotations(alertAnnotations)
|
||||
.content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap))
|
||||
.status(CommonConstants.ALERT_STATUS_FIRING)
|
||||
.triggerTimes(1)
|
||||
.startAt(currentTime)
|
||||
.activeAt(currentTime)
|
||||
.build();
|
||||
|
||||
alarmCommonReduce.reduceAndSendAlarm(alert.clone());
|
||||
|
||||
log.debug("Generated individual alert for define: {}", define.getName());
|
||||
}
|
||||
|
||||
private void generateGroupAlert(WindowAggregator.WindowData windowData,
|
||||
List<MatchingLogEvent> matchingLogs, long currentTime) {
|
||||
|
||||
List<SingleAlert> alerts = new ArrayList<>(matchingLogs.size());
|
||||
AlertDefine define = windowData.getAlertDefine();
|
||||
|
||||
// Create fingerprints for group alert
|
||||
Map<String, String> commonFingerPrints = createCommonFingerprints(define);
|
||||
|
||||
// Add window information to fingerprints
|
||||
commonFingerPrints.put(WINDOW_START_TIME, String.valueOf(windowData.getStartTime()));
|
||||
commonFingerPrints.put(WINDOW_END_TIME, String.valueOf(windowData.getEndTime()));
|
||||
commonFingerPrints.put(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_GROUP);
|
||||
commonFingerPrints.put(MATCHING_LOGS_COUNT, String.valueOf(matchingLogs.size()));
|
||||
|
||||
for (MatchingLogEvent event: matchingLogs) {
|
||||
LogEntry logEntry = event.getLogEntry();
|
||||
|
||||
Map<String, String> alertLabels = new HashMap<>(8);
|
||||
|
||||
alertLabels.putAll(commonFingerPrints);
|
||||
// add the log data to commonFingerPrints
|
||||
addLogEntryToMap(logEntry, alertLabels);
|
||||
|
||||
Map<String, Object> fieldValueMap = createFieldValueMap(logEntry, define);
|
||||
Map<String, String> alertAnnotations = createAlertAnnotations(define, fieldValueMap);
|
||||
// Create and send group alert
|
||||
SingleAlert alert = SingleAlert.builder()
|
||||
.labels(alertLabels)
|
||||
.annotations(alertAnnotations)
|
||||
.content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap))
|
||||
.status(CommonConstants.ALERT_STATUS_FIRING)
|
||||
.triggerTimes(matchingLogs.size())
|
||||
.startAt(currentTime)
|
||||
.activeAt(currentTime)
|
||||
.build();
|
||||
alerts.add(alert.clone());
|
||||
}
|
||||
alarmCommonReduce.reduceAndSendAlarmGroup(commonFingerPrints, alerts);
|
||||
|
||||
log.debug("Generated group alert for define: {} with {} matching logs",
|
||||
define.getName(), matchingLogs.size());
|
||||
}
|
||||
|
||||
private String getAlertMode(AlertDefine alertDefine) {
|
||||
String mode = null;
|
||||
if (alertDefine.getLabels() != null) {
|
||||
mode = alertDefine.getLabels().get(CommonConstants.ALERT_MODE_LABEL);
|
||||
}
|
||||
if (mode == null || mode.isEmpty()) {
|
||||
return CommonConstants.ALERT_MODE_GROUP; // Default to group mode if not specified
|
||||
} else {
|
||||
return mode;
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, String> createCommonFingerprints(AlertDefine define) {
|
||||
Map<String, String> fingerprints = new HashMap<>(8);
|
||||
fingerprints.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
|
||||
fingerprints.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(define.getId()));
|
||||
|
||||
if (define.getLabels() != null) {
|
||||
fingerprints.putAll(define.getLabels());
|
||||
}
|
||||
|
||||
return fingerprints;
|
||||
}
|
||||
|
||||
private Map<String, Object> createFieldValueMap(LogEntry logEntry, AlertDefine define) {
|
||||
Map<String, Object> fieldValueMap = new HashMap<>(8);
|
||||
fieldValueMap.put("log", logEntry);
|
||||
|
||||
if (define.getLabels() != null) {
|
||||
fieldValueMap.putAll(define.getLabels());
|
||||
}
|
||||
|
||||
return fieldValueMap;
|
||||
}
|
||||
|
||||
private Map<String, String> createAlertAnnotations(AlertDefine define, Map<String, Object> fieldValueMap) {
|
||||
Map<String, String> annotations = new HashMap<>(8);
|
||||
|
||||
if (define.getAnnotations() != null) {
|
||||
for (Map.Entry<String, String> entry : define.getAnnotations().entrySet()) {
|
||||
annotations.put(entry.getKey(),
|
||||
AlertTemplateUtil.render(entry.getValue(), fieldValueMap));
|
||||
}
|
||||
}
|
||||
|
||||
return annotations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the content from LogEntry object (except timestamp) to commonFingerPrints
|
||||
*
|
||||
* @param logEntry log entry object
|
||||
* @param context context
|
||||
*/
|
||||
private void addLogEntryToMap(LogEntry logEntry, Map<String, String> context) {
|
||||
// Add basic fields
|
||||
if (logEntry.getSeverityNumber() != null) {
|
||||
context.put("severityNumber", String.valueOf(logEntry.getSeverityNumber()));
|
||||
}
|
||||
if (logEntry.getSeverityText() != null) {
|
||||
context.put("severityText", logEntry.getSeverityText());
|
||||
}
|
||||
if (logEntry.getBody() != null) {
|
||||
context.put("body", String.valueOf(logEntry.getBody()));
|
||||
}
|
||||
if (logEntry.getDroppedAttributesCount() != null) {
|
||||
context.put("droppedAttributesCount", String.valueOf(logEntry.getDroppedAttributesCount()));
|
||||
}
|
||||
if (logEntry.getTraceId() != null) {
|
||||
context.put("traceId", logEntry.getTraceId());
|
||||
}
|
||||
if (logEntry.getSpanId() != null) {
|
||||
context.put("spanId", logEntry.getSpanId());
|
||||
}
|
||||
if (logEntry.getTraceFlags() != null) {
|
||||
context.put("traceFlags", String.valueOf(logEntry.getTraceFlags()));
|
||||
}
|
||||
|
||||
// Add attributes
|
||||
if (logEntry.getAttributes() != null && !logEntry.getAttributes().isEmpty()) {
|
||||
for (Map.Entry<String, Object> entry : logEntry.getAttributes().entrySet()) {
|
||||
if (entry.getValue() != null) {
|
||||
context.put("attr_" + entry.getKey(), String.valueOf(entry.getValue()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add resource
|
||||
if (logEntry.getResource() != null && !logEntry.getResource().isEmpty()) {
|
||||
for (Map.Entry<String, Object> entry : logEntry.getResource().entrySet()) {
|
||||
if (entry.getValue() != null) {
|
||||
context.put("resource_" + entry.getKey(), String.valueOf(entry.getValue()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add instrumentationScope
|
||||
if (logEntry.getInstrumentationScope() != null) {
|
||||
LogEntry.InstrumentationScope scope = logEntry.getInstrumentationScope();
|
||||
if (scope.getName() != null) {
|
||||
context.put("scope_name", scope.getName());
|
||||
}
|
||||
if (scope.getVersion() != null) {
|
||||
context.put("scope_version", scope.getVersion());
|
||||
}
|
||||
if (scope.getDroppedAttributesCount() != null) {
|
||||
context.put("scope_droppedAttributesCount", String.valueOf(scope.getDroppedAttributesCount()));
|
||||
}
|
||||
if (scope.getAttributes() != null && !scope.getAttributes().isEmpty()) {
|
||||
for (Map.Entry<String, Object> entry : scope.getAttributes().entrySet()) {
|
||||
if (entry.getValue() != null) {
|
||||
context.put("scope_attr_" + entry.getKey(), String.valueOf(entry.getValue()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-123
@@ -1,123 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.calculate.realtime.window;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.alert.AlerterWorkerPool;
|
||||
import org.apache.hertzbeat.alert.calculate.JexlExprCalculator;
|
||||
import org.apache.hertzbeat.alert.service.AlertDefineService;
|
||||
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
|
||||
import org.apache.hertzbeat.common.entity.log.LogEntry;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* LogWorker Responsible for:
|
||||
* 1. Receiving logs from WindowedLogRealTimeAlertCalculator
|
||||
* 2. Evaluating alert expressions against logs
|
||||
* 3. Sending matching logs to WindowAggregator
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class LogWorker {
|
||||
|
||||
private static final String LOG_PREFIX = "log";
|
||||
|
||||
private final AlertDefineService alertDefineService;
|
||||
private final JexlExprCalculator jexlExprCalculator;
|
||||
private final WindowAggregator windowAggregator;
|
||||
private final AlerterWorkerPool workerPool;
|
||||
|
||||
@Autowired
|
||||
public LogWorker(AlertDefineService alertDefineService,
|
||||
JexlExprCalculator jexlExprCalculator, WindowAggregator windowAggregator,
|
||||
AlerterWorkerPool workerPool) {
|
||||
this.alertDefineService = alertDefineService;
|
||||
this.jexlExprCalculator = jexlExprCalculator;
|
||||
this.windowAggregator = windowAggregator;
|
||||
this.workerPool = workerPool;
|
||||
}
|
||||
|
||||
public void reduceAndSendLogTask(LogEntry logEntry) {
|
||||
workerPool.executeLogJob(reduceLogTask(logEntry));
|
||||
}
|
||||
|
||||
public Runnable reduceLogTask(LogEntry logEntry) {
|
||||
return () -> {
|
||||
try {
|
||||
processLogEntry(logEntry);
|
||||
} catch (Exception e) {
|
||||
log.error("Error processing log entry in worker: {}", e.getMessage(), e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void processLogEntry(LogEntry logEntry) {
|
||||
// Get all log alert definitions
|
||||
List<AlertDefine> alertDefines = alertDefineService.getLogRealTimeAlertDefines();
|
||||
|
||||
// Create context for expression evaluation
|
||||
Map<String, Object> context = new HashMap<>(8);
|
||||
context.put(LOG_PREFIX, logEntry);
|
||||
|
||||
// Process each alert definition
|
||||
for (AlertDefine define : alertDefines) {
|
||||
if (define.getExpr() == null || define.getExpr().trim().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// Evaluate alert expression
|
||||
boolean match = jexlExprCalculator.execAlertExpression(context, define.getExpr(), false);
|
||||
|
||||
if (match) {
|
||||
// Create matching log event and send to WindowAggregator
|
||||
MatchingLogEvent event = createMatchingLogEvent(logEntry, define);
|
||||
windowAggregator.addMatchingLog(event);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Error evaluating expression for alert define {}: {}", define.getName(), e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private MatchingLogEvent createMatchingLogEvent(LogEntry logEntry, AlertDefine define) {
|
||||
long eventTimestamp = extractEventTimestamp(logEntry);
|
||||
|
||||
return MatchingLogEvent.builder()
|
||||
.logEntry(logEntry)
|
||||
.alertDefine(define)
|
||||
.eventTimestamp(eventTimestamp)
|
||||
.workerTimestamp(System.currentTimeMillis())
|
||||
.build();
|
||||
}
|
||||
|
||||
private long extractEventTimestamp(LogEntry logEntry) {
|
||||
if (logEntry.getTimeUnixNano() != null && logEntry.getTimeUnixNano() != 0) {
|
||||
return logEntry.getTimeUnixNano() / 1_000_000; // Convert to milliseconds
|
||||
}
|
||||
if (logEntry.getObservedTimeUnixNano() != null && logEntry.getObservedTimeUnixNano() != 0) {
|
||||
return logEntry.getObservedTimeUnixNano() / 1_000_000; // Convert to milliseconds
|
||||
}
|
||||
return System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
-213
@@ -1,213 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.calculate.realtime.window;
|
||||
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* Watermark Generator responsible for:
|
||||
* 1. Receiving maxTimestamp updates from WindowedLogRealTimeAlertCalculator
|
||||
* 2. Calculating watermarks based on configurable delay
|
||||
* 3. Broadcasting watermarks to all subscribers (WindowAggregator)
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class TimeService {
|
||||
|
||||
static final long DEFAULT_WATERMARK_DELAY_MS = 30_000; // 30 seconds
|
||||
private static final long WATERMARK_BROADCAST_INTERVAL_MS = 5_000; // 5 seconds
|
||||
|
||||
// Define acceptable timestamp range to filter abnormal timestamps
|
||||
private static final long MAX_FUTURE_TIME_MS = 60_000; // Allow 1 minute future time
|
||||
private static final long MAX_PAST_TIME_MS = 24 * 60 * 60 * 1000; // Allow 24 hours past time
|
||||
|
||||
private final AtomicLong maxTimestamp = new AtomicLong(0);
|
||||
private final AtomicLong currentWatermark = new AtomicLong(0);
|
||||
private final CopyOnWriteArrayList<WatermarkListener> listeners = new CopyOnWriteArrayList<>();
|
||||
private ScheduledExecutorService scheduler;
|
||||
|
||||
public TimeService(List<WatermarkListener> initialListeners) {
|
||||
listeners.addAll(initialListeners);
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void start() {
|
||||
// Create internal scheduled executor
|
||||
ThreadFactory threadFactory = new ThreadFactoryBuilder()
|
||||
.setUncaughtExceptionHandler((thread, throwable) -> {
|
||||
log.error("TimeService scheduler has uncaughtException.");
|
||||
log.error(throwable.getMessage(), throwable);
|
||||
})
|
||||
.setDaemon(true)
|
||||
.setNameFormat("timeservice-scheduler-%d")
|
||||
.build();
|
||||
|
||||
this.scheduler = Executors.newSingleThreadScheduledExecutor(threadFactory);
|
||||
|
||||
// Start watermark broadcast scheduler
|
||||
scheduler.scheduleAtFixedRate(
|
||||
this::broadcastWatermark,
|
||||
0,
|
||||
WATERMARK_BROADCAST_INTERVAL_MS,
|
||||
TimeUnit.MILLISECONDS
|
||||
);
|
||||
|
||||
log.info("TimeService started with watermark delay: {}ms", DEFAULT_WATERMARK_DELAY_MS);
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void stop() {
|
||||
if (scheduler != null && !scheduler.isShutdown()) {
|
||||
log.info("Shutting down TimeService scheduler...");
|
||||
scheduler.shutdown();
|
||||
try {
|
||||
if (!scheduler.awaitTermination(10, TimeUnit.SECONDS)) {
|
||||
log.warn("TimeService scheduler did not terminate within 10 seconds, forcing shutdown");
|
||||
scheduler.shutdownNow();
|
||||
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
log.error("TimeService scheduler did not terminate");
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
log.warn("Interrupted while waiting for TimeService scheduler to terminate");
|
||||
scheduler.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
log.info("TimeService stopped");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if timestamp is within acceptable range
|
||||
*/
|
||||
public boolean isValidTimestamp(long timestamp) {
|
||||
long currentTime = System.currentTimeMillis();
|
||||
return timestamp >= (currentTime - MAX_PAST_TIME_MS) && timestamp <= (currentTime + MAX_FUTURE_TIME_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if data is late based on current watermark
|
||||
*/
|
||||
public boolean isLateData(long timestamp) {
|
||||
return timestamp < getCurrentWatermark();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update max timestamp from WindowedLogRealTimeAlertCalculator
|
||||
*/
|
||||
public void updateMaxTimestamp(long timestamp) {
|
||||
maxTimestamp.getAndUpdate(current -> Math.max(current, timestamp));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add watermark listener
|
||||
*/
|
||||
public void addWatermarkListener(WatermarkListener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove watermark listener
|
||||
*/
|
||||
public void removeWatermarkListener(WatermarkListener listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current watermark
|
||||
*/
|
||||
public long getCurrentWatermark() {
|
||||
return currentWatermark.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get max timestamp
|
||||
*/
|
||||
public long getMaxTimestamp() {
|
||||
return maxTimestamp.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate and broadcast watermark
|
||||
*/
|
||||
private void broadcastWatermark() {
|
||||
|
||||
try {
|
||||
long maxTs = maxTimestamp.get();
|
||||
if (maxTs <= 0) {
|
||||
return;
|
||||
}
|
||||
// Calculate watermark: maxTimestamp - delay
|
||||
long newWatermark = maxTs - DEFAULT_WATERMARK_DELAY_MS;
|
||||
long currentWm = currentWatermark.get();
|
||||
|
||||
// Only advance watermark (monotonic property)
|
||||
if (newWatermark <= currentWm) {
|
||||
return;
|
||||
}
|
||||
if (currentWatermark.compareAndSet(currentWm, newWatermark)) {
|
||||
// Broadcast to all listeners
|
||||
Watermark watermark = new Watermark(newWatermark);
|
||||
for (WatermarkListener listener : listeners) {
|
||||
try {
|
||||
listener.onWatermark(watermark);
|
||||
} catch (Exception e) {
|
||||
log.error("Error notifying watermark listener: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
log.debug("Broadcast watermark: {} (maxTimestamp: {})", newWatermark, maxTs);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error in watermark broadcast: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Watermark data class
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public static class Watermark {
|
||||
private final long timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for watermark listeners
|
||||
*/
|
||||
public interface WatermarkListener {
|
||||
void onWatermark(Watermark watermark);
|
||||
}
|
||||
}
|
||||
-251
@@ -1,251 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.calculate.realtime.window;
|
||||
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
|
||||
/**
|
||||
* Window Aggregator Responsible for:
|
||||
* 1. Managing window data structure for all active windows
|
||||
* 2. Receiving matching logs from Workers
|
||||
* 3. Receiving watermarks from TimeService
|
||||
* 4. Sending closed windows to AlarmEvaluator
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class WindowAggregator implements TimeService.WatermarkListener, Runnable {
|
||||
|
||||
private static final long DEFAULT_WINDOW_SIZE_MS = 1 * 60 * 1000; // 1 minutes
|
||||
|
||||
private final AlarmEvaluator alarmEvaluator;
|
||||
private final BlockingQueue<MatchingLogEvent> eventQueue = new LinkedBlockingQueue<>();
|
||||
private final Map<WindowKey, WindowData> activeWindows = new HashMap<>();
|
||||
private final Object windowLock = new Object();
|
||||
|
||||
private ExecutorService aggregatorExecutor;
|
||||
|
||||
public WindowAggregator(AlarmEvaluator alarmEvaluator) {
|
||||
this.alarmEvaluator = alarmEvaluator;
|
||||
}
|
||||
|
||||
public void addMatchingLog(MatchingLogEvent event) {
|
||||
try {
|
||||
eventQueue.put(event);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.warn("Interrupted while adding matching log to aggregator");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWatermark(TimeService.Watermark watermark) {
|
||||
List<WindowData> closedWindows;
|
||||
|
||||
synchronized (windowLock) {
|
||||
closedWindows = new ArrayList<>();
|
||||
|
||||
// Find windows that should be closed based on watermark
|
||||
Iterator<Map.Entry<WindowKey, WindowData>> iterator = activeWindows.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<WindowKey, WindowData> entry = iterator.next();
|
||||
WindowData windowData = entry.getValue();
|
||||
|
||||
// Close window if its end time <= watermark timestamp
|
||||
if (windowData.getEndTime() <= watermark.getTimestamp()) {
|
||||
closedWindows.add(windowData);
|
||||
iterator.remove();
|
||||
|
||||
log.debug("Closing window: {} with {} matching logs",
|
||||
entry.getKey(), windowData.getMatchingLogs().size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (WindowData windowData : closedWindows) {
|
||||
alarmEvaluator.sendAndProcessWindowData(windowData);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
try {
|
||||
MatchingLogEvent event = eventQueue.take();
|
||||
processMatchingLogEvent(event);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
} catch (Exception e) {
|
||||
log.error("Error processing matching log event: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processMatchingLogEvent(MatchingLogEvent event) {
|
||||
|
||||
// Determine window size from alert define (if specified) or use default
|
||||
long windowSizeMs = getWindowSize(event.getAlertDefine());
|
||||
|
||||
// Calculate window boundaries
|
||||
long eventTime = event.getEventTimestamp();
|
||||
long windowStart = (eventTime / windowSizeMs) * windowSizeMs;
|
||||
long windowEnd = windowStart + windowSizeMs;
|
||||
|
||||
// Create window key
|
||||
WindowKey windowKey = new WindowKey(
|
||||
event.getAlertDefine().getId(),
|
||||
windowStart,
|
||||
windowEnd
|
||||
);
|
||||
synchronized (windowLock) {
|
||||
// Get or create window data
|
||||
WindowData windowData = activeWindows.computeIfAbsent(windowKey,
|
||||
key -> new WindowData(key, event.getAlertDefine()));
|
||||
// Add matching log to window
|
||||
windowData.addMatchingLog(event);
|
||||
log.debug("Added matching log to window: {} (total logs: {})",
|
||||
windowKey, windowData.getMatchingLogs().size());
|
||||
}
|
||||
}
|
||||
|
||||
private long getWindowSize(AlertDefine alertDefine) {
|
||||
// Check if alert define has custom window size configuration
|
||||
if (alertDefine.getPeriod() != null) {
|
||||
return alertDefine.getPeriod() * 1000; // Convert seconds to milliseconds
|
||||
}
|
||||
log.info("Using default window size of {} ms for alert define: {}",
|
||||
DEFAULT_WINDOW_SIZE_MS, alertDefine.getName());
|
||||
return DEFAULT_WINDOW_SIZE_MS;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void start() {
|
||||
// Create internal executor
|
||||
ThreadFactory threadFactory = new ThreadFactoryBuilder()
|
||||
.setUncaughtExceptionHandler((thread, throwable) -> {
|
||||
log.error("WindowAggregator executor has uncaughtException.");
|
||||
log.error(throwable.getMessage(), throwable);
|
||||
})
|
||||
.setDaemon(true)
|
||||
.setNameFormat("window-aggregator-%d")
|
||||
.build();
|
||||
|
||||
aggregatorExecutor = Executors.newSingleThreadExecutor(threadFactory);
|
||||
|
||||
// Submit aggregation task
|
||||
aggregatorExecutor.submit(this);
|
||||
|
||||
log.info("WindowAggregator started");
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void stop() {
|
||||
if (aggregatorExecutor != null && !aggregatorExecutor.isShutdown()) {
|
||||
log.info("Shutting down WindowAggregator executor...");
|
||||
aggregatorExecutor.shutdown();
|
||||
try {
|
||||
if (!aggregatorExecutor.awaitTermination(10, TimeUnit.SECONDS)) {
|
||||
log.warn("WindowAggregator executor did not terminate within 10 seconds, forcing shutdown");
|
||||
aggregatorExecutor.shutdownNow();
|
||||
if (!aggregatorExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
log.error("WindowAggregator executor did not terminate");
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
log.warn("Interrupted while waiting for WindowAggregator executor to terminate");
|
||||
aggregatorExecutor.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
log.info("WindowAggregator stopped");
|
||||
}
|
||||
|
||||
/**
|
||||
* Window key for identifying unique windows
|
||||
*/
|
||||
@Data
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public static class WindowKey {
|
||||
private final long alertDefineId;
|
||||
private final long startTime;
|
||||
private final long endTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Window data container
|
||||
*/
|
||||
public static class WindowData {
|
||||
@Getter
|
||||
private final WindowKey windowKey;
|
||||
@Getter
|
||||
private final AlertDefine alertDefine;
|
||||
private final List<MatchingLogEvent> matchingLogs = new ArrayList<>();
|
||||
@Getter
|
||||
private final long createdTime;
|
||||
|
||||
public WindowData(WindowKey windowKey, AlertDefine alertDefine) {
|
||||
this.windowKey = windowKey;
|
||||
this.alertDefine = alertDefine;
|
||||
this.createdTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public void addMatchingLog(MatchingLogEvent event) {
|
||||
matchingLogs.add(event);
|
||||
}
|
||||
|
||||
public List<MatchingLogEvent> getMatchingLogs() {
|
||||
return new ArrayList<>(matchingLogs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get start time of the window
|
||||
* @return start time
|
||||
*/
|
||||
public long getStartTime() { return windowKey.getStartTime(); }
|
||||
|
||||
/**
|
||||
* Get end time of the window
|
||||
* @return end time
|
||||
*/
|
||||
public long getEndTime() { return windowKey.getEndTime(); }
|
||||
}
|
||||
}
|
||||
-18
@@ -18,8 +18,6 @@
|
||||
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;
|
||||
@@ -68,22 +66,6 @@ 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 () -> {
|
||||
|
||||
+1
-14
@@ -159,20 +159,7 @@ 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);
|
||||
}
|
||||
|
||||
+3
-17
@@ -22,7 +22,6 @@ 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;
|
||||
@@ -107,23 +106,10 @@ public interface AlertDefineService {
|
||||
void importConfig(MultipartFile file) throws Exception;
|
||||
|
||||
/**
|
||||
* Get the real-time metrics alarm definition list
|
||||
* @return Real-time metrics alarm definition list
|
||||
* Get the real-time alarm definition list
|
||||
* @return Real-time alarm definition list
|
||||
*/
|
||||
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);
|
||||
List<AlertDefine> getRealTimeAlertDefines();
|
||||
|
||||
/**
|
||||
* Get define preview
|
||||
|
||||
-8
@@ -32,12 +32,4 @@ 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);
|
||||
}
|
||||
|
||||
+7
-42
@@ -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.periodic.PeriodicAlertRuleScheduler;
|
||||
import org.apache.hertzbeat.alert.calculate.PeriodicAlertRuleScheduler;
|
||||
import org.apache.hertzbeat.alert.dao.AlertDefineDao;
|
||||
import org.apache.hertzbeat.alert.service.AlertDefineImExportService;
|
||||
import org.apache.hertzbeat.alert.service.AlertDefineService;
|
||||
@@ -87,8 +87,7 @@ public class AlertDefineServiceImpl implements AlertDefineService {
|
||||
@Override
|
||||
public void validate(AlertDefine alertDefine, boolean isModify) throws IllegalArgumentException {
|
||||
if (StringUtils.hasText(alertDefine.getExpr())) {
|
||||
if (CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_REALTIME.equals(alertDefine.getType())
|
||||
|| CommonConstants.LOG_ALERT_THRESHOLD_TYPE_REALTIME.equals(alertDefine.getType())) {
|
||||
if (CommonConstants.ALERT_THRESHOLD_TYPE_REALTIME.equals(alertDefine.getType())) {
|
||||
try {
|
||||
JexlExpressionRunner.compile(alertDefine.getExpr());
|
||||
} catch (Exception e) {
|
||||
@@ -215,21 +214,11 @@ public class AlertDefineServiceImpl implements AlertDefineService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AlertDefine> getMetricsRealTimeAlertDefines() {
|
||||
List<AlertDefine> alertDefines = CacheFactory.getMetricsAlertDefineCache();
|
||||
public List<AlertDefine> getRealTimeAlertDefines() {
|
||||
List<AlertDefine> alertDefines = CacheFactory.getAlertDefineCache();
|
||||
if (alertDefines == null) {
|
||||
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);
|
||||
alertDefines = alertDefineDao.findAlertDefinesByTypeAndEnableTrue(CommonConstants.ALERT_THRESHOLD_TYPE_REALTIME);
|
||||
CacheFactory.setAlertDefineCache(alertDefines);
|
||||
}
|
||||
return alertDefines;
|
||||
}
|
||||
@@ -240,35 +229,11 @@ public class AlertDefineServiceImpl implements AlertDefineService {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
switch (type) {
|
||||
case CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_PERIODIC:
|
||||
case CommonConstants.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);
|
||||
}
|
||||
}
|
||||
|
||||
-23
@@ -95,29 +95,6 @@ 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);
|
||||
|
||||
+3
-119
@@ -17,8 +17,6 @@
|
||||
|
||||
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;
|
||||
@@ -35,11 +33,10 @@ public final class AlertTemplateUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* Match the variable ${key} or ${key.property.subproperty}
|
||||
* Match the variable ${key}
|
||||
* 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)) {
|
||||
@@ -53,8 +50,7 @@ public final class AlertTemplateUtil {
|
||||
Matcher matcher = PATTERN.matcher(template);
|
||||
StringBuilder builder = new StringBuilder();
|
||||
while (matcher.find()) {
|
||||
String propertyPath = matcher.group(1);
|
||||
Object objectValue = getNestedProperty(replaceData, propertyPath);
|
||||
Object objectValue = replaceData.getOrDefault(matcher.group(1), "NullValue");
|
||||
String value = objectValue != null ? objectValue.toString() : "NullValue";
|
||||
matcher.appendReplacement(builder, Matcher.quoteReplacement(value));
|
||||
}
|
||||
@@ -65,116 +61,4 @@ 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,36 +52,4 @@ 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());
|
||||
}
|
||||
}
|
||||
|
||||
+4
-5
@@ -15,9 +15,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.calculate.periodic;
|
||||
package org.apache.hertzbeat.alert.calculate;
|
||||
|
||||
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;
|
||||
@@ -51,10 +50,10 @@ import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Test case for {@link MetricsPeriodicAlertCalculator}
|
||||
* Test case for {@link PeriodicAlertCalculator}
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class MetricsPeriodicAlertCalculatorTest {
|
||||
class PeriodicAlertCalculatorTest {
|
||||
|
||||
@Mock
|
||||
private DataSourceService dataSourceService;
|
||||
@@ -66,7 +65,7 @@ class MetricsPeriodicAlertCalculatorTest {
|
||||
private AlarmCacheManager alarmCacheManager;
|
||||
|
||||
@InjectMocks
|
||||
private MetricsPeriodicAlertCalculator periodicAlertCalculator;
|
||||
private PeriodicAlertCalculator periodicAlertCalculator;
|
||||
|
||||
private AlertDefine rule;
|
||||
|
||||
+11
-14
@@ -15,12 +15,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.calculate.realtime;
|
||||
package org.apache.hertzbeat.alert.calculate;
|
||||
|
||||
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;
|
||||
@@ -50,7 +48,7 @@ import static org.mockito.Mockito.when;
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class MetricsRealTimeAlertCalculatorMatchTest {
|
||||
public class RealTimeAlertCalculatorMatchTest {
|
||||
|
||||
private final AlerterWorkerPool workerPool = new AlerterWorkerPool();
|
||||
|
||||
@@ -69,20 +67,19 @@ public class MetricsRealTimeAlertCalculatorMatchTest {
|
||||
@Mock
|
||||
private AlarmCacheManager alarmCacheManager;
|
||||
|
||||
private MetricsRealTimeAlertCalculator metricsRealTimeAlertCalculator;
|
||||
private RealTimeAlertCalculator realTimeAlertCalculator;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
when(singleAlertDao.querySingleAlertsByStatus(any())).thenReturn(new ArrayList<>());
|
||||
metricsRealTimeAlertCalculator = new MetricsRealTimeAlertCalculator(
|
||||
realTimeAlertCalculator = new RealTimeAlertCalculator(
|
||||
workerPool,
|
||||
dataQueue,
|
||||
alertDefineService,
|
||||
singleAlertDao,
|
||||
alarmCommonReduce,
|
||||
alarmCacheManager,
|
||||
new JexlExprCalculator(),
|
||||
false
|
||||
);
|
||||
}
|
||||
@@ -102,7 +99,7 @@ public class MetricsRealTimeAlertCalculatorMatchTest {
|
||||
|
||||
List<AlertDefine> allDefines = Collections.singletonList(matchDefine);
|
||||
|
||||
List<AlertDefine> filtered = metricsRealTimeAlertCalculator.filterThresholdsByAppAndMetrics(allDefines, app, "", Map.of(), instanceId, priority);
|
||||
List<AlertDefine> filtered = realTimeAlertCalculator.filterThresholdsByAppAndMetrics(allDefines, app, "", Map.of(), instanceId, priority);
|
||||
|
||||
// It should filter out 999999999.
|
||||
assertEquals(1, filtered.size());
|
||||
@@ -148,10 +145,10 @@ public class MetricsRealTimeAlertCalculatorMatchTest {
|
||||
|
||||
List<AlertDefine> allDefines = Collections.singletonList(matchDefine);
|
||||
|
||||
when(alertDefineService.getMetricsRealTimeAlertDefines()).thenReturn(allDefines);
|
||||
when(alertDefineService.getRealTimeAlertDefines()).thenReturn(allDefines);
|
||||
when(dataQueue.pollMetricsDataToAlerter()).thenReturn(metricsData).thenThrow(new InterruptedException());
|
||||
|
||||
metricsRealTimeAlertCalculator.startCalculate();
|
||||
realTimeAlertCalculator.startCalculate();
|
||||
|
||||
Thread.sleep(3000);
|
||||
|
||||
@@ -192,10 +189,10 @@ public class MetricsRealTimeAlertCalculatorMatchTest {
|
||||
|
||||
List<AlertDefine> allDefines = Collections.singletonList(matchDefine);
|
||||
|
||||
when(alertDefineService.getMetricsRealTimeAlertDefines()).thenReturn(allDefines);
|
||||
when(alertDefineService.getRealTimeAlertDefines()).thenReturn(allDefines);
|
||||
when(dataQueue.pollMetricsDataToAlerter()).thenReturn(metricsData).thenThrow(new InterruptedException());
|
||||
|
||||
metricsRealTimeAlertCalculator.startCalculate();
|
||||
realTimeAlertCalculator.startCalculate();
|
||||
|
||||
Thread.sleep(3000);
|
||||
|
||||
@@ -242,10 +239,10 @@ public class MetricsRealTimeAlertCalculatorMatchTest {
|
||||
|
||||
List<AlertDefine> allDefines = Collections.singletonList(matchDefine);
|
||||
|
||||
when(alertDefineService.getMetricsRealTimeAlertDefines()).thenReturn(allDefines);
|
||||
when(alertDefineService.getRealTimeAlertDefines()).thenReturn(allDefines);
|
||||
when(dataQueue.pollMetricsDataToAlerter()).thenReturn(metricsData).thenThrow(new InterruptedException());
|
||||
|
||||
metricsRealTimeAlertCalculator.startCalculate();
|
||||
realTimeAlertCalculator.startCalculate();
|
||||
|
||||
Thread.sleep(3000);
|
||||
|
||||
+4
-7
@@ -15,13 +15,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.calculate.realtime;
|
||||
package org.apache.hertzbeat.alert.calculate;
|
||||
|
||||
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;
|
||||
@@ -37,9 +35,9 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
class MetricsRealTimeAlertCalculatorTest {
|
||||
class RealTimeAlertCalculatorTest {
|
||||
|
||||
private MetricsRealTimeAlertCalculator calculator;
|
||||
private RealTimeAlertCalculator calculator;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
@@ -53,8 +51,7 @@ class MetricsRealTimeAlertCalculatorTest {
|
||||
Mockito.when(mockDao.querySingleAlertsByStatus(Mockito.anyString()))
|
||||
.thenReturn(Collections.emptyList());
|
||||
|
||||
calculator = new MetricsRealTimeAlertCalculator(mockPool, mockQueue, mockAlertDefineService
|
||||
, mockDao, mockReduce, alarmCacheManager, new JexlExprCalculator(), false);
|
||||
calculator = new RealTimeAlertCalculator(mockPool, mockQueue, mockAlertDefineService, mockDao, mockReduce, alarmCacheManager, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
-252
@@ -1,252 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.calculate.periodic;
|
||||
|
||||
import 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;
|
||||
}
|
||||
}
|
||||
-189
@@ -1,189 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.calculate.realtime;
|
||||
|
||||
import 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();
|
||||
}
|
||||
|
||||
}
|
||||
-346
@@ -1,346 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.calculate.realtime.window;
|
||||
|
||||
import 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
|
||||
}
|
||||
}
|
||||
-239
@@ -1,239 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.calculate.realtime.window;
|
||||
|
||||
import 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"));
|
||||
}
|
||||
}
|
||||
-291
@@ -1,291 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.calculate.realtime.window;
|
||||
|
||||
import 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());
|
||||
}
|
||||
}
|
||||
-239
@@ -1,239 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.calculate.realtime.window;
|
||||
|
||||
import 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));
|
||||
}
|
||||
}
|
||||
+6
-167
@@ -18,18 +18,15 @@
|
||||
package org.apache.hertzbeat.alert.service;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import org.apache.hertzbeat.alert.calculate.periodic.PeriodicAlertRuleScheduler;
|
||||
import org.apache.hertzbeat.alert.calculate.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;
|
||||
@@ -42,10 +39,8 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
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.apache.hertzbeat.common.constants.CommonConstants.ALERT_THRESHOLD_TYPE_PERIODIC;
|
||||
import static org.apache.hertzbeat.common.constants.CommonConstants.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;
|
||||
@@ -169,171 +164,15 @@ class AlertDefineServiceTest {
|
||||
}
|
||||
};
|
||||
when(dataSourceService.calculate(eq("promql"), eq(expr))).thenReturn(Lists.newArrayList(countValue1));
|
||||
List<Map<String, Object>> result = alertDefineService.getDefinePreview("promql", METRIC_ALERT_THRESHOLD_TYPE_PERIODIC, expr);
|
||||
List<Map<String, Object>> result = alertDefineService.getDefinePreview("promql", ALERT_THRESHOLD_TYPE_PERIODIC, expr);
|
||||
assertNotNull(result);
|
||||
assertEquals(1307, result.get(0).get("__value__"));
|
||||
|
||||
result = alertDefineService.getDefinePreview("promql", METRIC_ALERT_THRESHOLD_TYPE_PERIODIC, null);
|
||||
result = alertDefineService.getDefinePreview("promql", ALERT_THRESHOLD_TYPE_PERIODIC, null);
|
||||
assertEquals(0, result.size());
|
||||
|
||||
result = alertDefineService.getDefinePreview("promql", METRIC_ALERT_THRESHOLD_TYPE_REALTIME, null);
|
||||
result = alertDefineService.getDefinePreview("promql", 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"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-195
@@ -87,199 +87,4 @@ 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));
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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.value(dns.getRecordType()), DClass.value(dns.getQueryClass())));
|
||||
Message query = Message.newQuery(Record.newRecord(name, Type.ANY, 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()));
|
||||
|
||||
+1
-2
@@ -65,7 +65,6 @@ import org.apache.http.impl.auth.DigestScheme;
|
||||
import org.apache.http.impl.client.BasicAuthCache;
|
||||
import org.apache.http.impl.client.BasicCredentialsProvider;
|
||||
import org.apache.http.protocol.HttpContext;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@@ -277,7 +276,7 @@ public class PrometheusAutoCollectImpl implements PrometheusCollect {
|
||||
}
|
||||
|
||||
// if it has payload, would override post params
|
||||
if (StringUtils.hasLength(protocol.getPayload()) && (HttpMethod.POST.matches(protocol.getMethod()) || HttpMethod.PUT.matches(protocol.getMethod()))) {
|
||||
if (StringUtils.hasLength(protocol.getPayload())) {
|
||||
requestBuilder.setEntity(new StringEntity(protocol.getPayload(), StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
|
||||
+2
-127
@@ -23,13 +23,11 @@ import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
|
||||
@@ -43,7 +41,6 @@ public class OnlineParser {
|
||||
|
||||
private static final char RIGHT_BRACKET = '}';
|
||||
private static final char LEFT_BRACKET = '{';
|
||||
private static final char UTF8_REPLACEMENT_CHARACTER = '\uFFFD';
|
||||
|
||||
static {
|
||||
escapeMap.put((int) 'n', (int) '\n');
|
||||
@@ -355,135 +352,14 @@ public class OnlineParser {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (i <= 127) {
|
||||
stringBuilder.append((char) i);
|
||||
} else {
|
||||
handleUtf8Character(i, inputStream, stringBuilder);
|
||||
stringBuilder.append((char) i);
|
||||
}
|
||||
i = inputStream.read();
|
||||
}
|
||||
return new CharChecker(i);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles multi-byte UTF-8 character parsing from input stream.
|
||||
* Reads additional bytes based on the first byte and validates the UTF-8 sequence.
|
||||
* Appends the decoded character to the string builder or replacement character if invalid.
|
||||
*
|
||||
* @param firstByte the first byte of the UTF-8 character sequence
|
||||
* @param inputStream the input stream to read additional bytes from
|
||||
* @param stringBuilder the string builder to append the decoded character to
|
||||
* @throws IOException if an I/O error occurs while reading from the input stream
|
||||
*/
|
||||
private static void handleUtf8Character(int firstByte, InputStream inputStream, StringBuilder stringBuilder) throws IOException {
|
||||
byte[] byteArray = new byte[4];
|
||||
byteArray[0] = (byte) firstByte;
|
||||
int additionalBytes = calculateUtf8ContinuationBytes(firstByte);
|
||||
if (additionalBytes == -1) {
|
||||
appendInvalidCharacters(stringBuilder);
|
||||
return;
|
||||
}
|
||||
int totalBytes = 1;
|
||||
|
||||
for (int i = 0; i < additionalBytes; i++) {
|
||||
int nextByte = inputStream.read();
|
||||
if (nextByte == -1) {
|
||||
appendInvalidCharacters(stringBuilder);
|
||||
return;
|
||||
}
|
||||
// Verify subsequent byte format:10xxxxxx
|
||||
if ((nextByte & 0xC0) != 0x80) {
|
||||
appendInvalidCharacters(stringBuilder);
|
||||
return;
|
||||
}
|
||||
byteArray[i + 1] = (byte) nextByte;
|
||||
totalBytes++;
|
||||
}
|
||||
try {
|
||||
// todo: If stricter UTF-8 semantic validation is required,boundary conditions are slightly strengthened.
|
||||
String utf8Chars = new String(byteArray, 0, totalBytes, StandardCharsets.UTF_8);
|
||||
stringBuilder.append(utf8Chars);
|
||||
} catch (Exception e) {
|
||||
log.debug("Invalid UTF-8 sequence detected at firstByte: {}", Integer.toHexString(firstByte));
|
||||
appendInvalidCharacters(stringBuilder);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends the UTF-8 replacement character (\uFFFD) to the StringBuilder.
|
||||
* This method is used to append the replacement character to the string builder
|
||||
* when invalid UTF-8 byte sequences are encountered during parsing.
|
||||
* The replacement character (U+FFFD) is a special Unicode character used to
|
||||
* represent characters that cannot be decoded properly.
|
||||
*
|
||||
* @param stringBuilder the string builder to append to, no operation if null
|
||||
*/
|
||||
private static void appendInvalidCharacters(StringBuilder stringBuilder) {
|
||||
Optional.ofNullable(stringBuilder).ifPresent(t -> t.append(UTF8_REPLACEMENT_CHARACTER));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a label value contains invalid characters.
|
||||
* This method is used to validate the validity of Prometheus label values.
|
||||
* If the label value contains the UTF-8 replacement character (\uFFFD),
|
||||
* it is considered invalid because the replacement character indicates that
|
||||
* byte sequences that could not be properly decoded were encountered during parsing.
|
||||
*
|
||||
* According to Prometheus specifications, label values should not contain
|
||||
* replacement characters as this would cause data inconsistency and query issues.
|
||||
*
|
||||
* @param labelValue the label value to check
|
||||
* @return true if the label value is not blank and contains UTF-8 replacement character, false otherwise
|
||||
*/
|
||||
private static boolean isInvalidLabelValue(String labelValue) {
|
||||
return StringUtils.isNotBlank(labelValue) && labelValue.contains(String.valueOf(UTF8_REPLACEMENT_CHARACTER));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the number of continuation bytes needed for a UTF-8 character.
|
||||
* This method analyzes the first byte of a UTF-8 encoding to determine how many
|
||||
* continuation bytes (10xxxxxx pattern) are required to complete the character.
|
||||
*
|
||||
* UTF-8 encoding rules:
|
||||
* - 1-byte character: 0xxxxxxx (ASCII characters, this method won't be called)
|
||||
* - 2-byte character: 110xxxxx 10xxxxxx (returns 1 continuation byte)
|
||||
* - 3-byte character: 1110xxxx 10xxxxxx 10xxxxxx (returns 2 continuation bytes)
|
||||
* - 4-byte character: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx (returns 3 continuation bytes)
|
||||
*
|
||||
* Also validates byte sequences:
|
||||
* - 0xC0 and 0xC1: overlong encoding, invalid
|
||||
* - 0xF5-0xFF: out of Unicode range, invalid
|
||||
*
|
||||
* Note: A basic Overlong Encoding check has been added here.
|
||||
* If stricter and more comprehensive validation is required later,
|
||||
* this method should be separated out to serve solely as a check for the first character.
|
||||
*
|
||||
* @param firstByte the first byte of the UTF-8 character sequence
|
||||
* @return the number of continuation bytes needed, or -1 if the first byte is invalid
|
||||
* - returns 1: 2-byte character (needs 1 continuation byte)
|
||||
* - returns 2: 3-byte character (needs 2 continuation bytes)
|
||||
* - returns 3: 4-byte character (needs 3 continuation bytes)
|
||||
* - returns -1: invalid first byte
|
||||
*/
|
||||
private static int calculateUtf8ContinuationBytes(int firstByte) {
|
||||
if ((firstByte & 0xE0) == 0xC0) {
|
||||
if (firstByte <= 0xC1) {
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
if ((firstByte & 0xF0) == 0xE0) {
|
||||
return 2;
|
||||
}
|
||||
if ((firstByte & 0xF8) == 0xF0) {
|
||||
if (firstByte >= 0xF5) {
|
||||
return -1;
|
||||
}
|
||||
return 3;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static CharChecker skipSpaces(InputStream inputStream) throws IOException, FormatException {
|
||||
int i = getChar(inputStream);
|
||||
while (i == ' ') {
|
||||
@@ -540,8 +416,7 @@ public class OnlineParser {
|
||||
skipSpaces(inputStream).maybeQuotationMark().noElse();
|
||||
parseLabelValue(inputStream, stringBuilder).maybeQuotationMark().noElse();
|
||||
String labelValue = stringBuilder.toString();
|
||||
if (isInvalidLabelValue(labelValue)) {
|
||||
log.error("Invalid UTF-8 sequence detected at labelValue.");
|
||||
if (!labelValue.equals(new String(labelValue.getBytes(StandardCharsets.UTF_8)))) {
|
||||
throw new FormatException();
|
||||
}
|
||||
label.setValue(labelValue);
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ import java.util.Arrays;
|
||||
@Slf4j
|
||||
public class DnsSdCollectImpl extends AbstractCollect {
|
||||
|
||||
private static final int DEFAULT_TIME_OUT = 3000;
|
||||
private static final int DEFAULT_TIME_OUT = 3;
|
||||
|
||||
@Override
|
||||
public void preCheck(Metrics metrics) throws IllegalArgumentException {
|
||||
|
||||
-1
@@ -46,7 +46,6 @@ public class DnsCollectImplTest {
|
||||
.queryClass("IN")
|
||||
.address("www.google.com")
|
||||
.timeout("3000")
|
||||
.recordType("A")
|
||||
.port("53")
|
||||
.tcp("tcp")
|
||||
.build();
|
||||
|
||||
-151
@@ -29,7 +29,6 @@ import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
/**
|
||||
* Parse a Single metric in prometheus test
|
||||
@@ -445,157 +444,7 @@ public class OnlineParserSingleTest {
|
||||
assertEquals("NT AUTHORITY\nLocalService", metricFamily.getMetricList().get(0).getLabels().get(3).getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseMetricsWithChineseLabels() throws Exception {
|
||||
String str = "ST22{Dump_Name=\"Dump总数\",HostName=\"SAP_DEV\",instance_hostname=\"sapdev\"} 2\n";
|
||||
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
|
||||
Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "ST22");
|
||||
|
||||
assertNotNull(metricFamilyMap);
|
||||
MetricFamily metricFamily = metricFamilyMap.get("ST22");
|
||||
assertNotNull(metricFamily);
|
||||
assertEquals("ST22", metricFamily.getName());
|
||||
assertEquals(1, metricFamily.getMetricList().size());
|
||||
assertEquals(2.0, metricFamily.getMetricList().get(0).getValue());
|
||||
|
||||
// Verify Chinese label value is correctly parsed
|
||||
MetricFamily.Label dumpNameLabel = metricFamily.getMetricList().get(0).getLabels().stream()
|
||||
.filter(label -> "Dump_Name".equals(label.getName()))
|
||||
.findFirst().orElse(null);
|
||||
assertNotNull(dumpNameLabel);
|
||||
assertEquals("Dump总数", dumpNameLabel.getValue());
|
||||
}
|
||||
|
||||
private Map<String, MetricFamily> parseMetrics(InputStream inputStream, String metric) throws IOException {
|
||||
return OnlineParser.parseMetrics(inputStream, metric);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHandleUtf8Character_ValidTwoByteSequence() throws Exception {
|
||||
// Test valid 2-byte UTF-8 sequence: é (U+00E9) = 0xC3 0xA9
|
||||
String str = "test_metric{label=\"caf\u00e9\"} 1\n";
|
||||
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
|
||||
Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "test_metric");
|
||||
|
||||
assertNotNull(metricFamilyMap);
|
||||
MetricFamily metricFamily = metricFamilyMap.get("test_metric");
|
||||
assertNotNull(metricFamily);
|
||||
assertEquals(1, metricFamily.getMetricList().size());
|
||||
|
||||
MetricFamily.Label label = metricFamily.getMetricList().get(0).getLabels().get(0);
|
||||
assertEquals("label", label.getName());
|
||||
assertEquals("café", label.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHandleUtf8Character_ValidThreeByteSequence() throws Exception {
|
||||
// Test valid 3-byte UTF-8 sequence: Chinese character (U+4E2D) = 0xE4 0xB8 0xAD
|
||||
String str = "test_metric{label=\"\u4e2d\u6587\"} 1\n";
|
||||
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
|
||||
Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "test_metric");
|
||||
|
||||
assertNotNull(metricFamilyMap);
|
||||
MetricFamily metricFamily = metricFamilyMap.get("test_metric");
|
||||
assertNotNull(metricFamily);
|
||||
assertEquals(1, metricFamily.getMetricList().size());
|
||||
|
||||
MetricFamily.Label label = metricFamily.getMetricList().get(0).getLabels().get(0);
|
||||
assertEquals("label", label.getName());
|
||||
assertEquals("\u4e2d\u6587", label.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHandleUtf8Character_ValidFourByteSequence() throws Exception {
|
||||
// Test valid 4-byte UTF-8 sequence: 𝕳 (U+1D573) = 0xF0 0x9D 0x95 0xB3
|
||||
String str = "test_metric{label=\"\uD835\uDD73ello\"} 1\n";
|
||||
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
|
||||
Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "test_metric");
|
||||
|
||||
assertNotNull(metricFamilyMap);
|
||||
MetricFamily metricFamily = metricFamilyMap.get("test_metric");
|
||||
assertNotNull(metricFamily);
|
||||
assertEquals(1, metricFamily.getMetricList().size());
|
||||
|
||||
MetricFamily.Label label = metricFamily.getMetricList().get(0).getLabels().get(0);
|
||||
assertEquals("label", label.getName());
|
||||
assertEquals("𝕳ello", label.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHandleUtf8Character_InvalidFirstByte_0xC0() throws Exception {
|
||||
// Test invalid first byte 0xC0 (overlong encoding) - should cause parsing failure
|
||||
byte[] invalidBytes = {
|
||||
't', 'e', 's', 't', '_', 'm', 'e', 't', 'r', 'i', 'c', '{', 'l', 'a', 'b', 'e', 'l', '=', '"',
|
||||
(byte) 0xC0, (byte) 0x80,
|
||||
'"', '}', ' ', '1', '\n'
|
||||
};
|
||||
InputStream inputStream = new ByteArrayInputStream(invalidBytes);
|
||||
Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "test_metric");
|
||||
assertNull(metricFamilyMap);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHandleUtf8Character_InvalidFirstByte_0xC1() throws Exception {
|
||||
// Test invalid first byte 0xC1 (overlong encoding) - should cause parsing failure
|
||||
byte[] invalidBytes = {
|
||||
't', 'e', 's', 't', '_', 'm', 'e', 't', 'r', 'i', 'c', '{', 'l', 'a', 'b', 'e', 'l', '=', '"',
|
||||
(byte) 0xC1, (byte) 0x80,
|
||||
'"', '}', ' ', '1', '\n'
|
||||
};
|
||||
InputStream inputStream = new ByteArrayInputStream(invalidBytes);
|
||||
Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "test_metric");
|
||||
assertNull(metricFamilyMap);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHandleUtf8Character_InvalidFirstByte_0xF5() throws Exception {
|
||||
// Test invalid first byte 0xF5 (out of range) - should cause parsing failure
|
||||
byte[] invalidBytes = {
|
||||
't', 'e', 's', 't', '_', 'm', 'e', 't', 'r', 'i', 'c', '{', 'l', 'a', 'b', 'e', 'l', '=', '"',
|
||||
(byte) 0xF5, (byte) 0x80, (byte) 0x80, (byte) 0x80,
|
||||
'"', '}', ' ', '1', '\n'
|
||||
};
|
||||
InputStream inputStream = new ByteArrayInputStream(invalidBytes);
|
||||
Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "test_metric");
|
||||
assertNull(metricFamilyMap);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHandleUtf8Character_InvalidContinuationByte() throws Exception {
|
||||
// Test invalid continuation byte (should be 10xxxxxx but is 11xxxxxx) - should cause parsing failure
|
||||
byte[] invalidBytes = {
|
||||
't', 'e', 's', 't', '_', 'm', 'e', 't', 'r', 'i', 'c', '{', 'l', 'a', 'b', 'e', 'l', '=', '"',
|
||||
(byte) 0xC3, (byte) 0xC0,
|
||||
'"', '}', ' ', '1', '\n'
|
||||
};
|
||||
InputStream inputStream = new ByteArrayInputStream(invalidBytes);
|
||||
Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "test_metric");
|
||||
assertNull(metricFamilyMap);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHandleUtf8Character_MixedValidInvalid() throws Exception {
|
||||
// Test mixed valid UTF-8 sequences (removed invalid sequences that cause parsing to fail)
|
||||
byte[] mixedBytes = {
|
||||
't', 'e', 's', 't', '_', 'm', 'e', 't', 'r', 'i', 'c', '{', 'l', 'a', 'b', 'e', 'l', '=', '"',
|
||||
'H', 'e', 'l', 'l', 'o',
|
||||
(byte) 0xC3, (byte) 0xA9,
|
||||
(byte) 0xE4, (byte) 0xB8, (byte) 0xAD,
|
||||
(byte) 0xF0, (byte) 0x9F, (byte) 0x98, (byte) 0x80,
|
||||
'"', '}', ' ', '1', '\n'
|
||||
};
|
||||
InputStream inputStream = new ByteArrayInputStream(mixedBytes);
|
||||
Map<String, MetricFamily> metricFamilyMap = parseMetrics(inputStream, "test_metric");
|
||||
|
||||
assertNotNull(metricFamilyMap);
|
||||
MetricFamily metricFamily = metricFamilyMap.get("test_metric");
|
||||
assertNotNull(metricFamily);
|
||||
assertEquals(1, metricFamily.getMetricList().size());
|
||||
|
||||
MetricFamily.Label label = metricFamily.getMetricList().get(0).getLabels().get(0);
|
||||
assertEquals("label", label.getName());
|
||||
String value = label.getValue();
|
||||
assertNotNull(value);
|
||||
assertEquals("Hello\u00e9\u4e2d\ud83d\ude00", value);
|
||||
}
|
||||
}
|
||||
+8
-2
@@ -257,7 +257,10 @@ public class CommonDispatcher implements MetricsTaskDispatch, CollectDataDispatc
|
||||
// The periodic task pushes the task to the time wheel again.
|
||||
// First, determine the execution time of the task and the task collection interval.
|
||||
if (!timeout.isCancelled()) {
|
||||
timerDispatch.cyclicJob(timerJob);
|
||||
long spendTime = System.currentTimeMillis() - job.getDispatchTime();
|
||||
long interval = job.getInterval() - spendTime / 1000;
|
||||
interval = interval <= 0 ? 0 : interval;
|
||||
timerDispatch.cyclicJob(timerJob, interval, TimeUnit.SECONDS);
|
||||
}
|
||||
} else if (!metricsSet.isEmpty()) {
|
||||
// The execution of the current level metrics is completed, and the execution of the next level metrics starts
|
||||
@@ -357,7 +360,10 @@ public class CommonDispatcher implements MetricsTaskDispatch, CollectDataDispatc
|
||||
// The periodic task pushes the task to the time wheel again.
|
||||
// First, determine the execution time of the task and the task collection interval.
|
||||
if (!timeout.isCancelled()) {
|
||||
timerDispatch.cyclicJob(timerJob);
|
||||
long spendTime = System.currentTimeMillis() - job.getDispatchTime();
|
||||
long interval = job.getInterval() - spendTime / 1000;
|
||||
interval = interval <= 0 ? 0 : interval;
|
||||
timerDispatch.cyclicJob(timerJob, interval, TimeUnit.SECONDS);
|
||||
}
|
||||
// it is an asynchronous periodic cyclic task, directly response the collected data
|
||||
metricsDataList.forEach(commonDataQueue::sendMetricsData);
|
||||
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.collector.constants;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* Schedule Type
|
||||
*/
|
||||
@Getter
|
||||
public enum ScheduleTypeEnum {
|
||||
|
||||
INTERVAL("interval"),
|
||||
CRON("cron");
|
||||
|
||||
private final String type;
|
||||
|
||||
ScheduleTypeEnum(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
-21
@@ -19,7 +19,6 @@ package org.apache.hertzbeat.collector.dispatch.export;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.collector.dispatch.entrance.internal.CollectJobService;
|
||||
import org.apache.hertzbeat.common.entity.log.LogEntry;
|
||||
import org.apache.hertzbeat.common.entity.message.CollectRep;
|
||||
import org.apache.hertzbeat.common.queue.CommonDataQueue;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
@@ -88,24 +87,4 @@ public class NettyDataQueue implements CommonDataQueue {
|
||||
public void sendServiceDiscoveryData(CollectRep.MetricsData metricsData) {
|
||||
collectJobService.sendAsyncServiceDiscoveryData(metricsData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendLogEntry(LogEntry logEntry) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public LogEntry pollLogEntry() throws InterruptedException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendLogEntryToStorage(LogEntry logEntry) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public LogEntry pollLogEntryToStorage() throws InterruptedException {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
-6
@@ -45,12 +45,6 @@ public interface TimerDispatch {
|
||||
*/
|
||||
void cyclicJob(WheelTimerTask timerTask, long interval, TimeUnit timeUnit);
|
||||
|
||||
/**
|
||||
* Cyclic job
|
||||
* @param timerTask timerTask
|
||||
*/
|
||||
void cyclicJob(WheelTimerTask timerTask);
|
||||
|
||||
/**
|
||||
* Delete existing job
|
||||
* @param jobId jobId
|
||||
|
||||
+8
-47
@@ -17,17 +17,13 @@
|
||||
|
||||
package org.apache.hertzbeat.collector.timer;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.collector.constants.ScheduleTypeEnum;
|
||||
import org.apache.hertzbeat.collector.dispatch.entrance.internal.CollectResponseEventListener;
|
||||
import org.apache.hertzbeat.common.entity.job.Job;
|
||||
import org.apache.hertzbeat.common.entity.job.Metrics;
|
||||
@@ -36,7 +32,6 @@ import org.apache.hertzbeat.common.timer.HashedWheelTimer;
|
||||
import org.apache.hertzbeat.common.timer.Timeout;
|
||||
import org.apache.hertzbeat.common.timer.Timer;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.scheduling.support.CronExpression;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
@@ -63,12 +58,12 @@ public class TimerDispatcher implements TimerDispatch, DisposableBean {
|
||||
* jobId - listener
|
||||
*/
|
||||
private final Map<Long, CollectResponseEventListener> eventListeners;
|
||||
|
||||
|
||||
/**
|
||||
* is dispatcher online running
|
||||
*/
|
||||
private final AtomicBoolean started;
|
||||
|
||||
|
||||
public TimerDispatcher() {
|
||||
this.wheelTimer = new HashedWheelTimer(r -> {
|
||||
Thread ret = new Thread(r, "wheelTimer");
|
||||
@@ -89,8 +84,7 @@ public class TimerDispatcher implements TimerDispatch, DisposableBean {
|
||||
}
|
||||
WheelTimerTask timerJob = new WheelTimerTask(addJob);
|
||||
if (addJob.isCyclic()) {
|
||||
Long nextExecutionTime = getNextExecutionInterval(addJob);
|
||||
Timeout timeout = wheelTimer.newTimeout(timerJob, nextExecutionTime, TimeUnit.SECONDS);
|
||||
Timeout timeout = wheelTimer.newTimeout(timerJob, addJob.getInterval(), TimeUnit.SECONDS);
|
||||
currentCyclicTaskMap.put(addJob.getId(), timeout);
|
||||
} else {
|
||||
for (Metrics metric : addJob.getMetrics()) {
|
||||
@@ -117,13 +111,6 @@ public class TimerDispatcher implements TimerDispatch, DisposableBean {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cyclicJob(WheelTimerTask timerTask) {
|
||||
Job job = timerTask.getJob();
|
||||
Long nextExecutionTime = getNextExecutionInterval(job);
|
||||
cyclicJob(timerTask, nextExecutionTime, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteJob(long jobId, boolean isCyclic) {
|
||||
if (isCyclic) {
|
||||
@@ -138,7 +125,7 @@ public class TimerDispatcher implements TimerDispatch, DisposableBean {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void goOnline() {
|
||||
currentCyclicTaskMap.forEach((key, value) -> value.cancel());
|
||||
@@ -147,7 +134,7 @@ public class TimerDispatcher implements TimerDispatch, DisposableBean {
|
||||
currentTempTaskMap.clear();
|
||||
started.set(true);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void goOffline() {
|
||||
started.set(false);
|
||||
@@ -156,8 +143,8 @@ public class TimerDispatcher implements TimerDispatch, DisposableBean {
|
||||
currentTempTaskMap.forEach((key, value) -> value.cancel());
|
||||
currentTempTaskMap.clear();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void responseSyncJobData(long jobId, List<CollectRep.MetricsData> metricsDataTemps) {
|
||||
currentTempTaskMap.remove(jobId);
|
||||
@@ -166,35 +153,9 @@ public class TimerDispatcher implements TimerDispatch, DisposableBean {
|
||||
eventListener.response(metricsDataTemps);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void destroy() throws Exception {
|
||||
this.wheelTimer.stop();
|
||||
}
|
||||
|
||||
public Long getNextExecutionInterval(Job job) {
|
||||
if (ScheduleTypeEnum.CRON.getType().equals(job.getScheduleType()) && job.getCronExpression() != null && !job.getCronExpression().isEmpty()) {
|
||||
try {
|
||||
CronExpression cronExpression = CronExpression.parse(job.getCronExpression());
|
||||
ZonedDateTime nextExecutionTime = cronExpression.next(ZonedDateTime.now());
|
||||
long delay = Duration.between(ZonedDateTime.now(), nextExecutionTime).toMillis();
|
||||
// Convert to seconds and ensure non-negative
|
||||
return Math.max(0, delay / 1000);
|
||||
} catch (Exception e) {
|
||||
log.error("Invalid cron expression: {}", job.getCronExpression(), e);
|
||||
// Fall back to interval scheduling if cron is invalid
|
||||
return job.getInterval();
|
||||
}
|
||||
} else {
|
||||
if (job.getDispatchTime() > 0) {
|
||||
long spendTime = System.currentTimeMillis() - job.getDispatchTime();
|
||||
// Calculate remaining interval in seconds, preserving millisecond precision
|
||||
long intervalMs = job.getInterval() * 1000 - spendTime;
|
||||
// Ensure non-negative
|
||||
return Math.max(0, intervalMs / 1000);
|
||||
}
|
||||
return job.getInterval();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-182
@@ -1,182 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.collector.timer;
|
||||
|
||||
import org.apache.hertzbeat.collector.constants.ScheduleTypeEnum;
|
||||
import org.apache.hertzbeat.common.entity.job.Job;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Test class for TimerDispatcher's getNextExecutionInterval method
|
||||
*/
|
||||
public class TimerDispatcherTest {
|
||||
|
||||
@InjectMocks
|
||||
private TimerDispatcher timerDispatcher;
|
||||
|
||||
@Mock
|
||||
private Job job;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test getNextExecutionInterval with valid CRON expression
|
||||
* Should return the calculated interval based on cron expression
|
||||
*/
|
||||
@Test
|
||||
void testGetNextExecutionIntervalWithValidCron() {
|
||||
// Setup - Use a cron expression that runs every minute
|
||||
when(job.getScheduleType()).thenReturn(ScheduleTypeEnum.CRON.getType());
|
||||
when(job.getCronExpression()).thenReturn("0 * * * * ?");
|
||||
when(job.getInterval()).thenReturn(60L); // Default interval as fallback
|
||||
|
||||
// Execute
|
||||
Long result = timerDispatcher.getNextExecutionInterval(job);
|
||||
|
||||
// Verify - Result should be between 0 and 60 seconds
|
||||
// since the cron expression runs every minute
|
||||
assertEquals(true, result >= 0 && result <= 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test getNextExecutionInterval with invalid CRON expression
|
||||
* Should fall back to interval scheduling
|
||||
*/
|
||||
@Test
|
||||
void testGetNextExecutionIntervalWithInvalidCron() {
|
||||
// Setup - Use an invalid cron expression
|
||||
when(job.getScheduleType()).thenReturn(ScheduleTypeEnum.CRON.getType());
|
||||
when(job.getCronExpression()).thenReturn("invalid-cron-expression");
|
||||
when(job.getInterval()).thenReturn(300L); // 5 minutes
|
||||
|
||||
// Execute
|
||||
Long result = timerDispatcher.getNextExecutionInterval(job);
|
||||
|
||||
// Verify - Should fall back to interval value
|
||||
assertEquals(300L, result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test getNextExecutionInterval with empty CRON expression
|
||||
* Should fall back to interval scheduling
|
||||
*/
|
||||
@Test
|
||||
void testGetNextExecutionIntervalWithEmptyCron() {
|
||||
// Setup - Use empty cron expression
|
||||
when(job.getScheduleType()).thenReturn(ScheduleTypeEnum.CRON.getType());
|
||||
when(job.getCronExpression()).thenReturn("");
|
||||
when(job.getInterval()).thenReturn(120L); // 2 minutes
|
||||
|
||||
// Execute
|
||||
Long result = timerDispatcher.getNextExecutionInterval(job);
|
||||
|
||||
// Verify - Should fall back to interval value
|
||||
assertEquals(120L, result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test getNextExecutionInterval with INTERVAL schedule type and no dispatchTime
|
||||
* Should return the full interval value
|
||||
*/
|
||||
@Test
|
||||
void testGetNextExecutionIntervalWithIntervalNoDispatchTime() {
|
||||
// Setup - Use interval schedule type with no dispatchTime
|
||||
when(job.getScheduleType()).thenReturn(ScheduleTypeEnum.INTERVAL.getType());
|
||||
when(job.getDispatchTime()).thenReturn(0L); // No dispatchTime set
|
||||
when(job.getInterval()).thenReturn(600L); // 10 minutes
|
||||
|
||||
// Execute
|
||||
Long result = timerDispatcher.getNextExecutionInterval(job);
|
||||
|
||||
// Verify - Should return the full interval value
|
||||
assertEquals(600L, result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test getNextExecutionInterval with INTERVAL schedule type and dispatchTime
|
||||
* Should return the remaining interval time
|
||||
*/
|
||||
@Test
|
||||
void testGetNextExecutionIntervalWithIntervalAndDispatchTime() {
|
||||
// Setup - Use interval schedule type with dispatchTime
|
||||
long interval = 600L; // 10 minutes in seconds
|
||||
long spendTime = 300000L; // 5 minutes in milliseconds
|
||||
long dispatchTime = System.currentTimeMillis() - spendTime;
|
||||
|
||||
when(job.getScheduleType()).thenReturn(ScheduleTypeEnum.INTERVAL.getType());
|
||||
when(job.getDispatchTime()).thenReturn(dispatchTime);
|
||||
when(job.getInterval()).thenReturn(interval);
|
||||
|
||||
// Execute
|
||||
Long result = timerDispatcher.getNextExecutionInterval(job);
|
||||
|
||||
// Verify - Should return the remaining interval time (approximately 5 minutes)
|
||||
// Considering possible time differences during execution, we check a range
|
||||
assertEquals(true, result >= 295 && result <= 305);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test getNextExecutionInterval with INTERVAL schedule type and negative remaining time
|
||||
* Should return 0 to ensure non-negative value
|
||||
*/
|
||||
@Test
|
||||
void testGetNextExecutionIntervalWithIntervalAndNegativeRemainingTime() {
|
||||
// Setup - Use interval schedule type with dispatchTime resulting in negative remaining time
|
||||
long interval = 600L; // 10 minutes in seconds
|
||||
long spendTime = 1200000L; // 20 minutes in milliseconds (more than interval)
|
||||
long dispatchTime = System.currentTimeMillis() - spendTime;
|
||||
|
||||
when(job.getScheduleType()).thenReturn(ScheduleTypeEnum.INTERVAL.getType());
|
||||
when(job.getDispatchTime()).thenReturn(dispatchTime);
|
||||
when(job.getInterval()).thenReturn(interval);
|
||||
|
||||
// Execute
|
||||
Long result = timerDispatcher.getNextExecutionInterval(job);
|
||||
|
||||
// Verify - Should return 0 to ensure non-negative value
|
||||
assertEquals(0L, result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test getNextExecutionInterval with CRON schedule type and null cronExpression
|
||||
* Should fall back to interval scheduling
|
||||
*/
|
||||
@Test
|
||||
void testGetNextExecutionIntervalWithCronAndNullExpression() {
|
||||
// Setup - Use cron schedule type with null cronExpression
|
||||
when(job.getScheduleType()).thenReturn(ScheduleTypeEnum.CRON.getType());
|
||||
when(job.getCronExpression()).thenReturn(null);
|
||||
when(job.getInterval()).thenReturn(180L); // 3 minutes
|
||||
|
||||
// Execute
|
||||
Long result = timerDispatcher.getNextExecutionInterval(job);
|
||||
|
||||
// Verify - Should fall back to interval value
|
||||
assertEquals(180L, result);
|
||||
}
|
||||
}
|
||||
+7
-39
@@ -82,58 +82,26 @@ public final class CacheFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* get metrics alert define cache
|
||||
* get alert define cache
|
||||
* @return caffeine cache
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static List<AlertDefine> getMetricsAlertDefineCache() {
|
||||
return (List<AlertDefine>) COMMON_CACHE.get(CommonConstants.METRIC_CACHE_ALERT_DEFINE);
|
||||
public static List<AlertDefine> getAlertDefineCache() {
|
||||
return (List<AlertDefine>) COMMON_CACHE.get(CommonConstants.CACHE_ALERT_DEFINE);
|
||||
}
|
||||
|
||||
/**
|
||||
* set metrics alert define cache
|
||||
* set alert define cache
|
||||
* @param alertDefines alert defines
|
||||
*/
|
||||
public static void setMetricsAlertDefineCache(List<AlertDefine> alertDefines) {
|
||||
COMMON_CACHE.put(CommonConstants.METRIC_CACHE_ALERT_DEFINE, alertDefines);
|
||||
public static void setAlertDefineCache(List<AlertDefine> alertDefines) {
|
||||
COMMON_CACHE.put(CommonConstants.CACHE_ALERT_DEFINE, alertDefines);
|
||||
}
|
||||
|
||||
/**
|
||||
* clear metrics alert define cache
|
||||
*/
|
||||
public static void clearMetricsAlertDefineCache() {
|
||||
COMMON_CACHE.remove(CommonConstants.METRIC_CACHE_ALERT_DEFINE);
|
||||
}
|
||||
|
||||
/**
|
||||
* get log alert define cache
|
||||
* @return caffeine cache
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static List<AlertDefine> getLogAlertDefineCache() {
|
||||
return (List<AlertDefine>) COMMON_CACHE.get(CommonConstants.LOG_CACHE_ALERT_DEFINE);
|
||||
}
|
||||
|
||||
/**
|
||||
* set log alert define cache
|
||||
* @param alertDefines alert defines
|
||||
*/
|
||||
public static void setLogAlertDefineCache(List<AlertDefine> alertDefines) {
|
||||
COMMON_CACHE.put(CommonConstants.LOG_CACHE_ALERT_DEFINE, alertDefines);
|
||||
}
|
||||
|
||||
/**
|
||||
* clear log alert define cache
|
||||
*/
|
||||
public static void clearLogAlertDefineCache() {
|
||||
COMMON_CACHE.remove(CommonConstants.LOG_CACHE_ALERT_DEFINE);
|
||||
}
|
||||
|
||||
/**
|
||||
* clear alert define cache
|
||||
*/
|
||||
public static void clearAlertDefineCache() {
|
||||
clearLogAlertDefineCache();
|
||||
clearMetricsAlertDefineCache();
|
||||
COMMON_CACHE.remove(CommonConstants.CACHE_ALERT_DEFINE);
|
||||
}
|
||||
}
|
||||
|
||||
-18
@@ -115,16 +115,6 @@ public class CommonProperties {
|
||||
*/
|
||||
private String alertsDataQueueName;
|
||||
|
||||
/**
|
||||
* Queue name for log entry data
|
||||
*/
|
||||
private String logEntryQueueName;
|
||||
|
||||
/**
|
||||
* Queue name for log entry data to storage
|
||||
*/
|
||||
private String logEntryToStorageQueueName;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -150,13 +140,5 @@ public class CommonProperties {
|
||||
* alerts data topic
|
||||
*/
|
||||
private String alertsDataTopic;
|
||||
/**
|
||||
* log entry data topic
|
||||
*/
|
||||
private String logEntryDataTopic;
|
||||
/**
|
||||
* log entry data to storage topic
|
||||
*/
|
||||
private String logEntryDataToStorageTopic;
|
||||
}
|
||||
}
|
||||
|
||||
+6
-36
@@ -153,39 +153,14 @@ public interface CommonConstants {
|
||||
String ALERT_STATUS_PENDING = "pending";
|
||||
|
||||
/**
|
||||
* metric alert threshold type: realtime
|
||||
* alert threshold type: realtime
|
||||
*/
|
||||
String METRIC_ALERT_THRESHOLD_TYPE_REALTIME = "realtime_metric";
|
||||
String ALERT_THRESHOLD_TYPE_REALTIME = "realtime";
|
||||
|
||||
/**
|
||||
* metric alert threshold type: periodic
|
||||
* alert threshold type: periodic
|
||||
*/
|
||||
String METRIC_ALERT_THRESHOLD_TYPE_PERIODIC = "periodic_metric";
|
||||
|
||||
/**
|
||||
* log alert threshold type: realtime
|
||||
*/
|
||||
String LOG_ALERT_THRESHOLD_TYPE_REALTIME = "realtime_log";
|
||||
|
||||
/**
|
||||
* log alert threshold type: periodic
|
||||
*/
|
||||
String LOG_ALERT_THRESHOLD_TYPE_PERIODIC = "periodic_log";
|
||||
|
||||
/**
|
||||
* Alert mode label key
|
||||
*/
|
||||
String ALERT_MODE_LABEL = "alert_mode";
|
||||
|
||||
/**
|
||||
* Alert mode: group
|
||||
*/
|
||||
String ALERT_MODE_GROUP = "group";
|
||||
|
||||
/**
|
||||
* Alert mode: individual
|
||||
*/
|
||||
String ALERT_MODE_INDIVIDUAL = "individual";
|
||||
String ALERT_THRESHOLD_TYPE_PERIODIC = "periodic";
|
||||
|
||||
/**
|
||||
* Field parameter type: number
|
||||
@@ -263,14 +238,9 @@ public interface CommonConstants {
|
||||
String CACHE_ALERT_SILENCE = "alert_silence";
|
||||
|
||||
/**
|
||||
* cache key metric alert define
|
||||
* cache key alert define
|
||||
*/
|
||||
String METRIC_CACHE_ALERT_DEFINE = "metric_alert_define";
|
||||
|
||||
/**
|
||||
* cache key log alert define
|
||||
*/
|
||||
String LOG_CACHE_ALERT_DEFINE = "log_alert_define";
|
||||
String CACHE_ALERT_DEFINE = "alert_define";
|
||||
|
||||
/**
|
||||
* cache key alert converge
|
||||
|
||||
+1
-6
@@ -55,10 +55,5 @@ public enum GeneralConfigTypeEnum {
|
||||
/**
|
||||
* system store config
|
||||
*/
|
||||
oss,
|
||||
|
||||
/**
|
||||
* model llm provider
|
||||
*/
|
||||
provider
|
||||
oss
|
||||
}
|
||||
|
||||
+11
-34
@@ -24,46 +24,23 @@ import java.util.Arrays;
|
||||
*/
|
||||
public enum JexlKeywordsEnum {
|
||||
|
||||
/**
|
||||
* Official reserved keywords
|
||||
* @reference https://commons.apache.org/proper/commons-jexl/reference/syntax.html
|
||||
*/
|
||||
OR("or"),
|
||||
AND("and"),
|
||||
EQ("eq"),
|
||||
NE("ne"),
|
||||
LT("lt"),
|
||||
GT("gt"),
|
||||
LE("le"),
|
||||
GE("ge"),
|
||||
DIV("div"),
|
||||
MOD("mod"),
|
||||
NOT("not"),
|
||||
NULL("null"),
|
||||
TRUE("true"),
|
||||
FALSE("false"),
|
||||
SIZE("size"),
|
||||
EMPTY("empty"),
|
||||
NEW("new"),
|
||||
VAR("var"),
|
||||
DO("do"),
|
||||
WHILE("while"),
|
||||
BREAK("break"),
|
||||
CONTINUE("continue"),
|
||||
FUNCTION("function"),
|
||||
RETURN("return"),
|
||||
|
||||
/**
|
||||
* Syntax-reserved keywords
|
||||
*/
|
||||
IF("if"),
|
||||
ELSE("else"),
|
||||
ELSEIF("elseif"),
|
||||
WHILE("while"),
|
||||
DO("do"),
|
||||
FOR("for"),
|
||||
|
||||
/**
|
||||
* Built-in functions reserve keywords
|
||||
*/
|
||||
SIZE("size"),
|
||||
EMPTY("empty");
|
||||
|
||||
CONTINUE("continue"),
|
||||
BREAK("break"),
|
||||
TRUE("true"),
|
||||
FALSE("false"),
|
||||
NULL("null"),
|
||||
UNDEFINED("undefined");
|
||||
|
||||
private final String keyword;
|
||||
|
||||
|
||||
+2
-2
@@ -65,7 +65,7 @@ public class AlertDefine {
|
||||
@NotNull
|
||||
private String name;
|
||||
|
||||
@Schema(title = "Rule Type: realtime_metric, periodic_metric, realtime_log, periodic_log", example = "realtime_metric")
|
||||
@Schema(title = "Rule Type: realtime, periodic", example = "0")
|
||||
private String type;
|
||||
|
||||
@Schema(title = "Alarm Threshold Expr", example = "usage>90", accessMode = READ_WRITE)
|
||||
@@ -73,7 +73,7 @@ public class AlertDefine {
|
||||
@Column(length = 2048)
|
||||
private String expr;
|
||||
|
||||
@Schema(title = "Execution Period/ Window Size (seconds) - For periodic rules/ For log realtime", example = "300")
|
||||
@Schema(title = "Execution Period (seconds) - For periodic rules", example = "300")
|
||||
private Integer period;
|
||||
|
||||
@Schema(title = "Alarm Trigger Times.The alarm is triggered only after the required number of times is reached",
|
||||
|
||||
@@ -143,16 +143,6 @@ public class Job {
|
||||
*/
|
||||
private boolean prometheusProxyMode = false;
|
||||
|
||||
/**
|
||||
* Scheduling type: interval or cron
|
||||
*/
|
||||
private String scheduleType = "interval";
|
||||
|
||||
/**
|
||||
* Cron expression for scheduling, used when scheduleType is "cron"
|
||||
*/
|
||||
private String cronExpression = null;
|
||||
|
||||
/**
|
||||
* the collect data response metrics as env configmap for other collect use. ^o^xxx^o^
|
||||
*/
|
||||
|
||||
+1
-2
@@ -37,10 +37,9 @@ public class DnsProtocol implements CommonRequestProtocol, Protocol {
|
||||
private String timeout;
|
||||
private String tcp;
|
||||
private String queryClass;
|
||||
private String recordType;
|
||||
|
||||
public boolean isInvalid() {
|
||||
return StringUtils.isAnyBlank(dnsServerIP, port, address, timeout, tcp, queryClass, recordType);
|
||||
return StringUtils.isAnyBlank(dnsServerIP, port, address, timeout, tcp, queryClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.common.entity.log;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* OpenTelemetry Log Entry entity based on OpenTelemetry log data model specification.
|
||||
*
|
||||
* @see <a href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md">OpenTelemetry Log Data Model</a>
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LogEntry {
|
||||
|
||||
/**
|
||||
* Time when the event occurred.
|
||||
* Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970.
|
||||
*/
|
||||
private Long timeUnixNano;
|
||||
|
||||
/**
|
||||
* Time when the event was observed.
|
||||
* Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970.
|
||||
*/
|
||||
private Long observedTimeUnixNano;
|
||||
|
||||
/**
|
||||
* Numerical value of the severity.
|
||||
* Smaller numerical values correspond to less severe events (such as debug events),
|
||||
* larger numerical values correspond to more severe events (such as errors and critical events).
|
||||
*/
|
||||
private Integer severityNumber;
|
||||
|
||||
/**
|
||||
* The severity text (also known as log level).
|
||||
* This is the original string representation of the severity as it is known at the source.
|
||||
*/
|
||||
private String severityText;
|
||||
|
||||
/**
|
||||
* A value containing the body of the log record.
|
||||
* Can be for example a human-readable string message (including multi-line)
|
||||
* or it can be a structured data composed of arrays and maps of other values.
|
||||
*/
|
||||
private Object body;
|
||||
|
||||
/**
|
||||
* Additional information about the specific event occurrence.
|
||||
* Unlike the Resource field, these are NOT fixed for the lifetime of the process.
|
||||
*/
|
||||
private Map<String, Object> attributes;
|
||||
|
||||
/**
|
||||
* Dropped attributes count.
|
||||
* Number of attributes that were discarded due to limits being exceeded.
|
||||
*/
|
||||
private Integer droppedAttributesCount;
|
||||
|
||||
/**
|
||||
* A unique identifier for a trace.
|
||||
* All spans from the same trace share the same trace_id.
|
||||
* The ID is a 16-byte array represented as a hex string.
|
||||
*/
|
||||
private String traceId;
|
||||
|
||||
/**
|
||||
* A unique identifier for a span within a trace.
|
||||
* The ID is an 8-byte array represented as a hex string.
|
||||
*/
|
||||
private String spanId;
|
||||
|
||||
/**
|
||||
* Trace flag as defined in W3C Trace Context specification.
|
||||
* At the time of writing the specification defines one flag - the SAMPLED flag.
|
||||
*/
|
||||
private Integer traceFlags;
|
||||
|
||||
/**
|
||||
* Resource information.
|
||||
* Information about the entity producing the telemetry.
|
||||
*/
|
||||
private Map<String, Object> resource;
|
||||
|
||||
/**
|
||||
* Instrumentation Scope information.
|
||||
* Information about the instrumentation scope that emitted the log.
|
||||
*/
|
||||
private InstrumentationScope instrumentationScope;
|
||||
|
||||
/**
|
||||
* Instrumentation Scope information for logs.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class InstrumentationScope {
|
||||
|
||||
/**
|
||||
* The name of the instrumentation scope.
|
||||
* This should be the fully-qualified name of the instrumentation library.
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* The version of the instrumentation scope.
|
||||
*/
|
||||
private String version;
|
||||
|
||||
/**
|
||||
* Additional attributes that describe the scope.
|
||||
*/
|
||||
private Map<String, Object> attributes;
|
||||
|
||||
/**
|
||||
* Number of attributes that were discarded due to limits being exceeded.
|
||||
*/
|
||||
private Integer droppedAttributesCount;
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,6 @@ package org.apache.hertzbeat.common.entity.manager;
|
||||
|
||||
import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY;
|
||||
import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Convert;
|
||||
@@ -91,14 +90,6 @@ public class Monitor {
|
||||
@Min(10)
|
||||
private Integer intervals;
|
||||
|
||||
@Schema(title = "Schedule type: interval | cron", example = "interval", accessMode = READ_WRITE)
|
||||
@Size(max = 20)
|
||||
private String scheduleType;
|
||||
|
||||
@Schema(title = "Cron expression when scheduleType is cron", example = "0/5 * * * * ?", accessMode = READ_WRITE)
|
||||
@Size(max = 100)
|
||||
private String cronExpression;
|
||||
|
||||
@Schema(title = "Task status 0: Paused, 1: Up, 2: Down", accessMode = READ_WRITE)
|
||||
@Min(0)
|
||||
@Max(4)
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
|
||||
package org.apache.hertzbeat.common.queue;
|
||||
|
||||
import org.apache.hertzbeat.common.entity.log.LogEntry;
|
||||
import org.apache.hertzbeat.common.entity.message.CollectRep;
|
||||
|
||||
/**
|
||||
@@ -63,32 +62,4 @@ public interface CommonDataQueue {
|
||||
* @param metricsData service discovery data
|
||||
*/
|
||||
void sendServiceDiscoveryData(CollectRep.MetricsData metricsData);
|
||||
|
||||
/**
|
||||
* send log entry to queue
|
||||
* @param logEntry log entry data based on OpenTelemetry log data model
|
||||
* @throws InterruptedException when sending is interrupted
|
||||
*/
|
||||
void sendLogEntry(LogEntry logEntry);
|
||||
|
||||
/**
|
||||
* poll log entry from queue
|
||||
* @return log entry data
|
||||
* @throws InterruptedException when poll timeout
|
||||
*/
|
||||
LogEntry pollLogEntry() throws InterruptedException;
|
||||
|
||||
/**
|
||||
* send log entry to storage queue
|
||||
* @param logEntry log entry data based on OpenTelemetry log data model
|
||||
* @throws InterruptedException when sending is interrupted
|
||||
*/
|
||||
void sendLogEntryToStorage(LogEntry logEntry);
|
||||
|
||||
/**
|
||||
* poll log entry from storage queue
|
||||
* @return log entry data
|
||||
* @throws InterruptedException when poll timeout
|
||||
*/
|
||||
LogEntry pollLogEntryToStorage() throws InterruptedException;
|
||||
}
|
||||
|
||||
-29
@@ -22,7 +22,6 @@ import java.util.Map;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.common.constants.DataQueueConstants;
|
||||
import org.apache.hertzbeat.common.entity.log.LogEntry;
|
||||
import org.apache.hertzbeat.common.entity.message.CollectRep;
|
||||
import org.apache.hertzbeat.common.queue.CommonDataQueue;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
@@ -47,23 +46,17 @@ public class InMemoryCommonDataQueue implements CommonDataQueue, DisposableBean
|
||||
private final LinkedBlockingQueue<CollectRep.MetricsData> metricsDataToAlertQueue;
|
||||
private final LinkedBlockingQueue<CollectRep.MetricsData> metricsDataToStorageQueue;
|
||||
private final LinkedBlockingQueue<CollectRep.MetricsData> serviceDiscoveryDataQueue;
|
||||
private final LinkedBlockingQueue<LogEntry> logEntryQueue;
|
||||
private final LinkedBlockingQueue<LogEntry> logEntryToStorageQueue;
|
||||
|
||||
public InMemoryCommonDataQueue() {
|
||||
metricsDataToAlertQueue = new LinkedBlockingQueue<>();
|
||||
metricsDataToStorageQueue = new LinkedBlockingQueue<>();
|
||||
serviceDiscoveryDataQueue = new LinkedBlockingQueue<>();
|
||||
logEntryQueue = new LinkedBlockingQueue<>();
|
||||
logEntryToStorageQueue = new LinkedBlockingQueue<>();
|
||||
}
|
||||
|
||||
public Map<String, Integer> getQueueSizeMetricsInfo() {
|
||||
Map<String, Integer> metrics = new HashMap<>(8);
|
||||
metrics.put("metricsDataToAlertQueue", metricsDataToAlertQueue.size());
|
||||
metrics.put("metricsDataToStorageQueue", metricsDataToStorageQueue.size());
|
||||
metrics.put("logEntryQueue", logEntryQueue.size());
|
||||
metrics.put("logEntryToStorageQueue", logEntryToStorageQueue.size());
|
||||
return metrics;
|
||||
}
|
||||
|
||||
@@ -97,32 +90,10 @@ public class InMemoryCommonDataQueue implements CommonDataQueue, DisposableBean
|
||||
serviceDiscoveryDataQueue.offer(metricsData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendLogEntry(LogEntry logEntry) {
|
||||
logEntryQueue.offer(logEntry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LogEntry pollLogEntry() throws InterruptedException {
|
||||
return logEntryQueue.take();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendLogEntryToStorage(LogEntry logEntry) {
|
||||
logEntryToStorageQueue.offer(logEntry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LogEntry pollLogEntryToStorage() throws InterruptedException {
|
||||
return logEntryToStorageQueue.take();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
metricsDataToAlertQueue.clear();
|
||||
metricsDataToStorageQueue.clear();
|
||||
serviceDiscoveryDataQueue.clear();
|
||||
logEntryQueue.clear();
|
||||
logEntryToStorageQueue.clear();
|
||||
}
|
||||
}
|
||||
|
||||
-76
@@ -26,11 +26,8 @@ import java.util.concurrent.locks.ReentrantLock;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.common.config.CommonProperties;
|
||||
import org.apache.hertzbeat.common.constants.DataQueueConstants;
|
||||
import org.apache.hertzbeat.common.entity.log.LogEntry;
|
||||
import org.apache.hertzbeat.common.entity.message.CollectRep;
|
||||
import org.apache.hertzbeat.common.queue.CommonDataQueue;
|
||||
import org.apache.hertzbeat.common.serialize.KafkaLogEntryDeserializer;
|
||||
import org.apache.hertzbeat.common.serialize.KafkaLogEntrySerializer;
|
||||
import org.apache.hertzbeat.common.serialize.KafkaMetricsDataDeserializer;
|
||||
import org.apache.hertzbeat.common.serialize.KafkaMetricsDataSerializer;
|
||||
import org.apache.kafka.clients.consumer.ConsumerConfig;
|
||||
@@ -62,21 +59,14 @@ public class KafkaCommonDataQueue implements CommonDataQueue, DisposableBean {
|
||||
private final ReentrantLock metricDataToAlertLock = new ReentrantLock();
|
||||
private final ReentrantLock metricDataToStorageLock = new ReentrantLock();
|
||||
private final ReentrantLock serviceDiscoveryDataLock = new ReentrantLock();
|
||||
private final ReentrantLock logEntryLock = new ReentrantLock();
|
||||
private final ReentrantLock logEntryToStorageLock = new ReentrantLock();
|
||||
private final LinkedBlockingQueue<CollectRep.MetricsData> metricsDataToAlertQueue;
|
||||
private final LinkedBlockingQueue<CollectRep.MetricsData> metricsDataToStorageQueue;
|
||||
private final LinkedBlockingQueue<CollectRep.MetricsData> serviceDiscoveryDataQueue;
|
||||
private final LinkedBlockingQueue<LogEntry> logEntryQueue;
|
||||
private final LinkedBlockingQueue<LogEntry> logEntryToStorageQueue;
|
||||
private final CommonProperties.KafkaProperties kafka;
|
||||
private KafkaProducer<Long, CollectRep.MetricsData> metricsDataProducer;
|
||||
private KafkaProducer<Long, LogEntry> logEntryProducer;
|
||||
private KafkaConsumer<Long, CollectRep.MetricsData> metricsDataToAlertConsumer;
|
||||
private KafkaConsumer<Long, CollectRep.MetricsData> metricsDataToStorageConsumer;
|
||||
private KafkaConsumer<Long, CollectRep.MetricsData> serviceDiscoveryDataConsumer;
|
||||
private KafkaConsumer<Long, LogEntry> logEntryConsumer;
|
||||
private KafkaConsumer<Long, LogEntry> logEntryToStorageConsumer;
|
||||
|
||||
public KafkaCommonDataQueue(CommonProperties properties) {
|
||||
if (properties == null || properties.getQueue() == null || properties.getQueue().getKafka() == null) {
|
||||
@@ -87,8 +77,6 @@ public class KafkaCommonDataQueue implements CommonDataQueue, DisposableBean {
|
||||
metricsDataToAlertQueue = new LinkedBlockingQueue<>();
|
||||
metricsDataToStorageQueue = new LinkedBlockingQueue<>();
|
||||
serviceDiscoveryDataQueue = new LinkedBlockingQueue<>();
|
||||
logEntryQueue = new LinkedBlockingQueue<>();
|
||||
logEntryToStorageQueue = new LinkedBlockingQueue<>();
|
||||
initDataQueue();
|
||||
}
|
||||
|
||||
@@ -99,7 +87,6 @@ public class KafkaCommonDataQueue implements CommonDataQueue, DisposableBean {
|
||||
producerConfig.put(ProducerConfig.ACKS_CONFIG, "all");
|
||||
producerConfig.put(ProducerConfig.RETRIES_CONFIG, 3);
|
||||
metricsDataProducer = new KafkaProducer<>(producerConfig, new LongSerializer(), new KafkaMetricsDataSerializer());
|
||||
logEntryProducer = new KafkaProducer<>(producerConfig, new LongSerializer(), new KafkaLogEntrySerializer());
|
||||
|
||||
Map<String, Object> consumerConfig = new HashMap<>(4);
|
||||
consumerConfig.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.getServers());
|
||||
@@ -124,16 +111,6 @@ public class KafkaCommonDataQueue implements CommonDataQueue, DisposableBean {
|
||||
serviceDiscoveryDataConsumer = new KafkaConsumer<>(serviceDiscoveryDataConsumerConfig, new LongDeserializer(),
|
||||
new KafkaMetricsDataDeserializer());
|
||||
serviceDiscoveryDataConsumer.subscribe(Collections.singletonList(kafka.getServiceDiscoveryDataTopic()));
|
||||
|
||||
Map<String, Object> logEntryConsumerConfig = new HashMap<>(consumerConfig);
|
||||
logEntryConsumerConfig.put("group.id", "log-entry-consumer");
|
||||
logEntryConsumer = new KafkaConsumer<>(logEntryConsumerConfig, new LongDeserializer(), new KafkaLogEntryDeserializer());
|
||||
logEntryConsumer.subscribe(Collections.singletonList(kafka.getLogEntryDataTopic()));
|
||||
|
||||
Map<String, Object> logEntryToStorageConsumerConfig = new HashMap<>(consumerConfig);
|
||||
logEntryToStorageConsumerConfig.put("group.id", "log-entry-storage-consumer");
|
||||
logEntryToStorageConsumer = new KafkaConsumer<>(logEntryToStorageConsumerConfig, new LongDeserializer(), new KafkaLogEntryDeserializer());
|
||||
logEntryToStorageConsumer.subscribe(Collections.singletonList(kafka.getLogEntryDataToStorageTopic()));
|
||||
} catch (Exception e) {
|
||||
log.error("please config common.queue.kafka props correctly", e);
|
||||
throw e;
|
||||
@@ -215,50 +192,6 @@ public class KafkaCommonDataQueue implements CommonDataQueue, DisposableBean {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendLogEntry(LogEntry logEntry) {
|
||||
if (logEntryProducer != null) {
|
||||
try {
|
||||
ProducerRecord<Long, LogEntry> record = new ProducerRecord<>(kafka.getLogEntryDataTopic(), logEntry);
|
||||
logEntryProducer.send(record);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to send LogEntry to Kafka: {}", e.getMessage());
|
||||
// Fallback to memory queue if Kafka fails
|
||||
logEntryQueue.offer(logEntry);
|
||||
}
|
||||
} else {
|
||||
log.warn("logEntryProducer is not enabled, using memory queue");
|
||||
logEntryQueue.offer(logEntry);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public LogEntry pollLogEntry() throws InterruptedException {
|
||||
return genericPollDataFunction(logEntryQueue, logEntryConsumer, logEntryLock);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendLogEntryToStorage(LogEntry logEntry) {
|
||||
if (logEntryProducer != null) {
|
||||
try {
|
||||
ProducerRecord<Long, LogEntry> record = new ProducerRecord<>(kafka.getLogEntryDataToStorageTopic(), logEntry);
|
||||
logEntryProducer.send(record);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to send LogEntry to storage via Kafka: {}", e.getMessage());
|
||||
// Fallback to memory queue if Kafka fails
|
||||
logEntryToStorageQueue.offer(logEntry);
|
||||
}
|
||||
} else {
|
||||
log.warn("logEntryProducer is not enabled, using memory queue for storage");
|
||||
logEntryToStorageQueue.offer(logEntry);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public LogEntry pollLogEntryToStorage() throws InterruptedException {
|
||||
return genericPollDataFunction(logEntryToStorageQueue, logEntryToStorageConsumer, logEntryToStorageLock);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() throws Exception {
|
||||
if (metricsDataProducer != null) {
|
||||
@@ -273,14 +206,5 @@ public class KafkaCommonDataQueue implements CommonDataQueue, DisposableBean {
|
||||
if (serviceDiscoveryDataConsumer != null) {
|
||||
serviceDiscoveryDataConsumer.close();
|
||||
}
|
||||
if (logEntryProducer != null) {
|
||||
logEntryProducer.close();
|
||||
}
|
||||
if (logEntryConsumer != null) {
|
||||
logEntryConsumer.close();
|
||||
}
|
||||
if (logEntryToStorageConsumer != null) {
|
||||
logEntryToStorageConsumer.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-50
@@ -24,10 +24,8 @@ import io.lettuce.core.api.sync.RedisCommands;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.common.config.CommonProperties;
|
||||
import org.apache.hertzbeat.common.constants.DataQueueConstants;
|
||||
import org.apache.hertzbeat.common.entity.log.LogEntry;
|
||||
import org.apache.hertzbeat.common.entity.message.CollectRep;
|
||||
import org.apache.hertzbeat.common.queue.CommonDataQueue;
|
||||
import org.apache.hertzbeat.common.serialize.RedisLogEntryCodec;
|
||||
import org.apache.hertzbeat.common.serialize.RedisMetricsDataCodec;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
@@ -48,13 +46,9 @@ public class RedisCommonDataQueue implements CommonDataQueue, DisposableBean {
|
||||
private final RedisClient redisClient;
|
||||
private final StatefulRedisConnection<String, CollectRep.MetricsData> connection;
|
||||
private final RedisCommands<String, CollectRep.MetricsData> syncCommands;
|
||||
private final StatefulRedisConnection<String, LogEntry> logEntryConnection;
|
||||
private final RedisCommands<String, LogEntry> logEntrySyncCommands;
|
||||
private final String metricsDataQueueNameToStorage;
|
||||
private final String metricsDataQueueNameForServiceDiscovery;
|
||||
private final String metricsDataQueueNameToAlerter;
|
||||
private final String logEntryQueueName;
|
||||
private final String logEntryToStorageQueueName;
|
||||
private final CommonProperties.RedisProperties redisProperties;
|
||||
|
||||
public RedisCommonDataQueue(CommonProperties properties) {
|
||||
@@ -75,14 +69,9 @@ public class RedisCommonDataQueue implements CommonDataQueue, DisposableBean {
|
||||
RedisMetricsDataCodec codec = new RedisMetricsDataCodec();
|
||||
this.connection = redisClient.connect(codec);
|
||||
this.syncCommands = connection.sync();
|
||||
RedisLogEntryCodec logCodec = new RedisLogEntryCodec();
|
||||
this.logEntryConnection = redisClient.connect(logCodec);
|
||||
this.logEntrySyncCommands = logEntryConnection.sync();
|
||||
this.metricsDataQueueNameToStorage = redisProperties.getMetricsDataQueueNameToPersistentStorage();
|
||||
this.metricsDataQueueNameForServiceDiscovery = redisProperties.getMetricsDataQueueNameForServiceDiscovery();
|
||||
this.metricsDataQueueNameToAlerter = redisProperties.getMetricsDataQueueNameToAlerter();
|
||||
this.logEntryQueueName = redisProperties.getLogEntryQueueName();
|
||||
this.logEntryToStorageQueueName = redisProperties.getLogEntryToStorageQueueName();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -142,48 +131,9 @@ public class RedisCommonDataQueue implements CommonDataQueue, DisposableBean {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendLogEntry(LogEntry logEntry) {
|
||||
try {
|
||||
logEntrySyncCommands.lpush(logEntryQueueName, logEntry);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to send LogEntry to Redis: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public LogEntry pollLogEntry() throws InterruptedException {
|
||||
try {
|
||||
return logEntrySyncCommands.rpop(logEntryQueueName);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to poll LogEntry from Redis: {}", e.getMessage());
|
||||
throw new InterruptedException("Failed to poll LogEntry from Redis");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendLogEntryToStorage(LogEntry logEntry) {
|
||||
try {
|
||||
logEntrySyncCommands.lpush(logEntryToStorageQueueName, logEntry);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to send LogEntry to storage via Redis: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public LogEntry pollLogEntryToStorage() throws InterruptedException {
|
||||
try {
|
||||
return logEntrySyncCommands.rpop(logEntryToStorageQueueName);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to poll LogEntry from storage via Redis: {}", e.getMessage());
|
||||
throw new InterruptedException("Failed to poll LogEntry from storage via Redis");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
connection.close();
|
||||
logEntryConnection.close();
|
||||
redisClient.shutdown();
|
||||
}
|
||||
|
||||
|
||||
-70
@@ -1,70 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.common.serialize;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.common.entity.log.LogEntry;
|
||||
import org.apache.kafka.common.header.Headers;
|
||||
import org.apache.kafka.common.serialization.Deserializer;
|
||||
|
||||
/**
|
||||
* Kafka LogEntry deserializer using JSON
|
||||
*/
|
||||
@Slf4j
|
||||
public class KafkaLogEntryDeserializer implements Deserializer<LogEntry> {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public KafkaLogEntryDeserializer() {
|
||||
this.objectMapper = new ObjectMapper();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(Map<String, ?> configs, boolean isKey) {
|
||||
Deserializer.super.configure(configs, isKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LogEntry deserialize(String topic, byte[] data) {
|
||||
if (data == null || data.length == 0) {
|
||||
log.warn("Empty data received for topic: {}", topic);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
String jsonString = new String(data, StandardCharsets.UTF_8);
|
||||
return objectMapper.readValue(jsonString, LogEntry.class);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("Failed to deserialize LogEntry from JSON for topic: {}", topic, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public LogEntry deserialize(String topic, Headers headers, byte[] data) {
|
||||
return deserialize(topic, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
Deserializer.super.close();
|
||||
}
|
||||
}
|
||||
-70
@@ -1,70 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.common.serialize;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.common.entity.log.LogEntry;
|
||||
import org.apache.kafka.common.header.Headers;
|
||||
import org.apache.kafka.common.serialization.Serializer;
|
||||
|
||||
/**
|
||||
* Kafka LogEntry serializer using JSON
|
||||
*/
|
||||
@Slf4j
|
||||
public class KafkaLogEntrySerializer implements Serializer<LogEntry> {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public KafkaLogEntrySerializer() {
|
||||
this.objectMapper = new ObjectMapper();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(Map<String, ?> configs, boolean isKey) {
|
||||
Serializer.super.configure(configs, isKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize(String topic, LogEntry logEntry) {
|
||||
if (logEntry == null) {
|
||||
log.warn("LogEntry is null for topic: {}", topic);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
String jsonString = objectMapper.writeValueAsString(logEntry);
|
||||
return jsonString.getBytes(StandardCharsets.UTF_8);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("Failed to serialize LogEntry to JSON for topic: {}", topic, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize(String topic, Headers headers, LogEntry logEntry) {
|
||||
return serialize(topic, logEntry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
Serializer.super.close();
|
||||
}
|
||||
}
|
||||
-75
@@ -1,75 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.common.serialize;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.lettuce.core.codec.RedisCodec;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.common.entity.log.LogEntry;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* Redis LogEntry codec using JSON serialization
|
||||
*/
|
||||
@Slf4j
|
||||
public class RedisLogEntryCodec implements RedisCodec<String, LogEntry> {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public RedisLogEntryCodec() {
|
||||
this.objectMapper = new ObjectMapper();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decodeKey(ByteBuffer byteBuffer) {
|
||||
return Unpooled.wrappedBuffer(byteBuffer).toString(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LogEntry decodeValue(ByteBuffer byteBuffer) {
|
||||
if (byteBuffer == null || !byteBuffer.hasRemaining()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
String jsonString = Unpooled.wrappedBuffer(byteBuffer).toString(StandardCharsets.UTF_8);
|
||||
return objectMapper.readValue(jsonString, LogEntry.class);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to decode LogEntry from JSON: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer encodeKey(String s) {
|
||||
return ByteBuffer.wrap(s.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer encodeValue(LogEntry logEntry) {
|
||||
try {
|
||||
String jsonString = objectMapper.writeValueAsString(logEntry);
|
||||
return ByteBuffer.wrap(jsonString.getBytes(StandardCharsets.UTF_8));
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to encode LogEntry to JSON: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.common.util;
|
||||
|
||||
import org.apache.hertzbeat.common.constants.JexlKeywordsEnum;
|
||||
|
||||
/**
|
||||
* Validate JEXL rules
|
||||
*/
|
||||
public class JexlCheckerUtil {
|
||||
|
||||
public static final String SPACES_REGEX = "^\\S+(?:\\s+\\S+)+$";
|
||||
|
||||
/**
|
||||
* Verify Keyword Information
|
||||
*/
|
||||
public static boolean verifyKeywords(String str) {
|
||||
return JexlKeywordsEnum.match(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the starting character
|
||||
*/
|
||||
public static boolean verifyStartCharacter(String str) {
|
||||
char c = str.charAt(0);
|
||||
return !((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_' || c == '$');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify contains spaces
|
||||
*/
|
||||
public static boolean verifySpaces(String str) {
|
||||
return str.trim().matches(SPACES_REGEX);
|
||||
}
|
||||
}
|
||||
-80
@@ -19,10 +19,7 @@ package org.apache.hertzbeat.common.queue.impl;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.apache.hertzbeat.common.entity.log.LogEntry;
|
||||
import org.apache.hertzbeat.common.entity.message.CollectRep;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -52,71 +49,6 @@ class InMemoryCommonDataQueueTest {
|
||||
assertEquals(metricsData, polledMetricsData);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLogEntry() throws InterruptedException {
|
||||
|
||||
// Create a test log entry with comprehensive data
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
attributes.put("service.name", "hertzbeat");
|
||||
attributes.put("service.version", "1.0.0");
|
||||
|
||||
Map<String, Object> resource = new HashMap<>();
|
||||
resource.put("host.name", "localhost");
|
||||
resource.put("os.type", "linux");
|
||||
|
||||
LogEntry.InstrumentationScope scope = LogEntry.InstrumentationScope.builder()
|
||||
.name("org.apache.hertzbeat.test")
|
||||
.version("1.0.0")
|
||||
.build();
|
||||
|
||||
LogEntry logEntry = LogEntry.builder()
|
||||
.timeUnixNano(Instant.now().toEpochMilli() * 1_000_000L)
|
||||
.observedTimeUnixNano(Instant.now().toEpochMilli() * 1_000_000L)
|
||||
.severityNumber(9) // INFO level
|
||||
.severityText("INFO")
|
||||
.body("Test log message for hertzbeat queue")
|
||||
.attributes(attributes)
|
||||
.resource(resource)
|
||||
.instrumentationScope(scope)
|
||||
.traceId("1234567890abcdef1234567890abcdef")
|
||||
.spanId("1234567890abcdef")
|
||||
.traceFlags(1)
|
||||
.build();
|
||||
|
||||
// Test sending and polling log entry
|
||||
queue.sendLogEntry(logEntry);
|
||||
|
||||
LogEntry polledLogEntry = queue.pollLogEntry();
|
||||
|
||||
assertNotNull(polledLogEntry);
|
||||
assertEquals(logEntry.getSeverityText(), polledLogEntry.getSeverityText());
|
||||
assertEquals(logEntry.getBody(), polledLogEntry.getBody());
|
||||
assertEquals(logEntry.getTraceId(), polledLogEntry.getTraceId());
|
||||
assertEquals(logEntry.getSpanId(), polledLogEntry.getSpanId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLogEntryToStorage() throws InterruptedException {
|
||||
|
||||
// Create a simple log entry for storage test
|
||||
LogEntry logEntry = LogEntry.builder()
|
||||
.timeUnixNano(Instant.now().toEpochMilli() * 1_000_000L)
|
||||
.severityNumber(17) // ERROR level
|
||||
.severityText("ERROR")
|
||||
.body("Error log message for storage")
|
||||
.build();
|
||||
|
||||
// Test sending and polling log entry to storage
|
||||
queue.sendLogEntryToStorage(logEntry);
|
||||
|
||||
LogEntry polledLogEntry = queue.pollLogEntryToStorage();
|
||||
|
||||
assertNotNull(polledLogEntry);
|
||||
assertEquals(logEntry.getSeverityText(), polledLogEntry.getSeverityText());
|
||||
assertEquals(logEntry.getBody(), polledLogEntry.getBody());
|
||||
assertEquals(logEntry.getSeverityNumber(), polledLogEntry.getSeverityNumber());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetQueueSizeMetricsInfo() {
|
||||
|
||||
@@ -124,29 +56,19 @@ class InMemoryCommonDataQueueTest {
|
||||
|
||||
assertEquals(0, metricsInfo.get("metricsDataToAlertQueue"));
|
||||
assertEquals(0, metricsInfo.get("metricsDataToStorageQueue"));
|
||||
assertEquals(0, metricsInfo.get("logEntryQueue"));
|
||||
assertEquals(0, metricsInfo.get("logEntryToStorageQueue"));
|
||||
|
||||
// Add metrics data and log entries
|
||||
queue.sendMetricsData(CollectRep.MetricsData.newBuilder().build());
|
||||
queue.sendLogEntry(LogEntry.builder().body("Test log").build());
|
||||
queue.sendLogEntryToStorage(LogEntry.builder().body("Storage log").build());
|
||||
|
||||
metricsInfo = queue.getQueueSizeMetricsInfo();
|
||||
|
||||
assertEquals(1, metricsInfo.get("metricsDataToAlertQueue"));
|
||||
assertEquals(0, metricsInfo.get("metricsDataToStorageQueue"));
|
||||
assertEquals(1, metricsInfo.get("logEntryQueue"));
|
||||
assertEquals(1, metricsInfo.get("logEntryToStorageQueue"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDestroy() {
|
||||
|
||||
// Add both metrics data and log entries before destroy
|
||||
queue.sendMetricsData(CollectRep.MetricsData.newBuilder().build());
|
||||
queue.sendLogEntry(LogEntry.builder().body("Test log").build());
|
||||
queue.sendLogEntryToStorage(LogEntry.builder().body("Storage log").build());
|
||||
|
||||
queue.destroy();
|
||||
|
||||
@@ -154,8 +76,6 @@ class InMemoryCommonDataQueueTest {
|
||||
|
||||
assertEquals(0, metricsInfo.get("metricsDataToAlertQueue"));
|
||||
assertEquals(0, metricsInfo.get("metricsDataToStorageQueue"));
|
||||
assertEquals(0, metricsInfo.get("logEntryQueue"));
|
||||
assertEquals(0, metricsInfo.get("logEntryToStorageQueue"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-123
@@ -27,13 +27,10 @@ import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.withSettings;
|
||||
import java.lang.reflect.Field;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.apache.hertzbeat.common.config.CommonProperties;
|
||||
import org.apache.hertzbeat.common.entity.log.LogEntry;
|
||||
import org.apache.hertzbeat.common.entity.message.CollectRep;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecord;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecords;
|
||||
@@ -58,15 +55,6 @@ class KafkaCommonDataQueueTest {
|
||||
@Mock(lenient = true)
|
||||
private KafkaProducer<Long, CollectRep.MetricsData> metricsDataProducer;
|
||||
|
||||
@Mock(lenient = true)
|
||||
private KafkaProducer<Long, LogEntry> logEntryProducer;
|
||||
|
||||
@Mock(lenient = true)
|
||||
private KafkaConsumer<Long, LogEntry> logEntryConsumer;
|
||||
|
||||
@Mock(lenient = true)
|
||||
private KafkaConsumer<Long, LogEntry> logEntryToStorageConsumer;
|
||||
|
||||
private KafkaCommonDataQueue kafkaCommonDataQueue;
|
||||
|
||||
private CommonProperties commonProperties;
|
||||
@@ -82,8 +70,6 @@ class KafkaCommonDataQueueTest {
|
||||
|
||||
// Set all required topics
|
||||
when(kafkaProperties.getMetricsDataTopic()).thenReturn("metricsDataTopic");
|
||||
when(kafkaProperties.getLogEntryDataTopic()).thenReturn("logEntryDataTopic");
|
||||
when(kafkaProperties.getLogEntryDataToStorageTopic()).thenReturn("logEntryDataToStorageTopic");
|
||||
when(kafkaProperties.getAlertsDataTopic()).thenReturn("alertsDataTopic");
|
||||
when(kafkaProperties.getMetricsDataToStorageTopic()).thenReturn("metricsDataToStorageTopic");
|
||||
when(kafkaProperties.getServiceDiscoveryDataTopic()).thenReturn("serviceDiscoveryDataTopic");
|
||||
@@ -91,17 +77,12 @@ class KafkaCommonDataQueueTest {
|
||||
|
||||
// Simulate the subscribe method for consumers
|
||||
doNothing().when(metricsDataToAlertConsumer).subscribe(anyCollection());
|
||||
doNothing().when(logEntryConsumer).subscribe(anyCollection());
|
||||
doNothing().when(logEntryToStorageConsumer).subscribe(anyCollection());
|
||||
|
||||
kafkaCommonDataQueue = new KafkaCommonDataQueue(commonProperties);
|
||||
|
||||
// Use reflection to set private fields
|
||||
setPrivateField(kafkaCommonDataQueue, "metricsDataProducer", metricsDataProducer);
|
||||
setPrivateField(kafkaCommonDataQueue, "metricsDataToAlertConsumer", metricsDataToAlertConsumer);
|
||||
setPrivateField(kafkaCommonDataQueue, "logEntryProducer", logEntryProducer);
|
||||
setPrivateField(kafkaCommonDataQueue, "logEntryConsumer", logEntryConsumer);
|
||||
setPrivateField(kafkaCommonDataQueue, "logEntryToStorageConsumer", logEntryToStorageConsumer);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -141,115 +122,11 @@ class KafkaCommonDataQueueTest {
|
||||
verify(metricsDataToAlertConsumer).commitAsync();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSendLogEntry() {
|
||||
// Create a test log entry with comprehensive data
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
attributes.put("service.name", "hertzbeat");
|
||||
attributes.put("service.version", "1.0.0");
|
||||
|
||||
LogEntry logEntry = LogEntry.builder()
|
||||
.timeUnixNano(Instant.now().toEpochMilli() * 1_000_000L)
|
||||
.severityNumber(9) // INFO level
|
||||
.severityText("INFO")
|
||||
.body("Test log message for Kafka queue")
|
||||
.attributes(attributes)
|
||||
.traceId("1234567890abcdef1234567890abcdef")
|
||||
.spanId("1234567890abcdef")
|
||||
.build();
|
||||
|
||||
// Test sending log entry
|
||||
kafkaCommonDataQueue.sendLogEntry(logEntry);
|
||||
|
||||
// Verify that the producer was called
|
||||
verify(logEntryProducer).send(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSendLogEntryToStorage() {
|
||||
// Create a test log entry for storage
|
||||
LogEntry logEntry = LogEntry.builder()
|
||||
.timeUnixNano(Instant.now().toEpochMilli() * 1_000_000L)
|
||||
.severityNumber(17) // ERROR level
|
||||
.severityText("ERROR")
|
||||
.body("Error log message for storage via Kafka")
|
||||
.build();
|
||||
|
||||
// Test sending log entry to storage
|
||||
kafkaCommonDataQueue.sendLogEntryToStorage(logEntry);
|
||||
|
||||
// Verify that the producer was called
|
||||
verify(logEntryProducer).send(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPollLogEntry() throws InterruptedException {
|
||||
// Create test log entry
|
||||
LogEntry expectedLogEntry = LogEntry.builder()
|
||||
.timeUnixNano(Instant.now().toEpochMilli() * 1_000_000L)
|
||||
.severityNumber(13) // WARN level
|
||||
.severityText("WARN")
|
||||
.body("Test warning log message")
|
||||
.build();
|
||||
|
||||
// Create a ConsumerRecord containing test log entry
|
||||
ConsumerRecord<Long, LogEntry> record =
|
||||
new ConsumerRecord<>("logEntryDataTopic", 0, 0L, 1L, expectedLogEntry);
|
||||
|
||||
// Create ConsumerRecords containing the log entry record
|
||||
Map<TopicPartition, List<ConsumerRecord<Long, LogEntry>>> recordsMap =
|
||||
Collections.singletonMap(
|
||||
new TopicPartition("logEntryDataTopic", 0),
|
||||
Collections.singletonList(record));
|
||||
ConsumerRecords<Long, LogEntry> records = new ConsumerRecords<>(recordsMap);
|
||||
|
||||
when(logEntryConsumer.poll(any(Duration.class))).thenReturn(records);
|
||||
|
||||
LogEntry result = kafkaCommonDataQueue.pollLogEntry();
|
||||
|
||||
assertEquals(expectedLogEntry, result);
|
||||
verify(logEntryConsumer).commitAsync();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPollLogEntryToStorage() throws InterruptedException {
|
||||
// Create test log entry for storage
|
||||
LogEntry expectedLogEntry = LogEntry.builder()
|
||||
.timeUnixNano(Instant.now().toEpochMilli() * 1_000_000L)
|
||||
.severityNumber(21) // FATAL level
|
||||
.severityText("FATAL")
|
||||
.body("Critical error log for storage")
|
||||
.build();
|
||||
|
||||
// Create a ConsumerRecord containing test log entry
|
||||
ConsumerRecord<Long, LogEntry> record =
|
||||
new ConsumerRecord<>("logEntryDataToStorageTopic", 0, 0L, 1L, expectedLogEntry);
|
||||
|
||||
// Create ConsumerRecords containing the log entry record
|
||||
Map<TopicPartition, List<ConsumerRecord<Long, LogEntry>>> recordsMap =
|
||||
Collections.singletonMap(
|
||||
new TopicPartition("logEntryDataToStorageTopic", 0),
|
||||
Collections.singletonList(record));
|
||||
ConsumerRecords<Long, LogEntry> records = new ConsumerRecords<>(recordsMap);
|
||||
|
||||
when(logEntryToStorageConsumer.poll(any(Duration.class))).thenReturn(records);
|
||||
|
||||
LogEntry result = kafkaCommonDataQueue.pollLogEntryToStorage();
|
||||
|
||||
assertEquals(expectedLogEntry, result);
|
||||
verify(logEntryToStorageConsumer).commitAsync();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDestroy() throws Exception {
|
||||
kafkaCommonDataQueue.destroy();
|
||||
|
||||
// Verify that all producers and consumers are closed
|
||||
verify(metricsDataToAlertConsumer).close();
|
||||
verify(metricsDataProducer).close();
|
||||
verify(logEntryProducer).close();
|
||||
verify(logEntryConsumer).close();
|
||||
verify(logEntryToStorageConsumer).close();
|
||||
}
|
||||
|
||||
private void setPrivateField(Object object, String fieldName, Object value) throws Exception {
|
||||
|
||||
-91
@@ -27,13 +27,8 @@ import io.lettuce.core.RedisClient;
|
||||
import io.lettuce.core.RedisURI;
|
||||
import io.lettuce.core.api.StatefulRedisConnection;
|
||||
import io.lettuce.core.api.sync.RedisCommands;
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.apache.hertzbeat.common.config.CommonProperties;
|
||||
import org.apache.hertzbeat.common.entity.log.LogEntry;
|
||||
import org.apache.hertzbeat.common.entity.message.CollectRep;
|
||||
import org.apache.hertzbeat.common.serialize.RedisLogEntryCodec;
|
||||
import org.apache.hertzbeat.common.serialize.RedisMetricsDataCodec;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -51,13 +46,9 @@ class RedisCommonDataQueueTest {
|
||||
|
||||
@Mock
|
||||
private StatefulRedisConnection<String, CollectRep.MetricsData> connection;
|
||||
@Mock
|
||||
private StatefulRedisConnection<String, LogEntry> logEntryConnection;
|
||||
|
||||
@Mock
|
||||
private RedisCommands<String, CollectRep.MetricsData> syncCommands;
|
||||
@Mock
|
||||
private RedisCommands<String, LogEntry> logEntrySyncCommands;
|
||||
|
||||
private RedisClient redisClient;
|
||||
private CommonProperties commonProperties;
|
||||
@@ -74,16 +65,12 @@ class RedisCommonDataQueueTest {
|
||||
when(commonProperties.getQueue()).thenReturn(dataQueueProperties);
|
||||
when(dataQueueProperties.getRedis()).thenReturn(redisProperties);
|
||||
when(redisProperties.getMetricsDataQueueNameToAlerter()).thenReturn("metricsDataQueueToAlerter");
|
||||
when(redisProperties.getLogEntryQueueName()).thenReturn("logEntryQueue");
|
||||
when(redisProperties.getLogEntryToStorageQueueName()).thenReturn("logEntryToStorageQueue");
|
||||
when(redisProperties.getRedisHost()).thenReturn("localhost");
|
||||
when(redisProperties.getRedisPort()).thenReturn(6379);
|
||||
|
||||
try (MockedStatic<RedisClient> mockedRedisClient = mockStatic(RedisClient.class)) {
|
||||
mockedRedisClient.when(() -> RedisClient.create(any(RedisURI.class))).thenReturn(redisClient);
|
||||
when(redisClient.connect(any(RedisMetricsDataCodec.class))).thenReturn(connection);
|
||||
when(redisClient.connect(any(RedisLogEntryCodec.class))).thenReturn(logEntryConnection);
|
||||
when(logEntryConnection.sync()).thenReturn(logEntrySyncCommands);
|
||||
when(connection.sync()).thenReturn(syncCommands);
|
||||
|
||||
redisCommonDataQueue = new RedisCommonDataQueue(commonProperties);
|
||||
@@ -113,88 +100,10 @@ class RedisCommonDataQueueTest {
|
||||
verify(syncCommands).lpush("metricsDataQueueToAlerter", metricsData);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendLogEntry() {
|
||||
// Create a test log entry with comprehensive data
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
attributes.put("service.name", "hertzbeat");
|
||||
attributes.put("service.version", "1.0.0");
|
||||
|
||||
LogEntry logEntry = LogEntry.builder()
|
||||
.timeUnixNano(Instant.now().toEpochMilli() * 1_000_000L)
|
||||
.severityNumber(9) // INFO level
|
||||
.severityText("INFO")
|
||||
.body("Test log message for Redis queue")
|
||||
.attributes(attributes)
|
||||
.traceId("1234567890abcdef1234567890abcdef")
|
||||
.spanId("1234567890abcdef")
|
||||
.build();
|
||||
|
||||
// Test sending log entry
|
||||
redisCommonDataQueue.sendLogEntry(logEntry);
|
||||
|
||||
// Verify that the Redis commands were called
|
||||
verify(logEntrySyncCommands).lpush("logEntryQueue", logEntry);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSendLogEntryToStorage() {
|
||||
// Create a test log entry for storage
|
||||
LogEntry logEntry = LogEntry.builder()
|
||||
.timeUnixNano(Instant.now().toEpochMilli() * 1_000_000L)
|
||||
.severityNumber(17) // ERROR level
|
||||
.severityText("ERROR")
|
||||
.body("Error log message for storage via Redis")
|
||||
.build();
|
||||
|
||||
// Test sending log entry to storage
|
||||
redisCommonDataQueue.sendLogEntryToStorage(logEntry);
|
||||
|
||||
// Verify that the Redis commands were called
|
||||
verify(logEntrySyncCommands).lpush("logEntryToStorageQueue", logEntry);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPollLogEntry() throws Exception {
|
||||
// Create test log entry
|
||||
LogEntry expectedLogEntry = LogEntry.builder()
|
||||
.timeUnixNano(Instant.now().toEpochMilli() * 1_000_000L)
|
||||
.severityNumber(13) // WARN level
|
||||
.severityText("WARN")
|
||||
.body("Test warning log message")
|
||||
.build();
|
||||
|
||||
when(logEntrySyncCommands.rpop("logEntryQueue")).thenReturn(expectedLogEntry);
|
||||
|
||||
LogEntry result = redisCommonDataQueue.pollLogEntry();
|
||||
|
||||
assertEquals(expectedLogEntry, result);
|
||||
verify(logEntrySyncCommands).rpop("logEntryQueue");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPollLogEntryToStorage() throws Exception {
|
||||
// Create test log entry for storage
|
||||
LogEntry expectedLogEntry = LogEntry.builder()
|
||||
.timeUnixNano(Instant.now().toEpochMilli() * 1_000_000L)
|
||||
.severityNumber(21) // FATAL level
|
||||
.severityText("FATAL")
|
||||
.body("Critical error log for storage")
|
||||
.build();
|
||||
|
||||
when(logEntrySyncCommands.rpop("logEntryToStorageQueue")).thenReturn(expectedLogEntry);
|
||||
|
||||
LogEntry result = redisCommonDataQueue.pollLogEntryToStorage();
|
||||
|
||||
assertEquals(expectedLogEntry, result);
|
||||
verify(logEntrySyncCommands).rpop("logEntryToStorageQueue");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDestroy() {
|
||||
redisCommonDataQueue.destroy();
|
||||
verify(connection).close();
|
||||
verify(logEntryConnection).close();
|
||||
verify(redisClient).shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
-267
@@ -1,267 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.common.serialize;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.apache.hertzbeat.common.entity.log.LogEntry;
|
||||
import org.apache.kafka.common.header.Headers;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Test case for {@link KafkaLogEntryDeserializer}
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class KafkaLogEntryDeserializerTest {
|
||||
|
||||
private KafkaLogEntryDeserializer deserializer;
|
||||
private KafkaLogEntrySerializer serializer;
|
||||
|
||||
@Mock
|
||||
private Map<String, ?> configs;
|
||||
|
||||
@Mock
|
||||
private Headers headers;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
deserializer = new KafkaLogEntryDeserializer();
|
||||
serializer = new KafkaLogEntrySerializer();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConfigure() {
|
||||
// Test that configure method doesn't throw any exceptions
|
||||
deserializer.configure(configs, false);
|
||||
deserializer.configure(configs, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeserializeWithValidLogEntry() {
|
||||
// Create a comprehensive log entry
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
attributes.put("service.name", "hertzbeat");
|
||||
attributes.put("service.version", "1.0.0");
|
||||
attributes.put("environment", "test");
|
||||
|
||||
Map<String, Object> resource = new HashMap<>();
|
||||
resource.put("host.name", "localhost");
|
||||
resource.put("os.type", "linux");
|
||||
|
||||
LogEntry.InstrumentationScope scope = LogEntry.InstrumentationScope.builder()
|
||||
.name("org.apache.hertzbeat.test")
|
||||
.version("1.0.0")
|
||||
.attributes(new HashMap<>())
|
||||
.droppedAttributesCount(0)
|
||||
.build();
|
||||
|
||||
LogEntry originalLogEntry = LogEntry.builder()
|
||||
.timeUnixNano(1234567890123456789L)
|
||||
.observedTimeUnixNano(1234567890123456790L)
|
||||
.severityNumber(9) // INFO level
|
||||
.severityText("INFO")
|
||||
.body("Test log message for deserialization")
|
||||
.attributes(attributes)
|
||||
.droppedAttributesCount(0)
|
||||
.traceId("1234567890abcdef1234567890abcdef")
|
||||
.spanId("1234567890abcdef")
|
||||
.traceFlags(1)
|
||||
.resource(resource)
|
||||
.instrumentationScope(scope)
|
||||
.build();
|
||||
|
||||
// Serialize the log entry to bytes
|
||||
byte[] bytes = serializer.serialize("test-topic", originalLogEntry);
|
||||
assertNotNull(bytes);
|
||||
|
||||
// Deserialize the bytes back to log entry
|
||||
LogEntry deserializedLogEntry = deserializer.deserialize("test-topic", bytes);
|
||||
|
||||
assertNotNull(deserializedLogEntry);
|
||||
assertEquals(originalLogEntry.getTimeUnixNano(), deserializedLogEntry.getTimeUnixNano());
|
||||
assertEquals(originalLogEntry.getObservedTimeUnixNano(), deserializedLogEntry.getObservedTimeUnixNano());
|
||||
assertEquals(originalLogEntry.getSeverityNumber(), deserializedLogEntry.getSeverityNumber());
|
||||
assertEquals(originalLogEntry.getSeverityText(), deserializedLogEntry.getSeverityText());
|
||||
assertEquals(originalLogEntry.getBody(), deserializedLogEntry.getBody());
|
||||
assertEquals(originalLogEntry.getTraceId(), deserializedLogEntry.getTraceId());
|
||||
assertEquals(originalLogEntry.getSpanId(), deserializedLogEntry.getSpanId());
|
||||
assertEquals(originalLogEntry.getTraceFlags(), deserializedLogEntry.getTraceFlags());
|
||||
assertEquals(originalLogEntry.getDroppedAttributesCount(), deserializedLogEntry.getDroppedAttributesCount());
|
||||
|
||||
// Verify attributes
|
||||
assertNotNull(deserializedLogEntry.getAttributes());
|
||||
assertEquals("hertzbeat", deserializedLogEntry.getAttributes().get("service.name"));
|
||||
assertEquals("1.0.0", deserializedLogEntry.getAttributes().get("service.version"));
|
||||
assertEquals("test", deserializedLogEntry.getAttributes().get("environment"));
|
||||
|
||||
// Verify resource
|
||||
assertNotNull(deserializedLogEntry.getResource());
|
||||
assertEquals("localhost", deserializedLogEntry.getResource().get("host.name"));
|
||||
assertEquals("linux", deserializedLogEntry.getResource().get("os.type"));
|
||||
|
||||
// Verify instrumentation scope
|
||||
assertNotNull(deserializedLogEntry.getInstrumentationScope());
|
||||
assertEquals("org.apache.hertzbeat.test", deserializedLogEntry.getInstrumentationScope().getName());
|
||||
assertEquals("1.0.0", deserializedLogEntry.getInstrumentationScope().getVersion());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeserializeWithSimpleLogEntry() {
|
||||
// Test with minimal log entry
|
||||
LogEntry originalLogEntry = LogEntry.builder()
|
||||
.timeUnixNano(System.currentTimeMillis() * 1_000_000L)
|
||||
.severityNumber(17) // ERROR level
|
||||
.severityText("ERROR")
|
||||
.body("Simple error message")
|
||||
.build();
|
||||
|
||||
// Serialize and deserialize
|
||||
byte[] bytes = serializer.serialize("test-topic", originalLogEntry);
|
||||
LogEntry deserializedLogEntry = deserializer.deserialize("test-topic", bytes);
|
||||
|
||||
assertNotNull(deserializedLogEntry);
|
||||
assertEquals(originalLogEntry.getTimeUnixNano(), deserializedLogEntry.getTimeUnixNano());
|
||||
assertEquals(originalLogEntry.getSeverityNumber(), deserializedLogEntry.getSeverityNumber());
|
||||
assertEquals(originalLogEntry.getSeverityText(), deserializedLogEntry.getSeverityText());
|
||||
assertEquals(originalLogEntry.getBody(), deserializedLogEntry.getBody());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeserializeWithNullData() {
|
||||
// Test deserialization with null data
|
||||
LogEntry logEntry = deserializer.deserialize("test-topic", null);
|
||||
|
||||
assertNull(logEntry);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeserializeWithEmptyData() {
|
||||
// Test deserialization with empty data
|
||||
byte[] emptyData = new byte[0];
|
||||
LogEntry logEntry = deserializer.deserialize("test-topic", emptyData);
|
||||
|
||||
assertNull(logEntry);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeserializeWithInvalidData() {
|
||||
// Test deserialization with invalid JSON data
|
||||
byte[] invalidData = "invalid json data".getBytes(StandardCharsets.UTF_8);
|
||||
LogEntry logEntry = deserializer.deserialize("test-topic", invalidData);
|
||||
|
||||
assertNull(logEntry);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeserializeWithMalformedJson() {
|
||||
// Test deserialization with malformed JSON
|
||||
String malformedJson = "{\"severityText\":\"INFO\",\"body\":\"test\","; // Missing closing brace
|
||||
byte[] malformedData = malformedJson.getBytes(StandardCharsets.UTF_8);
|
||||
LogEntry logEntry = deserializer.deserialize("test-topic", malformedData);
|
||||
|
||||
assertNull(logEntry);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeserializeWithHeaders() {
|
||||
// Test deserialization with headers
|
||||
LogEntry originalLogEntry = LogEntry.builder()
|
||||
.timeUnixNano(System.currentTimeMillis() * 1_000_000L)
|
||||
.severityNumber(13) // WARN level
|
||||
.severityText("WARN")
|
||||
.body("Warning message with headers")
|
||||
.build();
|
||||
|
||||
// Serialize and deserialize with headers
|
||||
byte[] bytes = serializer.serialize("test-topic", headers, originalLogEntry);
|
||||
LogEntry deserializedLogEntry = deserializer.deserialize("test-topic", headers, bytes);
|
||||
|
||||
assertNotNull(deserializedLogEntry);
|
||||
assertEquals(originalLogEntry.getSeverityText(), deserializedLogEntry.getSeverityText());
|
||||
assertEquals(originalLogEntry.getBody(), deserializedLogEntry.getBody());
|
||||
assertEquals(originalLogEntry.getSeverityNumber(), deserializedLogEntry.getSeverityNumber());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeserializeWithComplexAttributes() {
|
||||
// Test with complex nested attributes
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
attributes.put("string_value", "test_string");
|
||||
attributes.put("numeric_value", 42);
|
||||
attributes.put("boolean_value", true);
|
||||
|
||||
// Note: Nested objects will be deserialized as LinkedHashMap by Jackson
|
||||
Map<String, Object> nestedMap = new HashMap<>();
|
||||
nestedMap.put("nested_key", "nested_value");
|
||||
attributes.put("nested_object", nestedMap);
|
||||
|
||||
LogEntry originalLogEntry = LogEntry.builder()
|
||||
.timeUnixNano(System.currentTimeMillis() * 1_000_000L)
|
||||
.severityNumber(5) // DEBUG level
|
||||
.severityText("DEBUG")
|
||||
.body("Debug message with complex attributes")
|
||||
.attributes(attributes)
|
||||
.build();
|
||||
|
||||
// Serialize and deserialize
|
||||
byte[] bytes = serializer.serialize("test-topic", originalLogEntry);
|
||||
LogEntry deserializedLogEntry = deserializer.deserialize("test-topic", bytes);
|
||||
|
||||
assertNotNull(deserializedLogEntry);
|
||||
assertEquals(originalLogEntry.getSeverityText(), deserializedLogEntry.getSeverityText());
|
||||
assertEquals(originalLogEntry.getBody(), deserializedLogEntry.getBody());
|
||||
|
||||
// Verify complex attributes
|
||||
assertNotNull(deserializedLogEntry.getAttributes());
|
||||
assertEquals("test_string", deserializedLogEntry.getAttributes().get("string_value"));
|
||||
assertEquals(42, deserializedLogEntry.getAttributes().get("numeric_value"));
|
||||
assertEquals(true, deserializedLogEntry.getAttributes().get("boolean_value"));
|
||||
|
||||
// Verify nested object (will be LinkedHashMap after JSON deserialization)
|
||||
Object nestedObject = deserializedLogEntry.getAttributes().get("nested_object");
|
||||
assertNotNull(nestedObject);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeserializeWithPartialLogEntry() {
|
||||
// Test with valid JSON but partial log entry structure
|
||||
String partialJson = "{\"severityText\":\"INFO\",\"body\":\"partial log entry\"}";
|
||||
byte[] partialData = partialJson.getBytes(StandardCharsets.UTF_8);
|
||||
LogEntry logEntry = deserializer.deserialize("test-topic", partialData);
|
||||
|
||||
assertNotNull(logEntry);
|
||||
assertEquals("INFO", logEntry.getSeverityText());
|
||||
assertEquals("partial log entry", logEntry.getBody());
|
||||
assertNull(logEntry.getSeverityNumber()); // Not provided in JSON
|
||||
assertNull(logEntry.getTimeUnixNano()); // Not provided in JSON
|
||||
}
|
||||
|
||||
@Test
|
||||
void testClose() {
|
||||
// Test that close method doesn't throw any exceptions
|
||||
deserializer.close();
|
||||
}
|
||||
}
|
||||
-202
@@ -1,202 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.common.serialize;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.apache.hertzbeat.common.entity.log.LogEntry;
|
||||
import org.apache.kafka.common.header.Headers;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Test case for {@link KafkaLogEntrySerializer}
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class KafkaLogEntrySerializerTest {
|
||||
|
||||
private KafkaLogEntrySerializer serializer;
|
||||
|
||||
@Mock
|
||||
private Map<String, ?> configs;
|
||||
|
||||
@Mock
|
||||
private Headers headers;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
serializer = new KafkaLogEntrySerializer();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConfigure() {
|
||||
// Test that configure method doesn't throw any exceptions
|
||||
serializer.configure(configs, false);
|
||||
serializer.configure(configs, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSerializeWithLogEntry() {
|
||||
// Create a comprehensive log entry for testing
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
attributes.put("service.name", "hertzbeat");
|
||||
attributes.put("service.version", "1.0.0");
|
||||
attributes.put("environment", "test");
|
||||
|
||||
Map<String, Object> resource = new HashMap<>();
|
||||
resource.put("host.name", "localhost");
|
||||
resource.put("os.type", "linux");
|
||||
|
||||
LogEntry.InstrumentationScope scope = LogEntry.InstrumentationScope.builder()
|
||||
.name("org.apache.hertzbeat.test")
|
||||
.version("1.0.0")
|
||||
.attributes(new HashMap<>())
|
||||
.droppedAttributesCount(0)
|
||||
.build();
|
||||
|
||||
LogEntry logEntry = LogEntry.builder()
|
||||
.timeUnixNano(Instant.now().toEpochMilli() * 1_000_000L)
|
||||
.observedTimeUnixNano(Instant.now().toEpochMilli() * 1_000_000L)
|
||||
.severityNumber(9) // INFO level
|
||||
.severityText("INFO")
|
||||
.body("Test log message for serialization")
|
||||
.attributes(attributes)
|
||||
.droppedAttributesCount(0)
|
||||
.traceId("1234567890abcdef1234567890abcdef")
|
||||
.spanId("1234567890abcdef")
|
||||
.traceFlags(1)
|
||||
.resource(resource)
|
||||
.instrumentationScope(scope)
|
||||
.build();
|
||||
|
||||
byte[] bytes = serializer.serialize("test-topic", logEntry);
|
||||
|
||||
assertNotNull(bytes);
|
||||
assertTrue(bytes.length > 0);
|
||||
|
||||
// Verify the JSON contains expected fields
|
||||
String jsonString = new String(bytes, StandardCharsets.UTF_8);
|
||||
assertTrue(jsonString.contains("\"severityText\":\"INFO\""));
|
||||
assertTrue(jsonString.contains("\"body\":\"Test log message for serialization\""));
|
||||
assertTrue(jsonString.contains("\"traceId\":\"1234567890abcdef1234567890abcdef\""));
|
||||
assertTrue(jsonString.contains("\"spanId\":\"1234567890abcdef\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSerializeWithSimpleLogEntry() {
|
||||
// Test with minimal log entry
|
||||
LogEntry logEntry = LogEntry.builder()
|
||||
.timeUnixNano(System.currentTimeMillis() * 1_000_000L)
|
||||
.severityNumber(17) // ERROR level
|
||||
.severityText("ERROR")
|
||||
.body("Simple error message")
|
||||
.build();
|
||||
|
||||
byte[] bytes = serializer.serialize("test-topic", logEntry);
|
||||
|
||||
assertNotNull(bytes);
|
||||
assertTrue(bytes.length > 0);
|
||||
|
||||
String jsonString = new String(bytes, StandardCharsets.UTF_8);
|
||||
assertTrue(jsonString.contains("\"severityText\":\"ERROR\""));
|
||||
assertTrue(jsonString.contains("\"body\":\"Simple error message\""));
|
||||
assertTrue(jsonString.contains("\"severityNumber\":17"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSerializeWithNullLogEntry() {
|
||||
// Test serialization with null log entry
|
||||
byte[] bytes = serializer.serialize("test-topic", null);
|
||||
|
||||
assertNull(bytes);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSerializeWithHeaders() {
|
||||
// Test serialization with headers
|
||||
LogEntry logEntry = LogEntry.builder()
|
||||
.timeUnixNano(System.currentTimeMillis() * 1_000_000L)
|
||||
.severityNumber(13) // WARN level
|
||||
.severityText("WARN")
|
||||
.body("Warning message with headers")
|
||||
.build();
|
||||
|
||||
byte[] bytes = serializer.serialize("test-topic", headers, logEntry);
|
||||
|
||||
assertNotNull(bytes);
|
||||
assertTrue(bytes.length > 0);
|
||||
|
||||
String jsonString = new String(bytes, StandardCharsets.UTF_8);
|
||||
assertTrue(jsonString.contains("\"severityText\":\"WARN\""));
|
||||
assertTrue(jsonString.contains("\"body\":\"Warning message with headers\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSerializeWithNullLogEntryAndHeaders() {
|
||||
// Test serialization with null log entry and headers
|
||||
byte[] bytes = serializer.serialize("test-topic", headers, null);
|
||||
|
||||
assertNull(bytes);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSerializeWithComplexAttributes() {
|
||||
// Test with complex nested attributes
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
attributes.put("string_value", "test_string");
|
||||
attributes.put("numeric_value", 42);
|
||||
attributes.put("boolean_value", true);
|
||||
|
||||
Map<String, Object> nestedMap = new HashMap<>();
|
||||
nestedMap.put("nested_key", "nested_value");
|
||||
attributes.put("nested_object", nestedMap);
|
||||
|
||||
LogEntry logEntry = LogEntry.builder()
|
||||
.timeUnixNano(System.currentTimeMillis() * 1_000_000L)
|
||||
.severityNumber(5) // DEBUG level
|
||||
.severityText("DEBUG")
|
||||
.body("Debug message with complex attributes")
|
||||
.attributes(attributes)
|
||||
.build();
|
||||
|
||||
byte[] bytes = serializer.serialize("test-topic", logEntry);
|
||||
|
||||
assertNotNull(bytes);
|
||||
assertTrue(bytes.length > 0);
|
||||
|
||||
String jsonString = new String(bytes, StandardCharsets.UTF_8);
|
||||
assertTrue(jsonString.contains("\"severityText\":\"DEBUG\""));
|
||||
assertTrue(jsonString.contains("\"string_value\":\"test_string\""));
|
||||
assertTrue(jsonString.contains("\"numeric_value\":42"));
|
||||
assertTrue(jsonString.contains("\"boolean_value\":true"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testClose() {
|
||||
// Test that close method doesn't throw any exceptions
|
||||
serializer.close();
|
||||
}
|
||||
}
|
||||
-161
@@ -1,161 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.common.serialize;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
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.junit.jupiter.MockitoExtension;
|
||||
|
||||
/**
|
||||
* Test case for {@link RedisLogEntryCodec}
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class RedisLogEntryCodecTest {
|
||||
|
||||
private RedisLogEntryCodec codec;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
codec = new RedisLogEntryCodec();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEncodeDecodeKey() {
|
||||
String key = "test-log-key";
|
||||
|
||||
// Test encoding
|
||||
ByteBuffer encodedKey = codec.encodeKey(key);
|
||||
assertNotNull(encodedKey);
|
||||
|
||||
// Test decoding
|
||||
String decodedKey = codec.decodeKey(encodedKey);
|
||||
assertEquals(key, decodedKey);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEncodeDecodeSimpleLogEntry() {
|
||||
// Create a simple log entry
|
||||
LogEntry logEntry = LogEntry.builder()
|
||||
.timeUnixNano(System.currentTimeMillis() * 1_000_000L)
|
||||
.severityNumber(17) // ERROR level
|
||||
.severityText("ERROR")
|
||||
.body("Simple error message")
|
||||
.build();
|
||||
|
||||
// Test encoding
|
||||
ByteBuffer encodedValue = codec.encodeValue(logEntry);
|
||||
assertNotNull(encodedValue);
|
||||
assertTrue(encodedValue.hasRemaining());
|
||||
|
||||
// Test decoding
|
||||
LogEntry decodedLogEntry = codec.decodeValue(encodedValue);
|
||||
assertNotNull(decodedLogEntry);
|
||||
assertEquals(logEntry.getSeverityNumber(), decodedLogEntry.getSeverityNumber());
|
||||
assertEquals(logEntry.getSeverityText(), decodedLogEntry.getSeverityText());
|
||||
assertEquals(logEntry.getBody(), decodedLogEntry.getBody());
|
||||
assertEquals(logEntry.getTimeUnixNano(), decodedLogEntry.getTimeUnixNano());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEncodeDecodeComplexLogEntry() {
|
||||
// Create a comprehensive log entry for testing
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
attributes.put("service.name", "hertzbeat");
|
||||
attributes.put("service.version", "1.0.0");
|
||||
attributes.put("environment", "test");
|
||||
attributes.put("numeric_value", 42);
|
||||
attributes.put("boolean_value", true);
|
||||
|
||||
Map<String, Object> resource = new HashMap<>();
|
||||
resource.put("host.name", "localhost");
|
||||
resource.put("os.type", "linux");
|
||||
|
||||
LogEntry.InstrumentationScope scope = LogEntry.InstrumentationScope.builder()
|
||||
.name("org.apache.hertzbeat.test")
|
||||
.version("1.0.0")
|
||||
.attributes(new HashMap<>())
|
||||
.droppedAttributesCount(0)
|
||||
.build();
|
||||
|
||||
LogEntry logEntry = LogEntry.builder()
|
||||
.timeUnixNano(Instant.now().toEpochMilli() * 1_000_000L)
|
||||
.observedTimeUnixNano(Instant.now().toEpochMilli() * 1_000_000L)
|
||||
.severityNumber(9) // INFO level
|
||||
.severityText("INFO")
|
||||
.body("Test log message for Redis codec")
|
||||
.attributes(attributes)
|
||||
.droppedAttributesCount(0)
|
||||
.traceId("1234567890abcdef1234567890abcdef")
|
||||
.spanId("1234567890abcdef")
|
||||
.traceFlags(1)
|
||||
.resource(resource)
|
||||
.instrumentationScope(scope)
|
||||
.build();
|
||||
|
||||
// Test encoding
|
||||
ByteBuffer encodedValue = codec.encodeValue(logEntry);
|
||||
assertNotNull(encodedValue);
|
||||
assertTrue(encodedValue.hasRemaining());
|
||||
|
||||
// Verify the JSON structure
|
||||
String jsonString = new String(encodedValue.array(), StandardCharsets.UTF_8);
|
||||
assertTrue(jsonString.contains("\"severityText\":\"INFO\""));
|
||||
assertTrue(jsonString.contains("\"body\":\"Test log message for Redis codec\""));
|
||||
assertTrue(jsonString.contains("\"traceId\":\"1234567890abcdef1234567890abcdef\""));
|
||||
assertTrue(jsonString.contains("\"spanId\":\"1234567890abcdef\""));
|
||||
|
||||
// Test decoding
|
||||
LogEntry decodedLogEntry = codec.decodeValue(encodedValue);
|
||||
assertNotNull(decodedLogEntry);
|
||||
assertEquals(logEntry.getSeverityNumber(), decodedLogEntry.getSeverityNumber());
|
||||
assertEquals(logEntry.getSeverityText(), decodedLogEntry.getSeverityText());
|
||||
assertEquals(logEntry.getBody(), decodedLogEntry.getBody());
|
||||
assertEquals(logEntry.getTraceId(), decodedLogEntry.getTraceId());
|
||||
assertEquals(logEntry.getSpanId(), decodedLogEntry.getSpanId());
|
||||
assertEquals(logEntry.getTraceFlags(), decodedLogEntry.getTraceFlags());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDecodeEmptyByteBuffer() {
|
||||
// Test decoding empty byte buffer
|
||||
ByteBuffer emptyBuffer = ByteBuffer.allocate(0);
|
||||
LogEntry decodedLogEntry = codec.decodeValue(emptyBuffer);
|
||||
assertNull(decodedLogEntry);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDecodeInvalidJson() {
|
||||
// Test decoding invalid JSON
|
||||
String invalidJson = "{ invalid json }";
|
||||
ByteBuffer invalidBuffer = ByteBuffer.wrap(invalidJson.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
LogEntry decodedLogEntry = codec.decodeValue(invalidBuffer);
|
||||
assertNull(decodedLogEntry);
|
||||
}
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
~ contributor license agreements. See the NOTICE file distributed with
|
||||
~ this work for additional information regarding copyright ownership.
|
||||
~ The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
~ (the "License"); you may not use this file except in compliance with
|
||||
~ the License. You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>org.apache.hertzbeat</groupId>
|
||||
<artifactId>hertzbeat-e2e</artifactId>
|
||||
<version>2.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>hertzbeat-log-e2e</artifactId>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<okhttp.version>4.12.0</okhttp.version>
|
||||
<jackson.version>2.15.2</jackson.version>
|
||||
<maven.resources.plugin.version>3.3.1</maven.resources.plugin.version>
|
||||
<maven.dependency.plugin.version>3.6.1</maven.dependency.plugin.version>
|
||||
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- HertzBeat modules -->
|
||||
<dependency>
|
||||
<groupId>org.apache.hertzbeat</groupId>
|
||||
<artifactId>hertzbeat-manager</artifactId>
|
||||
<version>${hertzbeat.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.hertzbeat</groupId>
|
||||
<artifactId>hertzbeat-log</artifactId>
|
||||
<version>${hertzbeat.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.hertzbeat</groupId>
|
||||
<artifactId>hertzbeat-alerter</artifactId>
|
||||
<version>${hertzbeat.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.hertzbeat</groupId>
|
||||
<artifactId>hertzbeat-warehouse</artifactId>
|
||||
<version>${hertzbeat.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- TestContainers -->
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Test -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- JUnit 5 -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- AssertJ -->
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Awaitility -->
|
||||
<dependency>
|
||||
<groupId>org.awaitility</groupId>
|
||||
<artifactId>awaitility</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<!-- Copy manager resources to test classpath -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<version>${maven.dependency.plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>unpack-manager-resources</id>
|
||||
<phase>generate-test-resources</phase>
|
||||
<goals>
|
||||
<goal>unpack</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<artifactItems>
|
||||
<artifactItem>
|
||||
<groupId>org.apache.hertzbeat</groupId>
|
||||
<artifactId>hertzbeat-manager</artifactId>
|
||||
<version>${hertzbeat.version}</version>
|
||||
<type>jar</type>
|
||||
<overWrite>true</overWrite>
|
||||
<outputDirectory>${project.build.testOutputDirectory}</outputDirectory>
|
||||
<includes>
|
||||
define/**,
|
||||
db/**,
|
||||
templates/**,
|
||||
grafana/**,
|
||||
*.yml,
|
||||
*.xml,
|
||||
*.txt
|
||||
</includes>
|
||||
</artifactItem>
|
||||
</artifactItems>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<!-- Ensure test resources are processed -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-resources-plugin</artifactId>
|
||||
<version>${maven.resources.plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy-test-resources</id>
|
||||
<phase>generate-test-resources</phase>
|
||||
<goals>
|
||||
<goal>testResources</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
-230
@@ -1,230 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.log.alert;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.alert.calculate.periodic.PeriodicAlertRuleScheduler;
|
||||
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.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestInstance;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.mock.mockito.SpyBean;
|
||||
import org.springframework.boot.test.web.server.LocalServerPort;
|
||||
import org.springframework.test.context.DynamicPropertyRegistry;
|
||||
import org.springframework.test.context.DynamicPropertySource;
|
||||
import org.testcontainers.containers.GenericContainer;
|
||||
import org.testcontainers.containers.Network;
|
||||
import org.testcontainers.containers.wait.strategy.Wait;
|
||||
import org.testcontainers.Testcontainers;
|
||||
import org.testcontainers.utility.DockerImageName;
|
||||
import org.testcontainers.utility.MountableFile;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
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.doAnswer;
|
||||
|
||||
/**
|
||||
* E2E tests for periodic log alert processing.
|
||||
*/
|
||||
@SpringBootTest(classes = org.apache.hertzbeat.manager.Manager.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@Slf4j
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
public class LogPeriodicAlertE2eTest {
|
||||
|
||||
private static final String VECTOR_IMAGE = "timberio/vector:latest-alpine";
|
||||
private static final int VECTOR_PORT = 8686;
|
||||
private static final String VECTOR_CONFIG_PATH = "/etc/vector/vector.yml";
|
||||
private static final String ENV_HERTZBEAT_PORT = "HERTZBEAT_PORT";
|
||||
private static final String GREPTIME_IMAGE = "greptime/greptimedb:latest";
|
||||
private static final int GREPTIME_HTTP_PORT = 4000;
|
||||
private static final int GREPTIME_GRPC_PORT = 4001;
|
||||
private static final Duration CONTAINER_STARTUP_TIMEOUT = Duration.ofSeconds(120);
|
||||
|
||||
@LocalServerPort
|
||||
private int port;
|
||||
|
||||
@Autowired
|
||||
PeriodicAlertRuleScheduler periodicAlertRuleScheduler;
|
||||
|
||||
AlertDefine errorCountAlertByGroup;
|
||||
AlertDefine errorCountAlertByIndividual;
|
||||
|
||||
@SpyBean
|
||||
private AlarmCommonReduce alarmCommonReduce;
|
||||
|
||||
static GenericContainer<?> vector;
|
||||
|
||||
static GenericContainer<?> greptimedb;
|
||||
|
||||
static {
|
||||
greptimedb = new GenericContainer<>(DockerImageName.parse(GREPTIME_IMAGE))
|
||||
.withExposedPorts(GREPTIME_HTTP_PORT, GREPTIME_GRPC_PORT)
|
||||
.withCommand("standalone", "start",
|
||||
"--http-addr", "0.0.0.0:" + GREPTIME_HTTP_PORT,
|
||||
"--rpc-bind-addr", "0.0.0.0:" + GREPTIME_GRPC_PORT)
|
||||
.waitingFor(Wait.forListeningPorts(GREPTIME_HTTP_PORT, GREPTIME_GRPC_PORT))
|
||||
.withStartupTimeout(CONTAINER_STARTUP_TIMEOUT);
|
||||
greptimedb.start();
|
||||
}
|
||||
|
||||
@DynamicPropertySource
|
||||
static void greptimeProps(DynamicPropertyRegistry r) {
|
||||
// Configure GreptimeDB storage
|
||||
r.add("warehouse.store.jpa.enabled", () -> "false");
|
||||
r.add("warehouse.store.greptime.enabled", () -> "true");
|
||||
r.add("warehouse.store.greptime.http-endpoint", () -> "http://localhost:" + greptimedb.getMappedPort(GREPTIME_HTTP_PORT));
|
||||
r.add("warehouse.store.greptime.grpc-endpoints", () -> "localhost:" + greptimedb.getMappedPort(GREPTIME_GRPC_PORT));
|
||||
r.add("warehouse.store.greptime.username", () -> "");
|
||||
r.add("warehouse.store.greptime.password", () -> "");
|
||||
}
|
||||
|
||||
@BeforeAll
|
||||
void setUpAll() {
|
||||
// Setup test alert definitions
|
||||
setupTestAlertDefines();
|
||||
Testcontainers.exposeHostPorts(port);
|
||||
vector = new GenericContainer<>(DockerImageName.parse(VECTOR_IMAGE))
|
||||
.withExposedPorts(VECTOR_PORT)
|
||||
.withCopyFileToContainer(MountableFile.forClasspathResource("vector.yml"), VECTOR_CONFIG_PATH)
|
||||
.withCommand("--config", "/etc/vector/vector.yml", "--verbose")
|
||||
.withLogConsumer(outputFrame -> log.info("Vector: {}", outputFrame.getUtf8String()))
|
||||
.withNetwork(Network.newNetwork())
|
||||
.withEnv(ENV_HERTZBEAT_PORT, String.valueOf(port))
|
||||
.waitingFor(Wait.forListeningPort())
|
||||
.withStartupTimeout(CONTAINER_STARTUP_TIMEOUT);
|
||||
vector.start();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPeriodicLogAlertWithIndividualAlert() {
|
||||
|
||||
List<SingleAlert> capturedAlerts = new ArrayList<>();
|
||||
|
||||
doAnswer(invocation -> {
|
||||
SingleAlert alert = invocation.getArgument(0);
|
||||
capturedAlerts.add(alert);
|
||||
return null;
|
||||
}).when(alarmCommonReduce).reduceAndSendAlarm(any(SingleAlert.class));
|
||||
|
||||
periodicAlertRuleScheduler.updateSchedule(errorCountAlertByIndividual);
|
||||
|
||||
// Wait for periodic individual alert to be generated through AlarmCommonReduce
|
||||
await().atMost(Duration.ofSeconds(60))
|
||||
.pollInterval(Duration.ofSeconds(3))
|
||||
.untilAsserted(() -> assertFalse(capturedAlerts.isEmpty(),
|
||||
"Should have generated at least one periodic individual alert"));
|
||||
|
||||
// Verify alert properties
|
||||
SingleAlert firstAlert = capturedAlerts.get(0);
|
||||
assertNotNull(firstAlert, "First periodic alert should not be null");
|
||||
assertEquals(CommonConstants.ALERT_STATUS_FIRING, firstAlert.getStatus(), "Alert should be in firing status");
|
||||
assertNotNull(firstAlert.getLabels(), "Alert should have labels");
|
||||
assertTrue(firstAlert.getLabels().containsKey(CommonConstants.LABEL_ALERT_SEVERITY), "Alert should have severity label");
|
||||
assertEquals(CommonConstants.ALERT_SEVERITY_CRITICAL, firstAlert.getLabels().get(CommonConstants.LABEL_ALERT_SEVERITY), "Alert should have critical severity");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPeriodicLogAlertWithGroupAlert() {
|
||||
|
||||
List<List<SingleAlert>> capturedGroupAlerts = new ArrayList<>();
|
||||
|
||||
doAnswer(invocation -> {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<SingleAlert> alerts = invocation.getArgument(1);
|
||||
capturedGroupAlerts.add(alerts);
|
||||
return null;
|
||||
}).when(alarmCommonReduce).reduceAndSendAlarmGroup(anyMap(), anyList());
|
||||
|
||||
periodicAlertRuleScheduler.updateSchedule(errorCountAlertByGroup);
|
||||
|
||||
await().atMost(Duration.ofSeconds(60))
|
||||
.pollInterval(Duration.ofSeconds(3))
|
||||
.untilAsserted(() -> assertFalse(capturedGroupAlerts.isEmpty(),
|
||||
"Should have generated periodic error count group alert"));
|
||||
|
||||
List<SingleAlert> groupAlerts = capturedGroupAlerts.get(0);
|
||||
|
||||
assertNotNull(groupAlerts, "Group alerts should not be null");
|
||||
assertFalse(groupAlerts.isEmpty(), "Group alerts should not be empty");
|
||||
|
||||
SingleAlert anyAlert = groupAlerts.get(0);
|
||||
assertEquals(CommonConstants.ALERT_STATUS_FIRING, anyAlert.getStatus(), "Alert should be in firing status");
|
||||
assertNotNull(anyAlert.getLabels(), "Alert should have labels");
|
||||
assertEquals(CommonConstants.ALERT_SEVERITY_CRITICAL, anyAlert.getLabels().get(CommonConstants.LABEL_ALERT_SEVERITY), "Alert should have critical severity");
|
||||
assertTrue(anyAlert.getTriggerTimes() >= 1, "Alert should indicate aggregated trigger times");
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup test alert definitions for periodic log processing
|
||||
*/
|
||||
private void setupTestAlertDefines() {
|
||||
// group
|
||||
errorCountAlertByGroup = AlertDefine.builder()
|
||||
.id(10L)
|
||||
.name("periodic_error_count_alert_group")
|
||||
.type(CommonConstants.LOG_ALERT_THRESHOLD_TYPE_PERIODIC)
|
||||
.expr("SELECT COUNT(*) as error_count FROM hertzbeat_logs WHERE time_unix_nano > NOW() - INTERVAL '10 minute'")
|
||||
.period(10) // Faster schedule for tests
|
||||
.template("High error count detected: {{ error_count }} errors in last period")
|
||||
.datasource("sql")
|
||||
.enable(true)
|
||||
.build();
|
||||
|
||||
Map<String, String> errorLabelsByGroup = new HashMap<>();
|
||||
errorLabelsByGroup.put(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_CRITICAL);
|
||||
errorLabelsByGroup.put(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_GROUP);
|
||||
errorLabelsByGroup.put("type", "error_count");
|
||||
errorCountAlertByGroup.setLabels(errorLabelsByGroup);
|
||||
|
||||
// individual
|
||||
errorCountAlertByIndividual = AlertDefine.builder()
|
||||
.id(11L)
|
||||
.name("periodic_error_count_alert_individual")
|
||||
.type(CommonConstants.LOG_ALERT_THRESHOLD_TYPE_PERIODIC)
|
||||
.expr("SELECT COUNT(*) as error_count, severity_text FROM hertzbeat_logs "
|
||||
+ "WHERE severity_text = 'ERROR' AND time_unix_nano > NOW() - INTERVAL '5 minute' GROUP BY severity_text HAVING COUNT(*) > 2")
|
||||
.period(10) // Faster schedule for tests
|
||||
.template("High error count detected: {{ error_count }} errors in last period")
|
||||
.datasource("sql")
|
||||
.enable(true)
|
||||
.build();
|
||||
|
||||
Map<String, String> errorLabelsByIndividual = new HashMap<>();
|
||||
errorLabelsByIndividual.put(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_CRITICAL);
|
||||
errorLabelsByIndividual.put(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_INDIVIDUAL);
|
||||
errorLabelsByIndividual.put("type", "error_count");
|
||||
errorCountAlertByIndividual.setLabels(errorLabelsByIndividual);
|
||||
}
|
||||
}
|
||||
-201
@@ -1,201 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.log.alert;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
|
||||
import org.apache.hertzbeat.common.cache.CacheFactory;
|
||||
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.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestInstance;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.mock.mockito.SpyBean;
|
||||
import org.springframework.boot.test.web.server.LocalServerPort;
|
||||
import org.testcontainers.containers.GenericContainer;
|
||||
import org.testcontainers.containers.Network;
|
||||
import org.testcontainers.containers.wait.strategy.Wait;
|
||||
import org.testcontainers.Testcontainers;
|
||||
import org.testcontainers.utility.DockerImageName;
|
||||
import org.testcontainers.utility.MountableFile;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
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.doAnswer;
|
||||
|
||||
/**
|
||||
* E2E tests for real-time log alert processing.
|
||||
*/
|
||||
@SpringBootTest(classes = org.apache.hertzbeat.manager.Manager.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@Slf4j
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
public class LogRealTimeAlertE2eTest {
|
||||
|
||||
private static final String VECTOR_IMAGE = "timberio/vector:latest-alpine";
|
||||
private static final int VECTOR_PORT = 8686;
|
||||
private static final String VECTOR_CONFIG_PATH = "/etc/vector/vector.yml";
|
||||
private static final String ENV_HERTZBEAT_PORT = "HERTZBEAT_PORT";
|
||||
private static final Duration CONTAINER_STARTUP_TIMEOUT = Duration.ofSeconds(120);
|
||||
|
||||
@LocalServerPort
|
||||
private int port;
|
||||
|
||||
private final List<SingleAlert> capturedAlerts = new ArrayList<>();
|
||||
private final ArrayList<List<SingleAlert>> capturedGroupAlerts = new ArrayList<>();
|
||||
|
||||
@SpyBean
|
||||
private AlarmCommonReduce alarmCommonReduce;
|
||||
|
||||
static GenericContainer<?> vector;
|
||||
|
||||
@BeforeAll
|
||||
void setUpAll() {
|
||||
// Setup test alert definitions
|
||||
setupTestAlertDefines();
|
||||
|
||||
// Expose host ports for testcontainers
|
||||
Testcontainers.exposeHostPorts(port);
|
||||
vector = new GenericContainer<>(DockerImageName.parse(VECTOR_IMAGE))
|
||||
.withExposedPorts(VECTOR_PORT)
|
||||
.withCopyFileToContainer(MountableFile.forClasspathResource("vector.yml"), VECTOR_CONFIG_PATH)
|
||||
.withCommand("--config", "/etc/vector/vector.yml", "--verbose")
|
||||
.withLogConsumer(outputFrame -> log.info("Vector: {}", outputFrame.getUtf8String()))
|
||||
.withNetwork(Network.newNetwork())
|
||||
.withEnv(ENV_HERTZBEAT_PORT, String.valueOf(port))
|
||||
.waitingFor(Wait.forListeningPort())
|
||||
.withStartupTimeout(CONTAINER_STARTUP_TIMEOUT);
|
||||
vector.start();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRealTimeLogAlertWithIndividualAlert() {
|
||||
doAnswer(invocation -> {
|
||||
SingleAlert alert = invocation.getArgument(0);
|
||||
capturedAlerts.add(alert);
|
||||
return null;
|
||||
}).when(alarmCommonReduce).reduceAndSendAlarm(any(SingleAlert.class));
|
||||
// Clear any existing captured alerts
|
||||
capturedAlerts.clear();
|
||||
|
||||
// Wait for real alert to be generated through AlarmCommonReduce
|
||||
await().atMost(Duration.ofSeconds(60))
|
||||
.pollInterval(Duration.ofSeconds(2))
|
||||
.untilAsserted(() -> assertFalse(capturedAlerts.isEmpty(),
|
||||
"Should have generated at least one alert for error logs"));
|
||||
|
||||
// Verify alert properties
|
||||
SingleAlert firstAlert = capturedAlerts.get(0);
|
||||
assertNotNull(firstAlert, "First alert should not be null");
|
||||
assertEquals(CommonConstants.ALERT_STATUS_FIRING, firstAlert.getStatus(), "Alert should be in firing status");
|
||||
assertNotNull(firstAlert.getLabels(), "Alert should have labels");
|
||||
assertTrue(firstAlert.getLabels().containsKey(CommonConstants.LABEL_ALERT_SEVERITY), "Alert should have severity label");
|
||||
assertEquals(CommonConstants.ALERT_SEVERITY_CRITICAL, firstAlert.getLabels().get(CommonConstants.LABEL_ALERT_SEVERITY), "Alert should have critical severity");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRealTimeLogAlertWithGroupAlert() {
|
||||
doAnswer(invocation -> {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<SingleAlert> alerts = invocation.getArgument(1);
|
||||
capturedGroupAlerts.add(alerts);
|
||||
return null;
|
||||
}).when(alarmCommonReduce).reduceAndSendAlarmGroup(anyMap(), anyList());
|
||||
|
||||
// Clear any existing captured alerts
|
||||
capturedGroupAlerts.clear();
|
||||
|
||||
// Wait for group alert to be generated via AlarmCommonReduce
|
||||
await().atMost(Duration.ofSeconds(60))
|
||||
.pollInterval(Duration.ofSeconds(3))
|
||||
.untilAsserted(() -> assertFalse(capturedGroupAlerts.isEmpty(),
|
||||
"Should have generated high frequency warning group alert"));
|
||||
|
||||
List<SingleAlert> groupAlerts = capturedGroupAlerts.get(0);
|
||||
assertNotNull(groupAlerts, "Group alerts should not be null");
|
||||
assertTrue(groupAlerts.size() >= 2, "Group should contain at least 5 alerts");
|
||||
|
||||
SingleAlert anyAlert = groupAlerts.get(0);
|
||||
assertEquals(CommonConstants.ALERT_STATUS_FIRING, anyAlert.getStatus(), "Alert should be in firing status");
|
||||
assertNotNull(anyAlert.getLabels(), "Alert should have labels");
|
||||
assertEquals(CommonConstants.ALERT_SEVERITY_WARNING, anyAlert.getLabels().get(CommonConstants.LABEL_ALERT_SEVERITY), "Alert should have warning severity");
|
||||
assertTrue(anyAlert.getTriggerTimes() >= 2, "Alert should indicate high frequency trigger");
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup test alert definitions for real-time log processing
|
||||
*/
|
||||
private void setupTestAlertDefines() {
|
||||
List<AlertDefine> alertDefines = new ArrayList<>();
|
||||
|
||||
// Create error log alert definition
|
||||
AlertDefine errorLogAlert = AlertDefine.builder()
|
||||
.id(1L)
|
||||
.name("error_log_alert")
|
||||
.type(CommonConstants.LOG_ALERT_THRESHOLD_TYPE_REALTIME)
|
||||
.expr("log.severityText == 'ERROR'")
|
||||
.period(10) // 10 seconds window
|
||||
.times(1) // Trigger on first occurrence
|
||||
.template("Error detected: {{ body }}")
|
||||
.enable(true)
|
||||
.build();
|
||||
|
||||
Map<String, String> errorLabels = new HashMap<>();
|
||||
errorLabels.put(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_CRITICAL);
|
||||
errorLabels.put(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_INDIVIDUAL);
|
||||
errorLogAlert.setLabels(errorLabels);
|
||||
|
||||
alertDefines.add(errorLogAlert);
|
||||
|
||||
// Create high frequency warning alert definition
|
||||
AlertDefine highFrequencyWarning = AlertDefine.builder()
|
||||
.id(2L)
|
||||
.name("high_frequency_warning")
|
||||
.type(CommonConstants.LOG_ALERT_THRESHOLD_TYPE_REALTIME)
|
||||
.expr("log.severityText == 'ERROR'")
|
||||
.period(10)
|
||||
.times(2)
|
||||
.template("High frequency warnings detected: {{ __rows__ }} warnings in 30 seconds")
|
||||
.enable(true)
|
||||
.build();
|
||||
|
||||
Map<String, String> warningLabels = new HashMap<>();
|
||||
warningLabels.put(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_WARNING);
|
||||
warningLabels.put(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_GROUP);
|
||||
highFrequencyWarning.setLabels(warningLabels);
|
||||
|
||||
alertDefines.add(highFrequencyWarning);
|
||||
|
||||
// Set alert definitions in cache
|
||||
CacheFactory.setLogAlertDefineCache(alertDefines);
|
||||
}
|
||||
|
||||
}
|
||||
-109
@@ -1,109 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.log.ingestion;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.common.entity.log.LogEntry;
|
||||
import org.apache.hertzbeat.common.queue.CommonDataQueue;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestInstance;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.web.server.LocalServerPort;
|
||||
import org.testcontainers.containers.GenericContainer;
|
||||
import org.testcontainers.containers.Network;
|
||||
import org.testcontainers.containers.wait.strategy.Wait;
|
||||
import org.testcontainers.Testcontainers;
|
||||
import org.testcontainers.utility.DockerImageName;
|
||||
import org.testcontainers.utility.MountableFile;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
/**
|
||||
* E2E tests for log ingestion.
|
||||
*/
|
||||
@SpringBootTest(classes = org.apache.hertzbeat.manager.Manager.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@Slf4j
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
public class LogIngestionE2eTest {
|
||||
|
||||
private static final String VECTOR_IMAGE = "timberio/vector:latest-alpine";
|
||||
private static final int VECTOR_PORT = 8686;
|
||||
private static final String VECTOR_CONFIG_PATH = "/etc/vector/vector.yml";
|
||||
private static final String ENV_HERTZBEAT_PORT = "HERTZBEAT_PORT";
|
||||
private static final Duration CONTAINER_STARTUP_TIMEOUT = Duration.ofSeconds(120);
|
||||
|
||||
@LocalServerPort
|
||||
private int port;
|
||||
|
||||
@Autowired
|
||||
private CommonDataQueue commonDataQueue;
|
||||
|
||||
static GenericContainer<?> vector;
|
||||
|
||||
@BeforeAll
|
||||
void setUpAll() {
|
||||
Testcontainers.exposeHostPorts(port);
|
||||
vector = new GenericContainer<>(DockerImageName.parse(VECTOR_IMAGE))
|
||||
.withExposedPorts(VECTOR_PORT)
|
||||
.withCopyFileToContainer(MountableFile.forClasspathResource("vector.yml"), VECTOR_CONFIG_PATH)
|
||||
.withCommand("--config", "/etc/vector/vector.yml", "--verbose")
|
||||
.withLogConsumer(outputFrame -> log.info("Vector: {}", outputFrame.getUtf8String()))
|
||||
.withNetwork(Network.newNetwork())
|
||||
.withEnv(ENV_HERTZBEAT_PORT, String.valueOf(port))
|
||||
.waitingFor(Wait.forListeningPort())
|
||||
.withStartupTimeout(CONTAINER_STARTUP_TIMEOUT);
|
||||
vector.start();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLogIngestion() {
|
||||
|
||||
// Start polling for log entries from the queue
|
||||
List<LogEntry> capturedLogs = new ArrayList<>();
|
||||
|
||||
// Wait for Vector to generate and send demo logs to HertzBeat
|
||||
await().atMost(Duration.ofSeconds(60))
|
||||
.pollInterval(Duration.ofSeconds(3))
|
||||
.untilAsserted(() -> {
|
||||
try {
|
||||
LogEntry logEntry = commonDataQueue.pollLogEntry();
|
||||
if (logEntry != null) {
|
||||
capturedLogs.add(logEntry);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new RuntimeException("Test interrupted", e);
|
||||
}
|
||||
});
|
||||
|
||||
// Verify the captured logs
|
||||
assertFalse(capturedLogs.isEmpty(), "No logs were captured from Vector");
|
||||
LogEntry firstLog = capturedLogs.get(0);
|
||||
assertNotNull(firstLog, "First log should not be null");
|
||||
assertNotNull(firstLog.getBody(), "Log body should not be null");
|
||||
assertNotNull(firstLog.getSeverityText(), "Severity text should not be null");
|
||||
}
|
||||
}
|
||||
-166
@@ -1,166 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.log.storage;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.common.entity.log.LogEntry;
|
||||
import org.apache.hertzbeat.common.queue.CommonDataQueue;
|
||||
import org.apache.hertzbeat.warehouse.store.history.tsdb.greptime.GreptimeDbDataStorage;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestInstance;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.web.server.LocalServerPort;
|
||||
import org.springframework.test.context.DynamicPropertyRegistry;
|
||||
import org.springframework.test.context.DynamicPropertySource;
|
||||
import org.testcontainers.containers.GenericContainer;
|
||||
import org.testcontainers.containers.Network;
|
||||
import org.testcontainers.containers.wait.strategy.Wait;
|
||||
import org.testcontainers.Testcontainers;
|
||||
import org.testcontainers.utility.DockerImageName;
|
||||
import org.testcontainers.utility.MountableFile;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
/**
|
||||
* E2E tests for GreptimeDB log storage.
|
||||
*/
|
||||
@SpringBootTest(classes = org.apache.hertzbeat.manager.Manager.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@Slf4j
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
public class GreptimeLogStorageE2eTest {
|
||||
|
||||
private static final String VECTOR_IMAGE = "timberio/vector:latest-alpine";
|
||||
private static final int VECTOR_PORT = 8686;
|
||||
private static final String VECTOR_CONFIG_PATH = "/etc/vector/vector.yml";
|
||||
private static final String ENV_HERTZBEAT_PORT = "HERTZBEAT_PORT";
|
||||
private static final String GREPTIME_IMAGE = "greptime/greptimedb:latest";
|
||||
private static final int GREPTIME_HTTP_PORT = 4000;
|
||||
private static final int GREPTIME_GRPC_PORT = 4001;
|
||||
private static final Duration CONTAINER_STARTUP_TIMEOUT = Duration.ofSeconds(120);
|
||||
|
||||
@LocalServerPort
|
||||
private int port;
|
||||
|
||||
@Autowired
|
||||
private CommonDataQueue commonDataQueue;
|
||||
|
||||
@Autowired
|
||||
private GreptimeDbDataStorage greptimeDbDataStorage;
|
||||
|
||||
static GenericContainer<?> vector;
|
||||
static GenericContainer<?> greptimedb;
|
||||
|
||||
static {
|
||||
greptimedb = new GenericContainer<>(DockerImageName.parse(GREPTIME_IMAGE))
|
||||
.withExposedPorts(GREPTIME_HTTP_PORT, GREPTIME_GRPC_PORT)
|
||||
.withCommand("standalone", "start",
|
||||
"--http-addr", "0.0.0.0:" + GREPTIME_HTTP_PORT,
|
||||
"--rpc-bind-addr", "0.0.0.0:" + GREPTIME_GRPC_PORT)
|
||||
.waitingFor(Wait.forListeningPorts(GREPTIME_HTTP_PORT, GREPTIME_GRPC_PORT))
|
||||
.withStartupTimeout(CONTAINER_STARTUP_TIMEOUT);
|
||||
greptimedb.start();
|
||||
}
|
||||
|
||||
@DynamicPropertySource
|
||||
static void greptimeProps(DynamicPropertyRegistry r) {
|
||||
r.add("warehouse.store.jpa.enabled", () -> "false");
|
||||
r.add("warehouse.store.greptime.enabled", () -> "true");
|
||||
r.add("warehouse.store.greptime.http-endpoint", () -> "http://localhost:" + greptimedb.getMappedPort(GREPTIME_HTTP_PORT));
|
||||
r.add("warehouse.store.greptime.grpc-endpoints", () -> "localhost:" + greptimedb.getMappedPort(GREPTIME_GRPC_PORT));
|
||||
r.add("warehouse.store.greptime.username", () -> "");
|
||||
r.add("warehouse.store.greptime.password", () -> "");
|
||||
}
|
||||
|
||||
|
||||
@BeforeAll
|
||||
void setUpAll() {
|
||||
// Expose host ports for testcontainers
|
||||
Testcontainers.exposeHostPorts(port);
|
||||
|
||||
vector = new GenericContainer<>(DockerImageName.parse(VECTOR_IMAGE))
|
||||
.withExposedPorts(VECTOR_PORT)
|
||||
.withCopyFileToContainer(MountableFile.forClasspathResource("vector.yml"), VECTOR_CONFIG_PATH)
|
||||
.withCommand("--config", "/etc/vector/vector.yml", "--verbose")
|
||||
.withLogConsumer(outputFrame -> log.info("Vector: {}", outputFrame.getUtf8String()))
|
||||
.withNetwork(Network.newNetwork())
|
||||
.withEnv(ENV_HERTZBEAT_PORT, String.valueOf(port))
|
||||
.waitingFor(Wait.forListeningPort())
|
||||
.withStartupTimeout(CONTAINER_STARTUP_TIMEOUT);
|
||||
vector.start();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLogStorageToGreptimeDb() {
|
||||
|
||||
List<LogEntry> capturedLogs = new ArrayList<>();
|
||||
|
||||
// Wait for Vector to generate and send logs to HertzBeat
|
||||
await().atMost(Duration.ofSeconds(30))
|
||||
.pollInterval(Duration.ofSeconds(3))
|
||||
.untilAsserted(() -> {
|
||||
// Poll log entries from the queue (non-blocking)
|
||||
try {
|
||||
LogEntry logEntry = commonDataQueue.pollLogEntry();
|
||||
if (logEntry != null) {
|
||||
capturedLogs.add(logEntry);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new RuntimeException("Test interrupted", e);
|
||||
}
|
||||
|
||||
// Assert that we have captured at least some logs
|
||||
assertFalse(capturedLogs.isEmpty(), "Should have captured at least one log entry");
|
||||
});
|
||||
|
||||
// Verify the captured logs
|
||||
assertFalse(capturedLogs.isEmpty(), "No logs were captured from Vector");
|
||||
LogEntry firstLog = capturedLogs.get(0);
|
||||
assertNotNull(firstLog, "First log should not be null");
|
||||
assertNotNull(firstLog.getBody(), "Log body should not be null");
|
||||
assertNotNull(firstLog.getSeverityText(), "Severity text should not be null");
|
||||
|
||||
// Additional wait to ensure logs are persisted to GreptimeDB
|
||||
await().atMost(Duration.ofSeconds(30))
|
||||
.pollInterval(Duration.ofSeconds(2))
|
||||
.untilAsserted(() -> {
|
||||
// Query GreptimeDB directly to verify data persistence
|
||||
List<LogEntry> storedLogs = queryStoredLogs();
|
||||
assertFalse(storedLogs.isEmpty(), "Should have logs stored in GreptimeDB");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to query stored logs directly from GreptimeDB
|
||||
*/
|
||||
private List<LogEntry> queryStoredLogs() {
|
||||
long endTime = System.currentTimeMillis();
|
||||
long startTime = endTime - Duration.ofMinutes(5).toMillis(); // Look back 5 minutes
|
||||
|
||||
return greptimeDbDataStorage.queryLogsByMultipleConditions(
|
||||
startTime, endTime, null, null, null, null);
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
# contributor license agreements. See the NOTICE file distributed with
|
||||
# this work for additional information regarding copyright ownership.
|
||||
# The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
# (the "License"); you may not use this file except in compliance with
|
||||
# the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# Vector configuration for HertzBeat E2E testing
|
||||
|
||||
api:
|
||||
enabled: true
|
||||
address: "0.0.0.0:8686"
|
||||
|
||||
sources:
|
||||
generate_syslog:
|
||||
type: "demo_logs"
|
||||
format: "syslog"
|
||||
count: 100000
|
||||
interval: 1
|
||||
|
||||
transforms:
|
||||
remap_syslog:
|
||||
inputs: ["generate_syslog"]
|
||||
type: "remap"
|
||||
source: |
|
||||
syslog = parse_syslog!(.message)
|
||||
|
||||
severity_text = if includes(["emerg", "err", "crit", "alert"], syslog.severity) {
|
||||
"ERROR"
|
||||
} else if syslog.severity == "warning" {
|
||||
"WARN"
|
||||
} else if syslog.severity == "debug" {
|
||||
"DEBUG"
|
||||
} else if includes(["info", "notice"], syslog.severity) {
|
||||
"INFO"
|
||||
} else {
|
||||
syslog.severity
|
||||
}
|
||||
|
||||
severity_number = if includes(["emerg", "err", "crit", "alert"], syslog.severity) {
|
||||
17
|
||||
} else if syslog.severity == "warning" {
|
||||
13
|
||||
} else if syslog.severity == "debug" {
|
||||
5
|
||||
} else if includes(["info", "notice"], syslog.severity) {
|
||||
9
|
||||
} else {
|
||||
9
|
||||
}
|
||||
|
||||
.resourceLogs = [{
|
||||
"resource": {
|
||||
"attributes": [
|
||||
{ "key": "source_type", "value": { "stringValue": .source_type } },
|
||||
{ "key": "service.name", "value": { "stringValue": syslog.appname } },
|
||||
{ "key": "host.hostname", "value": { "stringValue": syslog.hostname } }
|
||||
]
|
||||
},
|
||||
"scopeLogs": [{
|
||||
"scope": {
|
||||
"name": syslog.msgid
|
||||
},
|
||||
"logRecords": [{
|
||||
"timeUnixNano": to_unix_timestamp!(syslog.timestamp, unit: "nanoseconds"),
|
||||
"body": { "stringValue": syslog.message },
|
||||
"severityText": severity_text,
|
||||
"severityNumber": severity_number,
|
||||
"attributes": [
|
||||
{ "key": "syslog.procid", "value": { "stringValue": to_string(syslog.procid) } },
|
||||
{ "key": "syslog.facility", "value": { "stringValue": syslog.facility } },
|
||||
{ "key": "syslog.version", "value": { "stringValue": to_string(syslog.version) } }
|
||||
]
|
||||
}]
|
||||
}]
|
||||
}]
|
||||
|
||||
del(.message)
|
||||
del(.timestamp)
|
||||
del(.service)
|
||||
del(.source_type)
|
||||
|
||||
sinks:
|
||||
# Debug sink to log all processed data
|
||||
debug_console:
|
||||
inputs: [ "remap_syslog" ]
|
||||
type: console
|
||||
encoding:
|
||||
codec: json
|
||||
|
||||
# Send to HertzBeat
|
||||
emit_syslog:
|
||||
inputs: [ "remap_syslog" ]
|
||||
type: opentelemetry
|
||||
protocol:
|
||||
type: http
|
||||
uri: "http://host.testcontainers.internal:${HERTZBEAT_PORT:-1157}/api/logs/ingest/otlp"
|
||||
method: post
|
||||
encoding:
|
||||
codec: json
|
||||
framing:
|
||||
method: newline_delimited
|
||||
headers:
|
||||
content-type: application/json
|
||||
auth:
|
||||
strategy: basic
|
||||
user: admin
|
||||
password: hertzbeat
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
<module>hertzbeat-collector-common-e2e</module>
|
||||
<module>hertzbeat-collector-kafka-e2e</module>
|
||||
<module>hertzbeat-collector-basic-e2e</module>
|
||||
<module>hertzbeat-log-e2e</module>
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
~ contributor license agreements. See the NOTICE file distributed with
|
||||
~ this work for additional information regarding copyright ownership.
|
||||
~ The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
~ (the "License"); you may not use this file except in compliance with
|
||||
~ the License. You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>org.apache.hertzbeat</groupId>
|
||||
<artifactId>hertzbeat</artifactId>
|
||||
<version>2.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>hertzbeat-log</artifactId>
|
||||
<name>${project.artifactId}</name>
|
||||
|
||||
<dependencies>
|
||||
<!-- common -->
|
||||
<dependency>
|
||||
<groupId>org.apache.hertzbeat</groupId>
|
||||
<artifactId>hertzbeat-base</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<!-- warehouse -->
|
||||
<dependency>
|
||||
<groupId>org.apache.hertzbeat</groupId>
|
||||
<artifactId>hertzbeat-warehouse</artifactId>
|
||||
</dependency>
|
||||
<!-- spring -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<!-- swagger -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<!-- opentelemetry proto -->
|
||||
<dependency>
|
||||
<groupId>io.opentelemetry.proto</groupId>
|
||||
<artifactId>opentelemetry-proto</artifactId>
|
||||
</dependency>
|
||||
<!-- protobuf dependencies for OTLP processing -->
|
||||
<dependency>
|
||||
<groupId>com.google.protobuf</groupId>
|
||||
<artifactId>protobuf-java</artifactId>
|
||||
</dependency>
|
||||
<!-- protobuf java util -->
|
||||
<dependency>
|
||||
<groupId>com.google.protobuf</groupId>
|
||||
<artifactId>protobuf-java-util</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
-109
@@ -1,109 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.log.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||
import org.apache.hertzbeat.common.entity.dto.Message;
|
||||
import org.apache.hertzbeat.log.service.LogProtocolAdapter;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* Log Ingestion Controller
|
||||
*/
|
||||
@Tag(name = "Log Ingestion Controller")
|
||||
@RestController
|
||||
@RequestMapping(path = "/api/logs", produces = "application/json")
|
||||
@Slf4j
|
||||
public class LogIngestionController {
|
||||
|
||||
private static final String DEFAULT_PROTOCOL = "otlp";
|
||||
private final List<LogProtocolAdapter> protocolAdapters;
|
||||
|
||||
public LogIngestionController(List<LogProtocolAdapter> protocolAdapters) {
|
||||
this.protocolAdapters = protocolAdapters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive log payload pushed from external system specifying the log protocol.
|
||||
* Examples:
|
||||
* - POST /api/logs/ingest/otlp (content body is OTLP JSON)
|
||||
*
|
||||
* @param protocol log protocol identifier
|
||||
* @param content raw request body
|
||||
*/
|
||||
@PostMapping("/ingest/{protocol}")
|
||||
public ResponseEntity<Message<Void>> ingestExternLog(@PathVariable("protocol") String protocol,
|
||||
@RequestBody String content) {
|
||||
log.info("Receive extern log from protocol: {}, content length: {}", protocol, content == null ? 0 : content.length());
|
||||
if (!StringUtils.hasText(protocol)) {
|
||||
protocol = DEFAULT_PROTOCOL; // Default to OTLP if no protocol specified
|
||||
}
|
||||
for (LogProtocolAdapter adapter : protocolAdapters) {
|
||||
if (adapter.supportProtocol().equalsIgnoreCase(protocol)) {
|
||||
try {
|
||||
adapter.ingest(content);
|
||||
return ResponseEntity.ok(Message.success("Add extern log success"));
|
||||
} catch (Exception e) {
|
||||
log.error("Add extern log failed: {}", e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(Message.fail(CommonConstants.FAIL_CODE, "Add extern log failed: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
log.warn("Not support extern log from protocol: {}", protocol);
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(Message.fail(CommonConstants.FAIL_CODE, "Not support the " + protocol + " protocol log"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive default log payload (when protocol is not specified).
|
||||
* It will look for a service whose supportProtocol() returns "otlp".
|
||||
*/
|
||||
@PostMapping("/ingest")
|
||||
public ResponseEntity<Message<Void>> ingestDefaultExternLog(@RequestBody String content) {
|
||||
log.info("Receive default extern log content, length: {}", content == null ? 0 : content.length());
|
||||
LogProtocolAdapter adapter = protocolAdapters.stream()
|
||||
.filter(item -> DEFAULT_PROTOCOL.equalsIgnoreCase(item.supportProtocol()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (adapter != null) {
|
||||
try {
|
||||
adapter.ingest(content);
|
||||
return ResponseEntity.ok(Message.success("Add extern log success"));
|
||||
} catch (Exception e) {
|
||||
log.error("Add extern log failed: {}", e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(Message.fail(CommonConstants.FAIL_CODE, "Add extern log failed: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
log.error("Not support default extern log protocol");
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body(Message.fail(CommonConstants.FAIL_CODE, "Not support the default protocol log"));
|
||||
}
|
||||
}
|
||||
-64
@@ -1,64 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.log.controller;
|
||||
|
||||
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.common.entity.dto.Message;
|
||||
import org.apache.hertzbeat.warehouse.store.history.tsdb.HistoryDataWriter;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.apache.hertzbeat.common.constants.CommonConstants.FAIL_CODE;
|
||||
|
||||
/**
|
||||
* Controller for managing log entries in HertzBeat.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(path = "/api/logs", produces = "application/json")
|
||||
@Tag(name = "Log Management Controller")
|
||||
@Slf4j
|
||||
public class LogManagerController {
|
||||
|
||||
private final HistoryDataWriter historyDataWriter;
|
||||
|
||||
public LogManagerController(HistoryDataWriter historyDataWriter) {
|
||||
this.historyDataWriter = historyDataWriter;
|
||||
}
|
||||
|
||||
@DeleteMapping
|
||||
@Operation(summary = "Batch delete logs",
|
||||
description = "Batch delete logs by time timestamps. Deletes multiple log entries based on their Unix nanosecond timestamps.")
|
||||
public ResponseEntity<Message<String>> batchDelete(
|
||||
@Parameter(description = "List of Unix nanosecond timestamps for logs to delete", example = "1640995200000000000")
|
||||
@RequestParam(required = false) List<Long> timeUnixNanos) {
|
||||
boolean result = historyDataWriter.batchDeleteLogs(timeUnixNanos);
|
||||
if (result) {
|
||||
return ResponseEntity.ok(Message.success("Logs deleted successfully"));
|
||||
} else {
|
||||
return ResponseEntity.ok(Message.fail(FAIL_CODE, "Failed to delete logs"));
|
||||
}
|
||||
}
|
||||
}
|
||||
-226
@@ -1,226 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.log.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.hertzbeat.warehouse.store.history.tsdb.HistoryDataReader;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.common.entity.dto.Message;
|
||||
import org.apache.hertzbeat.common.entity.log.LogEntry;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* Log query and statistics APIs for UI consumption
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(path = "/api/logs", produces = "application/json")
|
||||
@Tag(name = "Log Query Controller")
|
||||
@Slf4j
|
||||
public class LogQueryController {
|
||||
|
||||
private final HistoryDataReader historyDataReader;
|
||||
|
||||
public LogQueryController(HistoryDataReader historyDataReader) {
|
||||
this.historyDataReader = historyDataReader;
|
||||
}
|
||||
|
||||
@GetMapping("/list")
|
||||
@Operation(summary = "Query logs by time range with optional filters",
|
||||
description = "Query logs by [start,end] in ms and optional filters with pagination. Returns paginated log entries sorted by timestamp in descending order.")
|
||||
public ResponseEntity<Message<Page<LogEntry>>> list(
|
||||
@Parameter(description = "Start timestamp in milliseconds (Unix timestamp)", example = "1640995200000")
|
||||
@RequestParam(value = "start", required = false) Long start,
|
||||
@Parameter(description = "End timestamp in milliseconds (Unix timestamp)", example = "1641081600000")
|
||||
@RequestParam(value = "end", required = false) Long end,
|
||||
@Parameter(description = "Trace ID for distributed tracing", example = "1234567890abcdef")
|
||||
@RequestParam(value = "traceId", required = false) String traceId,
|
||||
@Parameter(description = "Span ID for distributed tracing", example = "abcdef1234567890")
|
||||
@RequestParam(value = "spanId", required = false) String spanId,
|
||||
@Parameter(description = "Log severity number (1-24 according to OpenTelemetry standard)", example = "9")
|
||||
@RequestParam(value = "severityNumber", required = false) Integer severityNumber,
|
||||
@Parameter(description = "Log severity text (TRACE, DEBUG, INFO, WARN, ERROR, FATAL)", example = "INFO")
|
||||
@RequestParam(value = "severityText", required = false) String severityText,
|
||||
@Parameter(description = "Page index starting from 0", example = "0")
|
||||
@RequestParam(value = "pageIndex", required = false, defaultValue = "0") Integer pageIndex,
|
||||
@Parameter(description = "Number of items per page", example = "20")
|
||||
@RequestParam(value = "pageSize", required = false, defaultValue = "20") Integer pageSize) {
|
||||
Page<LogEntry> result = getPagedLogs(start, end, traceId, spanId, severityNumber, severityText, pageIndex, pageSize);
|
||||
return ResponseEntity.ok(Message.success(result));
|
||||
}
|
||||
|
||||
@GetMapping("/stats/overview")
|
||||
@Operation(summary = "Log overview statistics",
|
||||
description = "Overall counts and basic statistics with filters. Provides counts by severity levels according to OpenTelemetry standard.")
|
||||
public ResponseEntity<Message<Map<String, Object>>> overviewStats(
|
||||
@Parameter(description = "Start timestamp in milliseconds (Unix timestamp)", example = "1640995200000")
|
||||
@RequestParam(value = "start", required = false) Long start,
|
||||
@Parameter(description = "End timestamp in milliseconds (Unix timestamp)", example = "1641081600000")
|
||||
@RequestParam(value = "end", required = false) Long end,
|
||||
@Parameter(description = "Trace ID for distributed tracing", example = "1234567890abcdef")
|
||||
@RequestParam(value = "traceId", required = false) String traceId,
|
||||
@Parameter(description = "Span ID for distributed tracing", example = "abcdef1234567890")
|
||||
@RequestParam(value = "spanId", required = false) String spanId,
|
||||
@Parameter(description = "Log severity number (1-24 according to OpenTelemetry standard)", example = "9")
|
||||
@RequestParam(value = "severityNumber", required = false) Integer severityNumber,
|
||||
@Parameter(description = "Log severity text (TRACE, DEBUG, INFO, WARN, ERROR, FATAL)", example = "INFO")
|
||||
@RequestParam(value = "severityText", required = false) String severityText) {
|
||||
List<LogEntry> logs = getFilteredLogs(start, end, traceId, spanId, severityNumber, severityText);
|
||||
|
||||
Map<String, Object> overview = new HashMap<>();
|
||||
overview.put("totalCount", logs.size());
|
||||
|
||||
// Count by severity levels according to OpenTelemetry standard
|
||||
// TRACE: 1-4, DEBUG: 5-8, INFO: 9-12, WARN: 13-16, ERROR: 17-20, FATAL: 21-24
|
||||
long fatalCount = logs.stream().filter(log -> log.getSeverityNumber() != null && log.getSeverityNumber() >= 21 && log.getSeverityNumber() <= 24).count();
|
||||
long errorCount = logs.stream().filter(log -> log.getSeverityNumber() != null && log.getSeverityNumber() >= 17 && log.getSeverityNumber() <= 20).count();
|
||||
long warnCount = logs.stream().filter(log -> log.getSeverityNumber() != null && log.getSeverityNumber() >= 13 && log.getSeverityNumber() <= 16).count();
|
||||
long infoCount = logs.stream().filter(log -> log.getSeverityNumber() != null && log.getSeverityNumber() >= 9 && log.getSeverityNumber() <= 12).count();
|
||||
long debugCount = logs.stream().filter(log -> log.getSeverityNumber() != null && log.getSeverityNumber() >= 5 && log.getSeverityNumber() <= 8).count();
|
||||
long traceCount = logs.stream().filter(log -> log.getSeverityNumber() != null && log.getSeverityNumber() >= 1 && log.getSeverityNumber() <= 4).count();
|
||||
|
||||
overview.put("fatalCount", fatalCount);
|
||||
overview.put("errorCount", errorCount);
|
||||
overview.put("warnCount", warnCount);
|
||||
overview.put("infoCount", infoCount);
|
||||
overview.put("debugCount", debugCount);
|
||||
overview.put("traceCount", traceCount);
|
||||
|
||||
return ResponseEntity.ok(Message.success(overview));
|
||||
}
|
||||
|
||||
@GetMapping("/stats/trace-coverage")
|
||||
@Operation(summary = "Trace coverage statistics",
|
||||
description = "Statistics about trace information availability. Shows how many logs have trace IDs, span IDs, or both for distributed tracing analysis.")
|
||||
public ResponseEntity<Message<Map<String, Object>>> traceCoverageStats(
|
||||
@Parameter(description = "Start timestamp in milliseconds (Unix timestamp)", example = "1640995200000")
|
||||
@RequestParam(value = "start", required = false) Long start,
|
||||
@Parameter(description = "End timestamp in milliseconds (Unix timestamp)", example = "1641081600000")
|
||||
@RequestParam(value = "end", required = false) Long end,
|
||||
@Parameter(description = "Trace ID for distributed tracing", example = "1234567890abcdef")
|
||||
@RequestParam(value = "traceId", required = false) String traceId,
|
||||
@Parameter(description = "Span ID for distributed tracing", example = "abcdef1234567890")
|
||||
@RequestParam(value = "spanId", required = false) String spanId,
|
||||
@Parameter(description = "Log severity number (1-24 according to OpenTelemetry standard)", example = "9")
|
||||
@RequestParam(value = "severityNumber", required = false) Integer severityNumber,
|
||||
@Parameter(description = "Log severity text (TRACE, DEBUG, INFO, WARN, ERROR, FATAL)", example = "INFO")
|
||||
@RequestParam(value = "severityText", required = false) String severityText) {
|
||||
List<LogEntry> logs = getFilteredLogs(start, end, traceId, spanId, severityNumber, severityText);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
// Trace coverage statistics
|
||||
long withTraceId = logs.stream().filter(log -> log.getTraceId() != null && !log.getTraceId().isEmpty()).count();
|
||||
long withSpanId = logs.stream().filter(log -> log.getSpanId() != null && !log.getSpanId().isEmpty()).count();
|
||||
long withBothTraceAndSpan = logs.stream().filter(log ->
|
||||
log.getTraceId() != null && !log.getTraceId().isEmpty()
|
||||
&& log.getSpanId() != null && !log.getSpanId().isEmpty()).count();
|
||||
long withoutTrace = logs.size() - withTraceId;
|
||||
|
||||
Map<String, Long> traceCoverage = new HashMap<>();
|
||||
traceCoverage.put("withTrace", withTraceId);
|
||||
traceCoverage.put("withoutTrace", withoutTrace);
|
||||
traceCoverage.put("withSpan", withSpanId);
|
||||
traceCoverage.put("withBothTraceAndSpan", withBothTraceAndSpan);
|
||||
|
||||
result.put("traceCoverage", traceCoverage);
|
||||
return ResponseEntity.ok(Message.success(result));
|
||||
}
|
||||
|
||||
@GetMapping("/stats/trend")
|
||||
@Operation(summary = "Log trend over time",
|
||||
description = "Count logs by hour intervals with filters. Groups logs by hour and provides time-series data for trend analysis.")
|
||||
public ResponseEntity<Message<Map<String, Object>>> trendStats(
|
||||
@Parameter(description = "Start timestamp in milliseconds (Unix timestamp)", example = "1640995200000")
|
||||
@RequestParam(value = "start", required = false) Long start,
|
||||
@Parameter(description = "End timestamp in milliseconds (Unix timestamp)", example = "1641081600000")
|
||||
@RequestParam(value = "end", required = false) Long end,
|
||||
@Parameter(description = "Trace ID for distributed tracing", example = "1234567890abcdef")
|
||||
@RequestParam(value = "traceId", required = false) String traceId,
|
||||
@Parameter(description = "Span ID for distributed tracing", example = "abcdef1234567890")
|
||||
@RequestParam(value = "spanId", required = false) String spanId,
|
||||
@Parameter(description = "Log severity number (1-24 according to OpenTelemetry standard)", example = "9")
|
||||
@RequestParam(value = "severityNumber", required = false) Integer severityNumber,
|
||||
@Parameter(description = "Log severity text (TRACE, DEBUG, INFO, WARN, ERROR, FATAL)", example = "INFO")
|
||||
@RequestParam(value = "severityText", required = false) String severityText) {
|
||||
List<LogEntry> logs = getFilteredLogs(start, end, traceId, spanId, severityNumber, severityText);
|
||||
|
||||
// Group by hour
|
||||
Map<String, Long> hourlyStats = logs.stream()
|
||||
.filter(log -> log.getTimeUnixNano() != null)
|
||||
.collect(Collectors.groupingBy(
|
||||
log -> {
|
||||
long timestampMs = log.getTimeUnixNano() / 1_000_000L;
|
||||
LocalDateTime dateTime = LocalDateTime.ofInstant(
|
||||
Instant.ofEpochMilli(timestampMs),
|
||||
ZoneId.systemDefault());
|
||||
return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:00"));
|
||||
},
|
||||
Collectors.counting()));
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("hourlyStats", hourlyStats);
|
||||
return ResponseEntity.ok(Message.success(result));
|
||||
}
|
||||
|
||||
private List<LogEntry> getFilteredLogs(Long start, Long end, String traceId, String spanId,
|
||||
Integer severityNumber, String severityText) {
|
||||
// Use the new multi-condition query method
|
||||
return historyDataReader.queryLogsByMultipleConditions(start, end, traceId, spanId, severityNumber, severityText);
|
||||
}
|
||||
|
||||
private Page<LogEntry> getPagedLogs(Long start, Long end, String traceId, String spanId,
|
||||
Integer severityNumber, String severityText, Integer pageIndex, Integer pageSize) {
|
||||
// Calculate pagination parameters
|
||||
int offset = pageIndex * pageSize;
|
||||
|
||||
// Get total count and paginated data
|
||||
long totalElements = historyDataReader.countLogsByMultipleConditions(start, end, traceId, spanId, severityNumber, severityText);
|
||||
List<LogEntry> pagedLogs = historyDataReader.queryLogsByMultipleConditionsWithPagination(
|
||||
start, end, traceId, spanId, severityNumber, severityText, offset, pageSize);
|
||||
|
||||
// Create PageRequest (sorted by timestamp descending)
|
||||
Sort sort = Sort.by(Sort.Direction.DESC, "timeUnixNano");
|
||||
PageRequest pageRequest = PageRequest.of(pageIndex, pageSize, sort);
|
||||
|
||||
// Return Spring Data Page object
|
||||
return new PageImpl<>(pagedLogs, pageRequest, totalElements);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user