Compare commits

..
Author SHA1 Message Date
tomsun28 d2bfa4fc5d [docs] update doc and add contributors
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-09-23 23:55:22 +08:00
tomsun28 bc865d971e [docs] update doc and add contributors
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-09-23 23:54:01 +08:00
tomsun28 070ab41966 [docs] update doc and add contributors
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-09-23 23:51:53 +08:00
580 changed files with 31145 additions and 61362 deletions
+9 -75
View File
@@ -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,
+6 -6
View File
@@ -16,15 +16,11 @@
#
github:
description: An AI-powered next-generation open source real-time observability system.
description: Real-time observability system with agentless, performance cluster, prometheus-compatible, custom monitoring and status page building capabilities.
homepage: https://hertzbeat.apache.org/
labels:
- ai
- agent
- llm
- logs
- monitor
- monitoring
- monitor
- notifications
- alerting
- self-hosted
@@ -34,11 +30,15 @@ github:
- metrics
- observability
- uptime
- uptime-monitoring
- status
- status-page
- devops
- server
- linux
- database
- mysql
- cloud
enabled_merge_buttons:
squash: true
merge: false
+1
View File
@@ -15,6 +15,7 @@
"customizations": {
"vscode": {
"settings": {},
// same extensions as Gitpod, should match /.gitpod.yml
"extensions": [
"vscjava.vscode-java-pack",
"editorconfig.editorconfig",
+10 -10
View File
@@ -90,24 +90,24 @@ e2e:
backend:
- changed-files:
- any-glob-to-any-file:
- "hertzbeat-alerter/**/*"
- "hertzbeat-collector/**/*"
- "hertzbeat-common/**/*"
- "hertzbeat-grafana/**/*"
- "hertzbeat-manager/**/*"
- "hertzbeat-push/**/*"
- "hertzbeat-remoting/**/*"
- "hertzbeat-warehouse/**/*"
- "alerter/**/*"
- "collector/**/*"
- "common/**/*"
- "grafana/**/*"
- "manager/**/*"
- "push/**/*"
- "remoting/**/*"
- "warehouse/**/*"
collector:
- changed-files:
- any-glob-to-any-file:
- "hertzbeat-collector/**/*"
- "collector/**/*"
monitoring-template:
- changed-files:
- any-glob-to-any-file:
- "hertzbeat-manager/src/main/resources/define/**/*"
- "manager/src/main/resources/define/**/*"
HIP:
- changed-files:
+1 -1
View File
@@ -39,7 +39,7 @@ jobs:
python-version: '3.8'
- name: Check Markdown
uses: DavidAnson/markdownlint-cli2-action@992badcdf24e3b8eb7e87ff9287fe931bcb00c6e
uses: DavidAnson/markdownlint-cli2-action@v16
with:
globs: './home/**/*.md'
+3 -6
View File
@@ -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
+2 -5
View File
@@ -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: |
+3 -3
View File
@@ -4,7 +4,7 @@ image:
tasks:
- name: Run backend
before: cd hertzbeat-startup
before: cd hertzbeat-manager
command: |
gp sync-await setup-backend
mvn spring-boot:run
@@ -13,7 +13,7 @@ tasks:
before: cd web-app
command: |
gp sync-await setup-frontend
pnpm start --public-host "`gp url 4200`"
yarn start --public-host "`gp url 4200`"
openMode: split-right
- name: Setup backend
@@ -25,7 +25,7 @@ tasks:
- name: Setup frontend
init: |
cd web-app && pnpm install
cd web-app && yarn install
command: |
gp sync-done setup-frontend
exit 0
-1
View File
@@ -66,7 +66,6 @@ header:
- '.gitpod.yml'
- 'karma.conf.js'
- 'proxy.conf.js'
- 'pnpm-lock.yaml'
- '.helmignore'
- 'script/helm/**'
- 'web-app/src/app/core/**'
+44 -18
View File
@@ -20,41 +20,43 @@
- 💬**Suggestion**: You can also make some suggestions to the project or community to promote the healthy development of the community;
More see [Contribution Types](https://allcontributors.org/emoji-key)
More see [Contribution Types](https://allcontributors.org/docs/en/emoji-key)
Even small corrections to typos are very welcome :)
### Getting HertzBeat up and running
> To get HertzBeat code running on your development tools, and able to debug with breakpoints.
> This is a front-end and back-end separation project. To start the local code, the back-end [startup](hertzbeat-startup) and the front-end [web-app](web-app) must be started separately.
> This is a front-end and back-end separation project. To start the local code, the back-end [manager](manager) and the front-end [web-app](web-app) must be started separately.
#### Backend start
1. Requires `maven3+`, `java17` and `lombok` environments
2. (Optional) Modify the configuration file: `hertzbeat-startup/src/main/resources/application.yml`
2. (Optional) Modify the configuration file: `manager/src/main/resources/application.yml`
3. Execute under the project root directory: `mvn clean install -DskipTests`
4. Add VM Options: `--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED`
5. Start `springboot startup` service: `hertzbeat-startup/src/main/java/org/apache/hertzbeat/startup/HertzBeatApplication.java`
5. Start `springboot manager` service: `manager/src/main/java/org/apache/hertzbeat/hertzbeat-manager/Manager.java`
#### Frontend start
1. Need `Node 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
@@ -131,6 +133,17 @@ Add WeChat account `ahertzbeat` to pull you into the WeChat group.
## 🥐 Architecture
- **[manager](https://github.com/apache/hertzbeat/tree/master/hertzbeat-manager)** Provide monitoring management, system management basic services.
> Provides monitoring management, monitoring configuration management, system user management, etc.
- **[collector](https://github.com/apache/hertzbeat/tree/master/collector)** Provide metrics data collection services.
> Use common protocols to remotely collect and obtain peer-to-peer metrics data.
- **[warehouse](https://github.com/apache/hertzbeat/tree/master/warehouse)** Provide monitoring data warehousing services.
> Metrics data management, data query, calculation and statistics.
- **[alerter](https://github.com/apache/hertzbeat/tree/master/hertzbeat-alerter)** Provide alert service.
> Alarm calculation trigger, monitoring status linkage, alarm configuration, and alarm notification.
- **[web-app](https://github.com/apache/hertzbeat/tree/master/web-app)** Provide web ui.
> Angular Web UI.
![hertzBeat](home/static/img/docs/hertzbeat-arch.png)
<br>
@@ -161,41 +174,43 @@ Add WeChat account `ahertzbeat` to pull you into the WeChat group.
- 💬**建议**:也可以对项目或者社区提出一些建议,促进社区的良性发展;
更多贡献方式参见 [Contribution Types](https://allcontributors.org/emoji-key)
更多贡献方式参见 [Contribution Types](https://allcontributors.org/docs/en/emoji-key)
即便是小到错别字的修正我们也都非常欢迎 :)
### 让 HertzBeat 运行起来
> 让 HertzBeat 的代码在您的开发工具上运行起来,并且能够断点调试。
> 此为前后端分离项目,本地代码启动需将后端[hertzbeat-startup](hertzbeat-startup)和前端[web-app](web-app)分别启动生效。
> 此为前后端分离项目,本地代码启动需将后端[manager](manager)和前端[web-app](web-app)分别启动生效。
#### 后端启动
1. 需要 `maven3+`, `java17` 和 `lombok` 环境
2. (可选)修改配置文件配置信息-`hertzbeat-startup/src/main/resources/application.yml`
2. (可选)修改配置文件配置信息-`manager/src/main/resources/application.yml`
3. 在项目根目录运行编译: `mvn clean install -DskipTests`
4. 在 `jvm` 加入参数 `--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED`
5. 启动`springboot startup`服务 `hertzbeat-startup/src/main/java/org/apache/hertzbeat/startup/HertzBeatApplication.java`
5. 启动`springboot manager`服务 `manager/src/main/java/org/apache/hertzbeat/hertzbeat-manager/Manager.java`
#### 前端启动
1. 需要 `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*
### 寻找任务
@@ -271,4 +286,15 @@ Add WeChat account `ahertzbeat` to pull you into the WeChat group.
### 模块
- **[manager](https://github.com/apache/hertzbeat/tree/master/hertzbeat-manager)** 提供监控管理,系统管理基础服务
> 提供对监控的管理,监控应用配置的管理,系统用户租户后台管理等。
- **[collector](https://github.com/apache/hertzbeat/tree/master/collector)** 提供监控数据采集服务
> 使用通用协议远程采集获取对端指标数据。
- **[warehouse](https://github.com/apache/hertzbeat/tree/master/warehouse)** 提供监控数据仓储服务
> 采集指标结果数据管理,数据落盘,查询,计算统计。
- **[alerter](https://github.com/apache/hertzbeat/tree/master/hertzbeat-alerter)** 提供告警服务
> 告警计算触发,任务状态联动,告警配置,告警通知。
- **[web-app](https://github.com/apache/hertzbeat/tree/master/web-app)** 提供可视化控制台页面
> 监控告警系统可视化控制台前端
![hertzBeat](home/static/img/docs/hertzbeat-arch.png)
+23 -27
View File
@@ -9,6 +9,8 @@
<b><a href="README.md">English</a></b> | <a href="README_CN.md">中文</a> | <a href="README_JP.md">日本語</a>
</p>
> A real-time observability system with agentless, performance cluster, prometheus-compatible, custom monitoring and status page building capabilities.
[![Discord](https://img.shields.io/badge/Chat-Discord-7289DA?logo=discord)](https://discord.gg/Fb6M73htGr)
[![Reddit](https://img.shields.io/badge/Reddit-Community-7289DA?logo=reddit)](https://www.reddit.com/r/hertzbeat/)
[![Twitter](https://img.shields.io/twitter/follow/hertzbeat1024?logo=twitter)](https://x.com/hertzbeat1024)
@@ -19,7 +21,6 @@
[![YouTube Channel Subscribers](https://img.shields.io/youtube/channel/subscribers/UCri75zfWX0GHqJFPENEbLow?logo=youtube&label=YouTube%20Channel)](https://www.youtube.com/channel/UCri75zfWX0GHqJFPENEbLow)
[![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod&color=green)](https://gitpod.io/#https://github.com/apache/hertzbeat)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/apache/hertzbeat)
[![zread](https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTg0QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff)](https://zread.ai/apache/hertzbeat)
**Home: [hertzbeat.apache.org](https://hertzbeat.apache.org)**
**Email: <a href="mailto:dev-subscribe@hertzbeat.apache.org">Mail to `dev-subscribe@hertzbeat.apache.org`</a> to subscribe mailing lists**
@@ -27,23 +28,28 @@
## 🎡 <font color="green">Introduction</font>
[Apache HertzBeat™](https://github.com/apache/hertzbeat) is an AI-powered next-generation open source real-time observability system. Unified metrics and logs collection, centralized alerting distribution, intelligent management and analysis. No Agent required, high performance cluster, provides powerful custom monitoring and status page building capabilities.
[Apache HertzBeat™](https://github.com/apache/hertzbeat) is an easy-to-use, open source, real-time observability system with agentless, high performance cluster, prometheus-compatible, offers powerful custom monitoring and status page building capabilities.
### Features
- Integrates **collection + analysis + alerting + notification** into one platform, with new AI-powered interactions and features under HertzBeat AI, and built-in MCP Server capabilities.
- Unified metrics platform, agentless, Prometheus-compatible, supports application services, programs, databases, caches, operating systems, big data, middleware, web servers, cloud-native, networks, custom monitoring and more.
- Unified logging platform, seamlessly integrates multiple log sources through OTLP protocol for reporting.
- Unified alerting platform, integrates internal alerts with various external alert sources, unified alert processing and analysis, flexible real-time and periodic threshold rules, grouping convergence, silence, suppression, etc.
- Unified message distribution, alerts processed by the alerting platform are distributed via `Email` `Discord` `Slack` `Telegram` `DingTalk` `WeChat` `FeiShu` `SMS` `Webhook` `ServerChan` and other methods.
- Makes protocols such as `Http, Jmx, Ssh, Snmp, Jdbc, Prometheus` configurable, allowing you to collect any metrics by simply configuring the template `YML` file online. Imagine being able to quickly adapt to a new monitoring type like K8s or Docker simply by configuring online with HertzBeat.
- High performance, supports horizontal expansion of multi-collector clusters, multi-isolated network monitoring and cloud-edge collaboration.
- Provides powerful status page building capabilities, easily communicate the real-time status of your service to users.
* Combines **monitoring, alarm, and notification** features into one platform, and supports monitoring for web service, program, database, cache, os, webserver, middleware, bigdata, cloud-native, network, custom and more.
* Easy to use and agentless, web-based and with one-click monitoring and alerting, zero learning curve.
* Makes protocols such as `Http, Jmx, Ssh, Snmp, Jdbc, Prometheus` configurable, allowing you to collect any metrics by simply configuring the template `YML` file online. Imagine being able to quickly adapt to a new monitoring type like K8s or Docker simply by configuring online with HertzBeat.
* Compatible with the `Prometheus` ecosystem and more, can monitoring what `Prometheus` can monitoring with few clicks on webui.
* High performance, supports horizontal expansion of multi-collector clusters, multi-isolated network monitoring and cloud-edge collaboration.
* Provides flexible alarm threshold rules and timely notifications delivered via `Discord` `Slack` `Telegram` `Email` `Dingtalk` `WeChat` `FeiShu` `Webhook` `SMS` `ServerChan`.
* Provides powerful status page building capabilities, easily communicate the real-time status of your service to users.
> HertzBeat's unified platform, AI intelligence, powerful customization, multi-type support, high performance, and easy expansion, aims to help users quickly and conveniently achieve observability requirements.
> HertzBeat's powerful customization, multi-type support, high performance, easy expansion, and low coupling, aims to help users quickly build their own monitoring system.
----
![hertzbeat](home/static/img/home/1.png)
![hertzbeat](home/static/img/home/status.png)
![hertzbeat](home/static/img/home/9.png)
----
## 🥐 Architecture
@@ -52,8 +58,8 @@
## ⛄ Supported
> We define all metrics collection types such as `mysql`, `jvm`, and `k8s` as `YML` templates, allowing users to import them to support corresponding types of monitoring.
> Welcome everyone to contribute your customized general metrics type YML template during use.
> We define all monitoring collection types such as `mysql`, `jvm`, and `k8s` as `YML` monitoring templates, allowing users to import them to support corresponding types of monitoring.
> Welcome everyone to contribute your customized general monitoring type YML template during use.
- [Website](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-website.yml), [Port Telnet](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-port.yml),
@@ -155,8 +161,8 @@ Detailed config refer to [Install HertzBeat via Package](https://hertzbeat.apach
##### 3Start via source code
1. Local source code debugging needs to start the back-end project `hertzbeat-startup` and the front-end project `web-app`.
2. Backendneed `maven3+`, `java17`, `lombok`, add VM options in IDE: ` --add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED `, then start the `hertzbeat-startup` service.
1. Local source code debugging needs to start the back-end project `manager` and the front-end project `web-app`.
2. Backendneed `maven3+`, `java17`, `lombok`, add VM options in IDE: ` --add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED `, then start the `manager` service.
3. Webneed `nodejs npm angular-cli` environment, Run `ng serve --open` in `web-app` directory after backend startup.
4. Access `http://localhost:4200` to start, default account: `admin/hertzbeat`
@@ -355,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>
@@ -531,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>
+22 -25
View File
@@ -9,6 +9,8 @@
<a href="README.md">English</a> | <b><a href="README_CN.md">中文</a></b> | <a href="README_JP.md">日本語</a>
</p>
> 实时观测系统,无需 Agent,性能集群,兼容 Prometheus,自定义监控和状态页构建能力。
[![Discord](https://img.shields.io/badge/Chat-Discord-7289DA?logo=discord)](https://discord.gg/Fb6M73htGr)
[![Reddit](https://img.shields.io/badge/Reddit-Community-7289DA?logo=reddit)](https://www.reddit.com/r/hertzbeat/)
[![Twitter](https://img.shields.io/twitter/follow/hertzbeat1024?logo=twitter)](https://x.com/hertzbeat1024)
@@ -26,24 +28,29 @@
## 🎡 <font color="green">介绍</font>
[Apache HertzBeat™](https://github.com/apache/hertzbeat) 是 AI 驱动的下一代开源实时观测系统。指标日志统一收集,告警一站分发,智能管控分析。无需 Agent,高性能集群,提供强大的自定义监控和状态页构建能力。
[Apache HertzBeat™](https://github.com/apache/hertzbeat) 是一个易用友好的开源实时监控告警系统,无需 Agent,高性能集群,兼容 Prometheus,提供强大的自定义监控和状态页构建能力。
### 特点
-```采集+分析+告警+通知```为一体,HertzBeat AI 驱动下的新交互与功能,也内置 MCP Server 对外能力
- 统一的指标平台,无需 Agent,兼容 Prometheus,支持应用服务,程序,数据库,缓存,操作系统,大数据,中间件,Web 服务器,云原生,网络,自定义等。
- 统一的日志平台,通过 OTLP 协议多日志源无缝对接上报。
- 统一的告警平台,内部告警与外部多种告警源集成接入,统一告警处理分析,灵活的实时与周期阈值规则,分组收敛,静默,抑制等
- 统一的消息分发,告警平台处理后通过 `邮件` `Discord` `Slack` `Telegram` `钉钉` `微信` `飞书` `短信` `Webhook` `Server酱` 等方式分发通知。
-`Http, Jmx, Ssh, Snmp, Jdbc, Prometheus` 等协议规范可配置化,只需配置模板 `YML` 就能自定义采集指标。您相信只需简单配置即可快速适配一款 `K8s``Docker` 等新的监控类型吗?
- **监控+告警+通知** 为一体,支持对应用服务,应用程序,数据库,缓存,操作系统,大数据,中间件,Web 服务器,云原生,网络,自定义等监控阈值告警通知一步到位
- 易用友好,无需 `Agent`,全 `WEB` 页面操作,鼠标点一点就能监控告警,无需学习成本。
- `Http, Jmx, Ssh, Snmp, Jdbc, Prometheus` 等协议规范可配置化,只需在浏览器配置监控模板 `YML` 就能使用这些协议去自定义采集想要的指标。您相信只需简单配置即可快速适配一款 `K8s``Docker` 等新的监控类型吗?
- 兼容 `Prometheus` 的系统生态并且更多,只需页面操作就可以监控 `Prometheus` 所能监控的
- 高性能,支持多采集器集群横向扩展,支持多隔离网络监控,云边协同。
- 灵活的告警阈值规则,`邮件` `Discord` `Slack` `Telegram` `钉钉` `微信` `飞书` `短信` `Webhook` `Server酱` 等方式消息及时送达。
- 提供强大的状态页构建能力,轻松向用户传达您产品服务的实时状态。
> `HertzBeat`的统一平台,AI智能,强大自定义,多类型支持,高性能,易扩展,希望能帮助用户快速方便实现观测需求
> `HertzBeat`的强大自定义,多类型支持,高性能,易扩展,低耦合,希望能帮助用户快速搭建自有监控系统
----
![hertzbeat](home/static/img/home/1.png)
![hertzbeat](home/static/img/home/status.png)
![hertzbeat](home/static/img/home/9.png)
----
## 🥐 模块
@@ -52,8 +59,8 @@
## ⛄ 已支持
> 我们将监控采集类型(mysql,jvm,k8s)都定义为 `yml` 模板,用户可以导入这些模板来支持对应类型的监控!
> 欢迎大家一起贡献你使用过程中自定义的通用指标模板。
> 我们将监控采集类型(mysql,jvm,k8s)都定义为 `yml` 监控模板,用户可以导入这些模板来支持对应类型的监控!
> 欢迎大家一起贡献你使用过程中自定义的通用监控类型监控模板。
- [Website](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-website.yml), [Port Telnet](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-port.yml),
[Http Api](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-api.yml), [Ping Connect](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ping.yml),
@@ -91,7 +98,7 @@
- [CiscoSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-cisco_switch.yml), [HpeSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-hpe_switch.yml),
[HuaweiSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-huawei_switch.yml), [TpLinkSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-tplink_switch.yml),
[H3cSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-h3c_switch.yml)
- 和更多自定义指标模板。
- 和更多自定义监控模板。
- 通知支持 `Discord` `Slack` `Telegram` `邮件` `钉钉` `微信` `飞书` `短信` `Webhook` `Server酱`
## 🐕 快速开始
@@ -152,8 +159,8 @@
##### 方式三:本地代码启动
1. 此为前后端分离项目,本地代码调试需要分别启动后端工程 `hertzbeat-startup` 和前端工程 `web-app`
2. 后端:需要 `maven3+`, `java17` 和 `lombok` 环境,修改 `YML` 配置信息,添加JVM参数`--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED`后启动 `hertzbeat-startup` 服务即可。
1. 此为前后端分离项目,本地代码调试需要分别启动后端工程 `manager` 和前端工程 `web-app`
2. 后端:需要 `maven3+`, `java17` 和 `lombok` 环境,修改 `YML` 配置信息,添加JVM参数`--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED`后启动 `manager` 服务即可。
3. 前端:需要 `nodejs npm angular-cli`环境,待本地后端启动后,在 `web-app` 目录下启动 `ng serve --open`
4. 浏览器访问 `http://localhost:4200` 即可开始,默认账号密码 `admin/hertzbeat`
@@ -352,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>
@@ -528,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>
+22 -24
View File
@@ -9,6 +9,8 @@
<a href="README.md">English</a> | <a href="README_CN.md">中文</a> | <b><a href="README_JP.md">日本語</a></b>
</p>
> オープンソースのリアルタイム監視システム、エージェントレス、クラスタ対応、Prometheus互換、カスタムおよびステータスページ付き。
[![Discord](https://img.shields.io/badge/Chat-Discord-7289DA?logo=discord)](https://discord.gg/Fb6M73htGr)
[![Reddit](https://img.shields.io/badge/Reddit-Community-7289DA?logo=reddit)](https://www.reddit.com/r/hertzbeat/)
[![Twitter](https://img.shields.io/twitter/follow/hertzbeat1024?logo=twitter)](https://x.com/hertzbeat1024)
@@ -26,23 +28,29 @@
## 🎡 <font color="green">紹介</font>
[Apache HertzBeat](https://github.com/apache/hertzbeat) は AI 駆動の次世代オープンソースリアルタイム観測システムです。メトリクスとログの統一収集、アラートの一元配信、インテリジェント管理分析。エージェント不要、高性能クラスタ、強力なカスタム監視とステータスページ構築機能を提供します
[Apache HertzBeat](https://github.com/apache/hertzbeat) incubating)はオープンソースリアルタイム監視システム、エージェントレス、クラスタ対応、Prometheus互換、カスタムおよびステータスページ付き
### 特性
- **収集+分析+アラート+通知**を一つのプラットフォームに統合し、HertzBeat AI 駆動の新しいインタラクションと機能、内蔵 MCP Server 機能を提供
- 統一メトリクスプラットフォーム、エージェントレス、Prometheus互換、アプリケーションサービス、プログラム、データベース、キャッシュ、オペレーティングシステム、ビッグデータ、ミドルウェア、Webサーバー、クラウドネイティブ、ネットワーク、カスタム監視などをサポート
- 統一ログプラットフォーム、OTLP プロトコルを通じて複数のログソースをシームレスに統合してレポート
- 統一アラートプラットフォーム、内部アラートと様々な外部アラートソースを統合接続、統一アラート処理分析、柔軟なリアルタイムと周期的閾値ルール、グループ収束、サイレンス、抑制など
- 統一メッセージ配信、アラートプラットフォームで処理後、`メール` `Discord` `Slack` `Telegram` `DingTalk` `WeChat` `FeiShu` `SMS` `Webhook` `ServerChan` などの方法で配信通知
- `Http、Jmx、Ssh、Snmp、Jdbc、Prometheus`などのプロトコルを設定可能にし、テンプレート`YML`ファイルをオンラインで設定するだけで、あらゆるメトリクスを収集できるようにします。HertzBeatでオンライン設定するだけで、`K8s``Docker`のような新しい監視タイプに素早く対応できることを想像してみてください
- 高性能で、コレクタークラスタの水平拡張、マルチ分離ネットワーク監視、クラウドエッジ協調をサポート
- 強力なステータスページ構築機能を提供し、サービスのリアルタイムステータスをユーザーに簡単に伝達。
- **モニター・アラーム・通知** を一体化し、Web、データベース、キャッシュ、OS、ミドルウェア、クラウドネイティブ、ネットワークなどをサポート
- 使いやすさを重視し、マウスのクリックだけで完全なWebベースの操作が可能
- `Http、Jmx、Ssh、Snmp、jdbc、Prometheus`などのプロトコルを設定可能にし、テンプレート`YML`ファイルをオンラインで設定するだけで、あらゆるメトリクスを収集できるようにします。HertzBeatでオンライン設定するだけで、`K8S``Docker`のような新しい監視タイプに素早く対応できることを想像してみてください
- `Prometheus`のエコシステム互換、Prometheusが監視できるものを、全てオンライン設定によって監視することができます
- 高性能で、コレクタークラスタ、マルチアイソレートネットワーク、クラウドエッジをサポート
- 柔軟なアラーム閾値ルールで、メール、discord、slack、telegramなどを通じてタイムリーに通知
- 強力なステータスページを簡単に構築し、リアルタイムのステータスをユーザーと共有
> `HertzBeat`の統一プラットフォーム、AI インテリジェンス、強力なカスタマイズ、多種類サポート、高性能、容易な拡張性は、ユーザーが迅速かつ便利に観測要件を実現することを支援することを目的としています。
> `HertzBeat`の強力なカスタマイズ、多種類のサポート、高性能および容易な拡張性は、ユーザーが迅速に独自の監視システムを構築するのを支援することを目的としています。
----
![hertzbeat](home/static/img/home/1.png)
![hertzbeat](home/static/img/home/status.png)
![hertzbeat](home/static/img/home/9.png)
----
## 🥐 モジュール
@@ -155,8 +163,8 @@
##### 方式3:ローカルの実行
1. ローカルの実行には、バックエンドのプロジェクト`hertzbeat-startup`とフロントエンドのプロジェクト`web-app`を起動する必要があります。
2. バックエンド:`maven3+`、`java17`と`lombok`の環境は必要です。`YML` 設定を修正し、Java仮想マシンパラメータに`--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED` を追加し、`hertzbeat-startup` を起動します。
1. ローカルの実行には、バックエンドのプロジェクト`manager`とフロントエンドのプロジェクト`web-app`を起動する必要があります。
2. バックエンド:`maven3+`、`java17`と`lombok`の環境は必要です。`YML` 設定を修正し、Java仮想マシンパラメータに`--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED` を追加し、`manager` を起動します。
3. フロントエンド:`nodejs npm angular-cli`の環境は必要です。ローカルのバックエンドが立ち上がったら、`web-app` ディレクトリで `ng serve --open` というコマンドを実行します。
4. スタート:`http://localhost:4200`にアクセスします。デフォルトのアカウントとパスワード:`admin/hertzbeat`。
@@ -355,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>
@@ -531,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>
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "ftp",
"instance": "127.0.0.1",
"host": "127.0.0.1",
"name": "{{.param.monitorFTP}}"
},
"params": [
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "api",
"instance": "127.0.0.1",
"host": "127.0.0.1",
"name": "{{.param.monitorHTTP}}"
},
"params": [
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "ping",
"instance": "127.0.0.1",
"host": "127.0.0.1",
"name": "{{.param.monitorPing}}"
},
"params": [
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "port",
"instance": "127.0.0.1",
"host": "127.0.0.1",
"name": "{{.param.monitorPort}}"
},
"params": [
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "fullsite",
"instance": "127.0.0.1",
"host": "127.0.0.1",
"name": "{{.param.monitorSiteMap}}"
},
"params": [
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "ssl_cert",
"instance": "127.0.0.1",
"host": "127.0.0.1",
"name": "{{.param.monitorSSL}}"
},
"params": [
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "udp_port",
"instance": "127.0.0.1",
"host": "127.0.0.1",
"name": "{{.param.monitorUDP}}"
},
"params": [
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "website",
"instance": "127.0.0.1",
"host": "127.0.0.1",
"name": "{{.param.monitorWebsite}}"
},
"params": [
+1 -1
View File
@@ -4,7 +4,7 @@
"intervals": 10,
"tags": [],
"app": "kubernetes",
"instance": "172.29.0.11",
"host": "172.29.0.11",
"name": "Brave_Stingray_55yR"
},
"collector": "",
@@ -23,10 +23,10 @@
<artifactId>hertzbeat</artifactId>
<version>2.0-SNAPSHOT</version>
</parent>
<artifactId>hertzbeat-ai</artifactId>
<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,29 +51,16 @@
</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>
</dependency>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-alerter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-manager</artifactId>
</dependency>
<dependency>
<groupId>com.usthe.sureness</groupId>
<artifactId>spring-boot3-starter-sureness</artifactId>
@@ -103,4 +90,4 @@
</plugins>
</build>
</project>
</project>
@@ -0,0 +1,81 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.adapters;
import org.apache.hertzbeat.ai.agent.pojo.dto.Hierarchy;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.springframework.data.domain.Page;
import java.util.List;
/**
* Interface that provides access to alert definition information by retrieving data
* through the underlying alert define service.
*/
public interface AlertDefineServiceAdapter {
/**
* Add a new alert rule definition
* @param alertDefine Alert definition to add
* @return Created alert definition with ID
*/
AlertDefine addAlertDefine(AlertDefine alertDefine);
/**
* Get alert definitions with filtering and pagination
* @param search Search term
* @param app Monitor type filter
* @param enabled Enabled status filter
* @param sort Sort field
* @param order Sort order
* @param pageIndex Page index
* @param pageSize Page size
* @return Page of alert definitions
*/
Page<AlertDefine> getAlertDefines(String search, String app, Boolean enabled, String sort, String order, int pageIndex, int pageSize);
/**
* Get alert definition by ID
* @param id Alert definition ID
* @return Alert definition if found
*/
AlertDefine getAlertDefine(Long id);
/**
* Enable or disable alert definition
* @param id Alert definition ID
* @param enabled Whether to enable
*/
void toggleAlertDefineStatus(Long id, boolean enabled);
/**
* Modify/update an existing alert definition
* @param alertDefine Alert definition to update
* @return Updated alert definition
*/
AlertDefine modifyAlertDefine(AlertDefine alertDefine);
/**
* Get specific app hierarchy structure
* @param app App type
* @param lang Language for localization
* @return List of hierarchy objects for specific app
*/
List<Hierarchy> getAppHierarchy(String app, String lang);
}
@@ -0,0 +1,60 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.adapters;
import org.apache.hertzbeat.alert.dto.AlertSummary;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.springframework.data.domain.Page;
/**
* Interface that provides access to alert information by retrieving alert data
* through the underlying alert service.
*/
public interface AlertServiceAdapter {
/**
* Get single alerts with filtering and pagination
* @param status Alert status
* @param search Search term
* @param sort Sort field
* @param order Sort order
* @param pageIndex Page index
* @param pageSize Page size
* @return Page of single alerts
*/
Page<SingleAlert> getSingleAlerts(String status, String search, String sort, String order, int pageIndex, int pageSize);
/**
* Get group alerts with filtering and pagination
* @param status Alert status
* @param search Search term
* @param sort Sort field
* @param order Sort order
* @param pageIndex Page index
* @param pageSize Page size
* @return Page of group alerts
*/
Page<GroupAlert> getGroupAlerts(String status, String search, String sort, String order, int pageIndex, int pageSize);
/**
* Get alerts summary statistics
* @return Alert summary information
*/
AlertSummary getAlertsSummary();
}
@@ -0,0 +1,55 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.adapters;
import org.apache.hertzbeat.common.entity.dto.MetricsData;
import org.apache.hertzbeat.common.entity.dto.MetricsHistoryData;
/**
* Interface that provides access to metrics information by retrieving metrics data
* through the underlying metrics service.
*/
public interface MetricsServiceAdapter {
/**
* Check warehouse storage server status
* @return true if warehouse is available, false otherwise
*/
Boolean getWarehouseStorageServerStatus();
/**
* Query real-time metrics data
* @param monitorId Monitor ID
* @param metrics Metrics name
* @return Real-time metrics data
*/
MetricsData getMetricsData(Long monitorId, String metrics);
/**
* Query historical metrics data
* @param monitorId Monitor ID
* @param app Monitor type
* @param metrics Metrics name
* @param metric Metric field name
* @param label Label filter
* @param history Query historical time period
* @param interval Whether to aggregate data
* @return Historical metrics data
*/
MetricsHistoryData getMetricHistoryData(Long monitorId, String app, String metrics, String metric, String label, String history, Boolean interval);
}
@@ -0,0 +1,71 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.adapters;
import org.springframework.data.domain.Page;
import org.apache.hertzbeat.common.entity.manager.Monitor;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import java.util.List;
import java.util.Map;
/**
* Interface that provides access to monitor information by retrieving monitor data
* through the underlying monitor service.
*/
public interface MonitorServiceAdapter {
Page<Monitor> getMonitors(
List<Long> ids,
String app,
String search,
Byte status,
String sort,
String order,
Integer pageIndex,
Integer pageSize,
String labels
);
/**
* Add a new monitor
*
* @param monitor The monitor entity to create
* @param params List of parameters for the monitor
* @param collector Optional collector assignment
* @return The created monitor ID
*/
Long addMonitor(Monitor monitor, List<Param> params, String collector);
/**
* Get all available monitor types with their display names
*
* @param language Language code (e.g., "en-US", "zh-CN")
* @return Map of monitor type key to display name
*/
Map<String, String> getAvailableMonitorTypes(String language);
/**
* Get parameter definitions for a specific monitor type
*
* @param app Monitor type/application name (e.g., "linux", "mysql", "redis")
* @return List of parameter definitions for the monitor type
*/
List<ParamDefine> getMonitorParamDefines(String app);
}
@@ -0,0 +1,257 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.adapters.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.adapters.AlertDefineServiceAdapter;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.apache.hertzbeat.ai.agent.pojo.dto.Hierarchy;
import org.apache.hertzbeat.ai.agent.utils.UtilityClass;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.List;
/**
* Implementation of the AlertDefineServiceAdapter interface that provides access to alert definition information
* through reflection by invoking the underlying alert define service implementation.
*/
@Slf4j
@Component
public class AlertDefineServiceAdapterImpl implements AlertDefineServiceAdapter {
@Override
public AlertDefine addAlertDefine(AlertDefine alertDefine) {
try {
Object alertDefineService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for addAlertDefine: {}", subjectSum);
try {
alertDefineService = SpringContextHolder.getBean("alertDefineServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertDefineServiceImpl'");
}
assert alertDefineService != null;
log.debug("AlertDefineService bean found: {}", alertDefineService.getClass().getSimpleName());
Method method = alertDefineService.getClass().getMethod("addAlertDefine", AlertDefine.class);
method.invoke(alertDefineService, alertDefine);
log.debug("Successfully added alert define with ID: {}", alertDefine.getId());
return alertDefine;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: addAlertDefine", e);
} catch (Exception e) {
log.error("Failed to invoke addAlertDefine via adapter", e);
throw new RuntimeException("Failed to invoke addAlertDefine via adapter: " + e.getMessage(), e);
}
}
@Override
public Page<AlertDefine> getAlertDefines(String search, String app, Boolean enabled, String sort, String order, int pageIndex, int pageSize) {
try {
Object alertDefineService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAlertDefines: {}", subjectSum);
try {
alertDefineService = SpringContextHolder.getBean("alertDefineServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertDefineServiceImpl'");
}
assert alertDefineService != null;
log.debug("AlertDefineService bean found: {}", alertDefineService.getClass().getSimpleName());
Method method = alertDefineService.getClass().getMethod(
"getAlertDefines",
List.class, String.class, String.class, String.class, int.class, int.class);
@SuppressWarnings("unchecked")
Page<AlertDefine> result = (Page<AlertDefine>) method.invoke(
alertDefineService, null, search, sort, order, pageIndex, pageSize);
log.debug("Successfully retrieved {} alert defines", result.getContent().size());
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getAlertDefines", e);
} catch (Exception e) {
log.error("Failed to invoke getAlertDefines via adapter", e);
throw new RuntimeException("Failed to invoke getAlertDefines via adapter: " + e.getMessage(), e);
}
}
@Override
public AlertDefine getAlertDefine(Long id) {
try {
Object alertDefineService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAlertDefine: {}", subjectSum);
try {
alertDefineService = SpringContextHolder.getBean("alertDefineServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertDefineServiceImpl'");
}
assert alertDefineService != null;
log.debug("AlertDefineService bean found: {}", alertDefineService.getClass().getSimpleName());
Method method = alertDefineService.getClass().getMethod("getAlertDefine", long.class);
AlertDefine result = (AlertDefine) method.invoke(alertDefineService, id);
log.debug("Successfully retrieved alert define with ID: {}", id);
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getAlertDefine", e);
} catch (Exception e) {
log.error("Failed to invoke getAlertDefine via adapter for ID: {}", id, e);
throw new RuntimeException("Failed to invoke getAlertDefine via adapter: " + e.getMessage(), e);
}
}
@Override
public void toggleAlertDefineStatus(Long id, boolean enabled) {
try {
Object alertDefineService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for toggleAlertDefineStatus: {}", subjectSum);
try {
alertDefineService = SpringContextHolder.getBean("alertDefineServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertDefineServiceImpl'");
}
assert alertDefineService != null;
log.debug("AlertDefineService bean found: {}", alertDefineService.getClass().getSimpleName());
// First get the existing AlertDefine
Method getMethod = alertDefineService.getClass().getMethod("getAlertDefine", long.class);
AlertDefine alertDefine = (AlertDefine) getMethod.invoke(alertDefineService, id);
if (alertDefine == null) {
throw new RuntimeException("AlertDefine with ID " + id + " not found");
}
// Update the enable status
alertDefine.setEnable(enabled);
// Use modifyAlertDefine to save the changes
Method modifyMethod = alertDefineService.getClass().getMethod("modifyAlertDefine", AlertDefine.class);
modifyMethod.invoke(alertDefineService, alertDefine);
log.debug("Successfully toggled alert define status for ID: {} to enabled: {}", id, enabled);
} catch (Exception e) {
log.error("Failed to invoke toggleAlertDefineStatus via adapter for ID: {}", id, e);
throw new RuntimeException("Failed to invoke toggleAlertDefineStatus via adapter: " + e.getMessage(), e);
}
}
@Override
public AlertDefine modifyAlertDefine(AlertDefine alertDefine) {
try {
Object alertDefineService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for modifyAlertDefine: {}", subjectSum);
try {
alertDefineService = SpringContextHolder.getBean("alertDefineServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertDefineServiceImpl'");
}
assert alertDefineService != null;
log.debug("AlertDefineService bean found: {}", alertDefineService.getClass().getSimpleName());
Method method = alertDefineService.getClass().getMethod("modifyAlertDefine", AlertDefine.class);
method.invoke(alertDefineService, alertDefine);
log.debug("Successfully modified alert define with ID: {}", alertDefine.getId());
return alertDefine;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: modifyAlertDefine", e);
} catch (Exception e) {
log.error("Failed to invoke modifyAlertDefine via adapter", e);
throw new RuntimeException("Failed to invoke modifyAlertDefine via adapter: " + e.getMessage(), e);
}
}
/**
* Retrieves the application hierarchy for a given app and language.
* Uses reflection to call the underlying app service method.
*
* @param app The application name
* @param lang The language code (optional, defaults to "en-US")
* @return List of Hierarchy objects representing the app hierarchy
*/
@Override
public List<Hierarchy> getAppHierarchy(String app, String lang) {
try {
Object appService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAppHierarchy: {}", subjectSum);
try {
appService = SpringContextHolder.getBean("appServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'appServiceImpl', trying by class name");
}
assert appService != null;
log.debug("AppService bean found for getAppHierarchy: {}", appService.getClass().getSimpleName());
// Provide default language if not specified
if (lang == null || lang.trim().isEmpty()) {
lang = "en-US";
}
// Call getAppHierarchy method: getAppHierarchy(String app, String lang)
Method method = appService.getClass().getMethod("getAppHierarchy", String.class, String.class);
List<?> managerHierarchies = (List<?>) method.invoke(appService, app, lang);
// Convert manager DTOs to ai-agent DTOs
List<Hierarchy> result = UtilityClass.convertToAgentHierarchies(managerHierarchies);
log.debug("Successfully retrieved and converted {} hierarchies for app '{}'", result.size(), app);
return result;
} catch (Exception e) {
log.error("Failed to get app hierarchy for app '{}': {}", app, e.getMessage(), e);
throw new RuntimeException("Failed to get app hierarchy for " + app, e);
}
}
}
@@ -0,0 +1,141 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.adapters.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.adapters.AlertServiceAdapter;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.apache.hertzbeat.alert.dto.AlertSummary;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* Implementation of the AlertServiceAdapter interface that provides access to alert information
* through reflection by invoking the underlying alert service implementation.
*/
@Slf4j
@Component
public class AlertServiceAdapterImpl implements AlertServiceAdapter {
@Override
public Page<SingleAlert> getSingleAlerts(String status, String search, String sort, String order, int pageIndex, int pageSize) {
try {
Object alertService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getSingleAlerts: {}", subjectSum);
try {
alertService = SpringContextHolder.getBean("alertServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertServiceImpl'");
}
assert alertService != null;
log.debug("AlertService bean found: {}", alertService.getClass().getSimpleName());
Method method = alertService.getClass().getMethod(
"getSingleAlerts",
String.class, String.class, String.class, String.class, int.class, int.class);
@SuppressWarnings("unchecked")
Page<SingleAlert> result = (Page<SingleAlert>) method.invoke(
alertService, status, search, sort, order, pageIndex, pageSize);
log.debug("Successfully retrieved {} single alerts", result.getContent().size());
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getSingleAlerts", e);
} catch (Exception e) {
log.error("Failed to invoke getSingleAlerts via adapter", e);
throw new RuntimeException("Failed to invoke getSingleAlerts via adapter: " + e.getMessage(), e);
}
}
@Override
public Page<GroupAlert> getGroupAlerts(String status, String search, String sort, String order, int pageIndex, int pageSize) {
try {
Object alertService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getGroupAlerts: {}", subjectSum);
try {
alertService = SpringContextHolder.getBean("alertServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertServiceImpl'");
}
assert alertService != null;
log.debug("AlertService bean found: {}", alertService.getClass().getSimpleName());
Method method = alertService.getClass().getMethod(
"getGroupAlerts",
String.class, String.class, String.class, String.class, int.class, int.class);
@SuppressWarnings("unchecked")
Page<GroupAlert> result = (Page<GroupAlert>) method.invoke(
alertService, status, search, sort, order, pageIndex, pageSize);
log.debug("Successfully retrieved {} group alerts", result.getContent().size());
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getGroupAlerts", e);
} catch (Exception e) {
log.error("Failed to invoke getGroupAlerts via adapter", e);
throw new RuntimeException("Failed to invoke getGroupAlerts via adapter: " + e.getMessage(), e);
}
}
@Override
public AlertSummary getAlertsSummary() {
try {
Object alertService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAlertsSummary: {}", subjectSum);
try {
alertService = SpringContextHolder.getBean("alertServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertServiceImpl'");
}
assert alertService != null;
log.debug("AlertService bean found: {}", alertService.getClass().getSimpleName());
Method method = alertService.getClass().getMethod("getAlertsSummary");
AlertSummary result = (AlertSummary) method.invoke(alertService);
log.debug("Successfully retrieved alerts summary");
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getAlertsSummary", e);
} catch (Exception e) {
log.error("Failed to invoke getAlertsSummary via adapter", e);
throw new RuntimeException("Failed to invoke getAlertsSummary via adapter: " + e.getMessage(), e);
}
}
}
@@ -0,0 +1,136 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.adapters.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.adapters.MetricsServiceAdapter;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.apache.hertzbeat.common.entity.dto.MetricsData;
import org.apache.hertzbeat.common.entity.dto.MetricsHistoryData;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* Implementation of the MetricsServiceAdapter interface that provides access to metrics information
* through reflection by invoking the underlying metrics service implementation.
*/
@Slf4j
@Component
public class MetricsServiceAdapterImpl implements MetricsServiceAdapter {
@Override
public Boolean getWarehouseStorageServerStatus() {
try {
Object metricsDataService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getWarehouseStorageServerStatus: {}", subjectSum);
try {
metricsDataService = SpringContextHolder.getBean("metricsDataServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'metricsDataServiceImpl'");
}
assert metricsDataService != null;
log.debug("MetricsDataService bean found: {}", metricsDataService.getClass().getSimpleName());
Method method = metricsDataService.getClass().getMethod("getWarehouseStorageServerStatus");
Boolean result = (Boolean) method.invoke(metricsDataService);
log.debug("Successfully retrieved warehouse storage server status: {}", result);
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getWarehouseStorageServerStatus", e);
} catch (Exception e) {
log.error("Failed to invoke getWarehouseStorageServerStatus via adapter", e);
throw new RuntimeException("Failed to invoke getWarehouseStorageServerStatus via adapter: " + e.getMessage(), e);
}
}
@Override
public MetricsData getMetricsData(Long monitorId, String metrics) {
try {
Object metricsDataService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getMetricsData: {}", subjectSum);
try {
metricsDataService = SpringContextHolder.getBean("metricsDataServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'metricsDataServiceImpl'");
}
assert metricsDataService != null;
log.debug("MetricsDataService bean found: {}", metricsDataService.getClass().getSimpleName());
Method method = metricsDataService.getClass().getMethod(
"getMetricsData",
Long.class, String.class);
MetricsData result = (MetricsData) method.invoke(metricsDataService, monitorId, metrics);
log.debug("Successfully retrieved metrics data for monitor {} and metrics {}", monitorId, metrics);
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getMetricsData", e);
} catch (Exception e) {
log.error("Failed to invoke getMetricsData via adapter for monitor {} and metrics {}", monitorId, metrics, e);
throw new RuntimeException("Failed to invoke getMetricsData via adapter: " + e.getMessage(), e);
}
}
@Override
public MetricsHistoryData getMetricHistoryData(Long monitorId, String app, String metrics, String metric, String label, String history, Boolean interval) {
try {
Object metricsDataService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getMetricHistoryData: {}", subjectSum);
try {
metricsDataService = SpringContextHolder.getBean("metricsDataServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'metricsDataServiceImpl'");
}
assert metricsDataService != null;
log.debug("MetricsDataService bean found: {}", metricsDataService.getClass().getSimpleName());
Method method = metricsDataService.getClass().getMethod(
"getMetricHistoryData",
Long.class, String.class, String.class, String.class, String.class, String.class, Boolean.class);
MetricsHistoryData result = (MetricsHistoryData) method.invoke(
metricsDataService, monitorId, app, metrics, metric, label, history, interval);
log.debug("Successfully retrieved historical metrics data for monitor {} and metrics {}", monitorId, metrics);
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getMetricHistoryData", e);
} catch (Exception e) {
log.error("Failed to invoke getMetricHistoryData via adapter for monitor {} and metrics {}", monitorId, metrics, e);
throw new RuntimeException("Failed to invoke getMetricHistoryData via adapter: " + e.getMessage(), e);
}
}
}
@@ -0,0 +1,214 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.adapters.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.adapters.MonitorServiceAdapter;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.springframework.data.domain.Page;
import org.apache.hertzbeat.common.entity.manager.Monitor;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
/**
* Implementation of the MonitorServiceAdapter interface that provides access to monitor information
* through reflection by invoking the underlying monitor service implementation.
*/
@Slf4j
@Component
public class MonitorServiceAdapterImpl implements MonitorServiceAdapter {
@Override
public Page<Monitor> getMonitors(
List<Long> ids,
String app,
String search,
Byte status,
String sort,
String order,
Integer pageIndex,
Integer pageSize,
String labels) {
try {
// Provide default values for all nullable parameters
if (sort == null || sort.trim().isEmpty()) {
sort = "gmtCreate";
}
if (order == null || order.trim().isEmpty()) {
order = "desc";
}
if (pageIndex == null) {
pageIndex = 0;
}
if (pageSize == null) {
pageSize = 8;
}
Object monitorService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject: {}", subjectSum);
try {
monitorService = SpringContextHolder.getBean("monitorServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'monitorServiceImpl', trying by class name");
}
assert monitorService != null;
log.debug("MonitorService bean found: {}", monitorService.getClass().getSimpleName());
Method method = monitorService.getClass().getMethod(
"getMonitors",
List.class, String.class, String.class, Byte.class,
String.class, String.class, int.class, int.class, String.class);
@SuppressWarnings("unchecked")
Page<Monitor> result = (Page<Monitor>) method.invoke(
monitorService,
ids, app, search, status, sort, order, pageIndex, pageSize, labels);
log.debug("MonitorServiceAdapter.getMonitors result: {}", result.getContent());
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getMonitors", e);
} catch (Exception e) {
log.debug("Failed to invoke getMonitors via adapter", e);
throw new RuntimeException("Failed to invoke getMonitors via adapter", e);
}
}
@Override
public Long addMonitor(Monitor monitor, List<Param> params, String collector) {
try {
Object monitorService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for addMonitor: {}", subjectSum);
try {
monitorService = SpringContextHolder.getBean("monitorServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'monitorServiceImpl', trying by class name");
}
assert monitorService != null;
log.debug("MonitorService bean found for addMonitor: {}", monitorService.getClass().getSimpleName());
// Call addMonitor method: addMonitor(Monitor monitor, List<Param> params, String collector, GrafanaDashboard dashboard)
Method method = monitorService.getClass().getMethod(
"addMonitor",
Monitor.class, List.class, String.class,
Class.forName("org.apache.hertzbeat.common.entity.grafana.GrafanaDashboard"));
// Call the method with null dashboard
method.invoke(monitorService, monitor, params, collector, null);
log.debug("Successfully added monitor: {} with ID: {}", monitor.getName(), monitor.getId());
return monitor.getId();
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: addMonitor", e);
} catch (Exception e) {
log.error("Failed to invoke addMonitor via adapter", e);
throw new RuntimeException("Failed to invoke addMonitor via adapter: " + e.getMessage(), e);
}
}
@Override
public Map<String, String> getAvailableMonitorTypes(String language) {
try {
Object appService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAvailableMonitorTypes: {}", subjectSum);
try {
appService = SpringContextHolder.getBean("appServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'appServiceImpl', trying by class name");
}
assert appService != null;
log.debug("AppService bean found for getAvailableMonitorTypes: {}", appService.getClass().getSimpleName());
// Provide default language if not specified
if (language == null || language.trim().isEmpty()) {
language = "en-US";
}
// Call getI18nApps method: getI18nApps(String lang)
Method method = appService.getClass().getMethod("getI18nApps", String.class);
@SuppressWarnings("unchecked")
Map<String, String> result = (Map<String, String>) method.invoke(appService, language);
log.debug("Successfully retrieved {} monitor types", result.size());
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getI18nApps", e);
} catch (Exception e) {
log.error("Failed to invoke getI18nApps via adapter", e);
throw new RuntimeException("Failed to invoke getI18nApps via adapter: " + e.getMessage(), e);
}
}
@Override
public List<ParamDefine> getMonitorParamDefines(String app) {
try {
Object appService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getMonitorParamDefines: {}", subjectSum);
try {
appService = SpringContextHolder.getBean("appServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'appServiceImpl', trying by class name");
}
assert appService != null;
log.debug("AppService bean found for getMonitorParamDefines: {}", appService.getClass().getSimpleName());
// Validate app parameter
if (app == null || app.trim().isEmpty()) {
throw new IllegalArgumentException("Monitor type/app parameter is required");
}
// Call getAppParamDefines method: getAppParamDefines(String app)
Method method = appService.getClass().getMethod("getAppParamDefines", String.class);
@SuppressWarnings("unchecked")
List<ParamDefine> result = (List<ParamDefine>) method.invoke(appService, app.toLowerCase().trim());
log.debug("Successfully retrieved {} parameter definitions for monitor type: {}", result.size(), app);
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getAppParamDefines", e);
} catch (Exception e) {
log.error("Failed to invoke getAppParamDefines via adapter for app: {}", app, e);
throw new RuntimeException("Failed to invoke getAppParamDefines via adapter: " + e.getMessage(), e);
}
}
}
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.ai.config;
package org.apache.hertzbeat.ai.agent.config;
import com.fasterxml.jackson.core.type.TypeReference;
@@ -0,0 +1,57 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.config;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.pojo.dto.OpenAiConfigDto;
import org.apache.hertzbeat.ai.agent.service.OpenAiConfigService;
import org.springframework.ai.model.ApiKey;
import org.springframework.stereotype.Component;
/**
* Dynamic OpenAI API Key implementation that retrieves the API key
* from our configuration service (database first, then YAML fallback)
*/
@Slf4j
@Component
public class DynamicOpenAiApiKey implements ApiKey {
private final OpenAiConfigService openAiConfigService;
public DynamicOpenAiApiKey(OpenAiConfigService openAiConfigService) {
this.openAiConfigService = openAiConfigService;
}
@Override
public String getValue() {
try {
OpenAiConfigDto effectiveConfig = openAiConfigService.getEffectiveConfig();
if (effectiveConfig != null && effectiveConfig.isEnable() && effectiveConfig.getApiKey() != null) {
log.debug("Retrieved OpenAI API key from configuration service");
return effectiveConfig.getApiKey();
} else {
log.warn("No valid OpenAI API key found in configuration");
return null;
}
} catch (Exception e) {
log.error("Error retrieving OpenAI API key from configuration", e);
return null;
}
}
}
@@ -0,0 +1,76 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.config;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Configuration class for Large Language Model (LLM) settings.
*/
@Configuration
public class LlmConfig {
@Value("${spring.ai.openai.chat.options.model}")
private String model;
/**
* Create OpenAI API instance with dynamic API key
*/
@Bean
public OpenAiApi openAiApi(DynamicOpenAiApiKey dynamicApiKey) {
return OpenAiApi.builder()
.apiKey(dynamicApiKey)
.build();
}
/**
* Create OpenAI Chat Options with custom settings
*/
@Bean
public OpenAiChatOptions openAiChatOptions() {
return OpenAiChatOptions.builder()
.model(model)
.temperature(0.3)
.build();
}
/**
* Create OpenAI Chat Model with custom API configuration
*/
@Bean
public OpenAiChatModel openAiChatModel(OpenAiApi openAiApi, OpenAiChatOptions openAiChatOptions) {
return OpenAiChatModel.builder()
.openAiApi(openAiApi)
.defaultOptions(openAiChatOptions)
.build();
}
@Bean
public ChatClient openAiChatClient(OpenAiChatModel openAiChatModel) {
return ChatClient.create(openAiChatModel);
}
}
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.ai.config;
package org.apache.hertzbeat.ai.agent.config;
import com.usthe.sureness.subject.SubjectSum;
import org.springframework.core.NamedInheritableThreadLocal;
@@ -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();
}
}
@@ -16,7 +16,7 @@
*/
package org.apache.hertzbeat.ai.config;
package org.apache.hertzbeat.ai.agent.config;
import org.springframework.stereotype.Component;
@@ -179,4 +179,4 @@ public class PromptProvider {
Keep responses focused on monitoring topics and HertzBeat's comprehensive capabilities.
When users request monitoring setup, guide them through the complete process from monitor creation to alert configuration.
""";
}
}
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.ai.controller;
package org.apache.hertzbeat.ai.agent.controller;
import com.usthe.sureness.subject.SubjectSum;
import com.usthe.sureness.util.SurenessContextHolder;
@@ -23,11 +23,11 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.config.McpContextHolder;
import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext;
import org.apache.hertzbeat.ai.pojo.dto.ChatResponseChunk;
import org.apache.hertzbeat.ai.service.ConversationService;
import org.apache.hertzbeat.common.entity.ai.ChatConversation;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatRequestContext;
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatResponseDto;
import org.apache.hertzbeat.ai.agent.pojo.dto.ConversationDto;
import org.apache.hertzbeat.ai.agent.service.ConversationService;
import org.apache.hertzbeat.common.entity.dto.Message;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
@@ -63,6 +63,23 @@ public class ChatController {
this.conversationService = conversationService;
}
/**
* Create a new conversation
*
* @return Created conversation details
*/
@PostMapping(path = "/conversations")
@Operation(summary = "Create a new conversation", description = "Create a new conversation")
public ResponseEntity<Message<ConversationDto>> createConversation() {
try {
ConversationDto conversation = conversationService.createConversation();
return ResponseEntity.ok(Message.success(conversation));
} catch (Exception e) {
log.error("Error creating conversation: ", e);
return ResponseEntity.ok(Message.fail((byte) -1, "Failed to create conversation"));
}
}
/**
* Send a message and get a streaming response with conversation tracking
*
@@ -71,13 +88,14 @@ public class ChatController {
*/
@PostMapping(value = "/stream", produces = TEXT_EVENT_STREAM_VALUE)
@Operation(summary = "Send a chat message with streaming response", description = "Send a message to AI and get a streaming response with conversation tracking")
public Flux<ServerSentEvent<ChatResponseChunk>> streamChat(@Valid @RequestBody ChatRequestContext context) {
public Flux<ServerSentEvent<ChatResponseDto>> streamChat(@Valid @RequestBody ChatRequestContext context) {
try {
// Validate message is not empty
SubjectSum subject = SurenessContextHolder.getBindSubject();
log.info(subject.toString());
McpContextHolder.setSubject(subject);
if (context.getMessage() == null || context.getMessage().trim().isEmpty()) {
ChatResponseChunk errorResponse = ChatResponseChunk.builder()
ChatResponseDto errorResponse = ChatResponseDto.builder()
.conversationId(context.getConversationId())
.response("Error: Message cannot be empty")
.build();
@@ -91,7 +109,7 @@ public class ChatController {
} catch (Exception e) {
log.error("Error in stream chat endpoint: ", e);
ChatResponseChunk errorResponse = ChatResponseChunk.builder()
ChatResponseDto errorResponse = ChatResponseDto.builder()
.conversationId(context.getConversationId())
.response("An error occurred: " + e.getMessage())
.build();
@@ -101,18 +119,6 @@ public class ChatController {
}
}
/**
* Create a new conversation
*
* @return Created conversation details
*/
@PostMapping(path = "/conversations")
@Operation(summary = "Create a new conversation", description = "Create a new conversation")
public ResponseEntity<Message<ChatConversation>> createConversation() {
ChatConversation conversation = conversationService.createConversation();
return ResponseEntity.ok(Message.success(conversation));
}
/**
* Get all conversations
*
@@ -120,9 +126,14 @@ public class ChatController {
*/
@GetMapping(path = "/conversations")
@Operation(summary = "List all conversations", description = "Get a list of all conversations")
public ResponseEntity<Message<List<ChatConversation>>> listConversations() {
List<ChatConversation> conversations = conversationService.getAllConversations();
return ResponseEntity.ok(Message.success(conversations));
public ResponseEntity<Message<List<ConversationDto>>> listConversations() {
try {
List<ConversationDto> conversations = conversationService.getAllConversations();
return ResponseEntity.ok(Message.success(conversations));
} catch (Exception e) {
log.error("Error listing conversations: ", e);
return ResponseEntity.ok(Message.fail((byte) -1, "Failed to retrieve conversations"));
}
}
/**
@@ -133,10 +144,26 @@ public class ChatController {
*/
@GetMapping(path = "/conversations/{conversationId}")
@Operation(summary = "Get conversation history", description = "Get detailed information and message history for a specific conversation")
public ResponseEntity<Message<ChatConversation>> getConversation(
@Parameter(description = "Conversation ID", example = "12345678") @PathVariable(value = "conversationId") Long conversationId) {
ChatConversation conversation = conversationService.getConversation(conversationId);
return ResponseEntity.ok(Message.success(conversation));
public ResponseEntity<Message<ConversationDto>> getConversation(
@Parameter(description = "Conversation ID", example = "conv-12345678") @PathVariable("conversationId") String conversationId) {
try {
// Validate conversation ID
if (conversationId == null || conversationId.trim().isEmpty()) {
return ResponseEntity.ok(Message.fail((byte) -1, "Conversation ID is required"));
}
ConversationDto conversation = conversationService.getConversation(conversationId);
if (conversation == null) {
return ResponseEntity.ok(Message.fail((byte) -1, "Conversation not found: " + conversationId));
}
return ResponseEntity.ok(Message.success(conversation));
} catch (Exception e) {
log.error("Error getting conversation: ", e);
return ResponseEntity.ok(Message.fail((byte) -1, "Failed to retrieve conversation"));
}
}
/**
@@ -148,8 +175,23 @@ public class ChatController {
@DeleteMapping(path = "/conversations/{conversationId}")
@Operation(summary = "Delete conversation", description = "Delete a specific conversation and all its messages")
public ResponseEntity<Message<Void>> deleteConversation(
@Parameter(description = "Conversation ID", example = "2345678") @PathVariable("conversationId") Long conversationId) {
conversationService.deleteConversation(conversationId);
return ResponseEntity.ok(Message.success());
@Parameter(description = "Conversation ID", example = "conv-12345678") @PathVariable("conversationId") String conversationId) {
try {
// Validate conversation ID
if (conversationId == null || conversationId.trim().isEmpty()) {
return ResponseEntity.ok(Message.fail((byte) -1, "Conversation ID is required"));
}
boolean deleted = conversationService.deleteConversation(conversationId);
if (!deleted) {
return ResponseEntity.ok(Message.fail((byte) -1, "Conversation not found: " + conversationId));
}
return ResponseEntity.ok(Message.success("Conversation deleted successfully"));
} catch (Exception e) {
log.error("Error deleting conversation: ", e);
return ResponseEntity.ok(Message.fail((byte) -1, "Failed to delete conversation"));
}
}
}
}
@@ -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,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);
}
@@ -15,74 +15,66 @@
* limitations under the License.
*/
package org.apache.hertzbeat.common.entity.ai;
package org.apache.hertzbeat.ai.agent.entity;
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.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.Lob;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotBlank;
import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* Entity for storing individual chat messages in conversations
* OpenAI Agent Config Entity
*/
@Entity
@Table(name = "hzb_ai_agent_config")
@Data
@Builder
@Entity
@EntityListeners(AuditingEntityListener.class)
@Table(name = "hzb_ai_message", indexes = {
@Index(name = "idx_message_conversation_id", columnList = "conversation_id")
})
@AllArgsConstructor
@NoArgsConstructor
public class ChatMessage {
@Schema(description = "OpenAI Agent config entity")
@EntityListeners(AuditingEntityListener.class)
public class OpenAiConfig {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Schema(title = "conversation id")
private Long conversationId;
@Schema(title = "Config type: openai, primary key", description = "Config type: openai, primary key",
accessMode = READ_WRITE)
@NotBlank(message = "type can not null")
private String type;
@Schema(title = "message content")
@Lob
@NotBlank
@Schema(title = "Config content", description = "Config contentformat json", accessMode = READ_WRITE)
@Column(length = 8192)
private String content;
@Schema(title = "message role: user, system")
private String role;
@Schema(title = "The creator of this record", example = "tom", accessMode = READ_ONLY)
@CreatedBy
private String creator;
@Schema(title = "The modifier of this record", example = "tom", accessMode = READ_ONLY)
@Schema(title = "This record was last modified by", example = "tom", accessMode = READ_ONLY)
@LastModifiedBy
private String modifier;
@Schema(title = "Record create time", example = "1612198922000", accessMode = READ_ONLY)
@Schema(title = "This record creation time (millisecond timestamp)", accessMode = READ_ONLY)
@CreatedDate
private LocalDateTime gmtCreate;
@Schema(title = "Record modify time", example = "1612198444000", accessMode = READ_ONLY)
@Schema(title = "Record the latest modification time (timestamp in milliseconds)", accessMode = READ_ONLY)
@LastModifiedDate
private LocalDateTime gmtUpdate;
}
}
@@ -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);
}
}
}
@@ -16,7 +16,7 @@
*/
package org.apache.hertzbeat.ai.pojo.dto;
package org.apache.hertzbeat.ai.agent.pojo.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
@@ -24,7 +24,6 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import org.apache.hertzbeat.common.entity.ai.ChatMessage;
/**
* Chat request context for AI chat endpoint.
@@ -41,10 +40,10 @@ public class ChatRequestContext {
/**
* Optional conversation ID for context
*/
private Long conversationId;
private String conversationId;
/**
* Conversation history messages for context
*/
private List<ChatMessage> conversationHistory;
}
private List<MessageDto> conversationHistory;
}
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.ai.pojo.dto;
package org.apache.hertzbeat.ai.agent.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
@@ -31,18 +31,17 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Chat response")
public class ChatResponseChunk {
public class ChatResponseDto {
@Schema(description = "Conversation ID", example = "123")
private Long conversationId;
@Schema(description = "Conversation ID", example = "conv-123")
private String conversationId;
@Schema(description = "AI response message", example = "Here are your monitors...")
private String response;
@Schema(description = "User message ID", example = "123")
private Long userMessageId;
@Schema(description = "Assistant message ID", example = "123")
private Long assistantMessageId;
@Schema(description = "User message ID", example = "msg-user-123")
private String userMessageId;
}
@Schema(description = "Assistant message ID", example = "msg-assistant-123")
private String assistantMessageId;
}
@@ -0,0 +1,53 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.List;
/**
* Conversation DTO for AI chat conversations.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Conversation information")
public class ConversationDto {
@Schema(description = "Conversation ID", example = "conv-123")
private String conversationId;
@Schema(description = "Creation time")
private LocalDateTime createdAt;
@Schema(description = "Last updated time")
private LocalDateTime updatedAt;
@Schema(description = "Messages in this conversation")
private List<MessageDto> messages;
@Schema(description = "Message count")
private Integer messageCount;
}
@@ -0,0 +1,87 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE;
/**
* Hierarchical structure
* eg: Monitoring Type metrics Information Hierarchy Relationship
*/
@AllArgsConstructor
@NoArgsConstructor
@Data
@Schema(description = "Monitor Hierarchy")
public class Hierarchy {
/**
* Category value
*/
@Schema(description = "Category Value", example = "os", accessMode = READ_WRITE)
String category;
/**
* Attribute value
*/
@Schema(description = "Attribute value", example = "linux", accessMode = READ_WRITE)
String value;
/**
* Attribute internationalization tag
*/
@Schema(description = "Attribute internationalization tag", example = "Linux system", accessMode = READ_WRITE)
String label;
/**
* Is it a leaf node
*/
@Schema(description = "Is it a leaf node", example = "true", accessMode = READ_WRITE)
Boolean isLeaf = false;
/**
* Is hide this app type in main menus layout
*/
@Schema(description = "Is hide this app in main menus layout, only for app type, default true.", example = "true")
Boolean hide = true;
/**
* For leaf metric
* metric type 0-number: number 1-string: string
*/
@Schema(description = "metric type 0-number: number 1-string: string")
private Byte type;
/**
* metric unit
*/
@Schema(description = "metric unit")
private String unit;
/**
* Next level of association
*/
@Schema(description = "Next Hierarchy", accessMode = READ_WRITE)
private List<Hierarchy> children;
}
@@ -0,0 +1,52 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* Message DTO for chat messages.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Chat message")
public class MessageDto {
@Schema(description = "Message ID", example = "msg-123")
private String messageId;
@Schema(description = "Conversation ID", example = "conv-123")
private String conversationId;
@Schema(description = "Message content", example = "List all monitors")
private String content;
@Schema(description = "Message role", example = "user", allowableValues = {"user", "assistant"})
private String role;
@Schema(description = "Message timestamp")
private LocalDateTime timestamp;
}
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.common.entity.dto;
package org.apache.hertzbeat.ai.agent.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
@@ -24,27 +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 = "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-...")
@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;
/**
* 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;
}
}
@@ -16,9 +16,9 @@
*/
package org.apache.hertzbeat.ai.service;
package org.apache.hertzbeat.ai.agent.service;
import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext;
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatRequestContext;
import reactor.core.publisher.Flux;
/**
@@ -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();
}
}
@@ -16,10 +16,10 @@
*/
package org.apache.hertzbeat.ai.service;
package org.apache.hertzbeat.ai.agent.service;
import org.apache.hertzbeat.ai.pojo.dto.ChatResponseChunk;
import org.apache.hertzbeat.common.entity.ai.ChatConversation;
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatResponseDto;
import org.apache.hertzbeat.ai.agent.pojo.dto.ConversationDto;
import org.springframework.http.codec.ServerSentEvent;
import reactor.core.publisher.Flux;
@@ -30,6 +30,13 @@ import java.util.List;
*/
public interface ConversationService {
/**
* Create a new conversation
*
* @return Created conversation data
*/
ConversationDto createConversation();
/**
* Send a message and receive a streaming response
*
@@ -37,14 +44,8 @@ public interface ConversationService {
* @param conversationId Optional conversation ID for continuing a chat
* @return Flux of ServerSentEvent for streaming the response
*/
Flux<ServerSentEvent<ChatResponseChunk>> streamChat(String message, Long conversationId);
Flux<ServerSentEvent<ChatResponseDto>> streamChat(String message, String conversationId);
/**
* Create a new conversation
*
* @return Created conversation data
*/
ChatConversation createConversation();
/**
* Get conversation history for a specific conversation
@@ -52,19 +53,28 @@ public interface ConversationService {
* @param conversationId Conversation ID
* @return Conversation data including messages
*/
ChatConversation getConversation(Long conversationId);
ConversationDto getConversation(String conversationId);
/**
* Get all conversations for the current user
*
* @return List of conversations
*/
List<ChatConversation> getAllConversations();
List<ConversationDto> getAllConversations();
/**
* Delete a conversation
*
* @param conversationId Conversation ID to delete
* @return true if deleted, false if conversation not found
*/
void deleteConversation(Long conversationId);
}
boolean deleteConversation(String conversationId);
/**
* Check if a conversation exists
*
* @param conversationId Conversation ID to check
* @return true if conversation exists, false otherwise
*/
boolean conversationExists(String conversationId);
}
@@ -16,10 +16,10 @@
*/
package org.apache.hertzbeat.ai.service;
package org.apache.hertzbeat.ai.agent.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.hertzbeat.ai.config.CustomSseServerTransport;
import org.apache.hertzbeat.ai.agent.config.CustomSseServerTransport;
import org.springframework.ai.mcp.server.autoconfigure.McpServerProperties;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.web.servlet.function.RouterFunction;
@@ -0,0 +1,100 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.service;
import org.apache.hertzbeat.ai.agent.event.OpenAiConfigChangeEvent;
import org.apache.hertzbeat.ai.agent.pojo.dto.OpenAiConfigDto;
/**
* OpenAI Configuration Service
* Consolidated service for OpenAI configuration, validation, and client factory management
*/
public interface OpenAiConfigService {
/**
* Save OpenAI configuration
* @param config OpenAI configuration
*/
void saveConfig(OpenAiConfigDto config);
/**
* Get OpenAI configuration
* @return OpenAI configuration
*/
OpenAiConfigDto getConfig();
/**
* Check if OpenAI is properly configured
* @return true if configured and enabled
*/
boolean isConfigured();
/**
* Get effective OpenAI configuration (DB first, then YAML fallback)
* @return effective configuration or null if not configured
*/
OpenAiConfigDto getEffectiveConfig();
/**
* Validate OpenAI API key by calling the OpenAI API
* @param apiKey the API key to validate
* @return validation result with success status and message
*/
ValidationResult validateApiKey(String apiKey);
/**
* Force reload of OpenAI configuration cache
* This method is typically called when configuration changes
*/
void reloadConfig();
/**
* Handle OpenAI configuration change events
* @param event OpenAI configuration change event
*/
void onOpenAiConfigChange(OpenAiConfigChangeEvent event);
/**
* Validation result class
*/
class ValidationResult {
private final boolean valid;
private final String message;
private ValidationResult(boolean valid, String message) {
this.valid = valid;
this.message = message;
}
public static ValidationResult success(String message) {
return new ValidationResult(true, message);
}
public static ValidationResult failure(String message) {
return new ValidationResult(false, message);
}
public boolean isValid() {
return valid;
}
public String getMessage() {
return message;
}
}
}
@@ -16,18 +16,14 @@
*/
package org.apache.hertzbeat.ai.service.impl;
package org.apache.hertzbeat.ai.agent.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.config.PromptProvider;
import org.apache.hertzbeat.common.entity.ai.ChatMessage;
import org.apache.hertzbeat.common.entity.dto.ModelProviderConfig;
import org.apache.hertzbeat.ai.service.ChatClientProviderService;
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
import org.apache.hertzbeat.common.entity.manager.GeneralConfig;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.hertzbeat.ai.agent.config.PromptProvider;
import org.apache.hertzbeat.ai.agent.pojo.dto.MessageDto;
import org.apache.hertzbeat.ai.agent.service.ChatClientProviderService;
import org.springframework.stereotype.Service;
import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext;
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatRequestContext;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
@@ -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,33 +45,32 @@ 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) {
return this.chatClient.prompt()
.user(message)
.call()
.content();
}
@Override
public Flux<String> streamChat(ChatRequestContext context) {
try {
// Get the current (potentially refreshed) ChatClient instance
ChatClient chatClient = applicationContext.getBean("openAiChatClient", ChatClient.class);
List<Message> messages = new ArrayList<>();
// Add conversation history if available
if (context.getConversationHistory() != null && !context.getConversationHistory().isEmpty()) {
for (ChatMessage historyMessage : context.getConversationHistory()) {
for (MessageDto historyMessage : context.getConversationHistory()) {
if ("user".equals(historyMessage.getRole())) {
messages.add(new UserMessage(historyMessage.getContent()));
} else if ("assistant".equals(historyMessage.getRole())) {
@@ -89,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)
@@ -103,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.getApiKey() != null;
}
return isConfigured;
}
}
}
@@ -0,0 +1,276 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatRequestContext;
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatResponseDto;
import org.apache.hertzbeat.ai.agent.pojo.dto.ConversationDto;
import org.apache.hertzbeat.ai.agent.pojo.dto.MessageDto;
import org.apache.hertzbeat.ai.agent.service.ChatClientProviderService;
import org.apache.hertzbeat.ai.agent.service.ConversationService;
import org.apache.hertzbeat.ai.agent.service.OpenAiConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* Implementation of the ConversationService interface for managing chat conversations.
*/
@Slf4j
@Service
public class ConversationServiceImpl implements ConversationService {
private final Map<String, Map<String, Object>> conversations = new ConcurrentHashMap<>();
private final Map<String, List<Map<String, Object>>> conversationMessages = new ConcurrentHashMap<>();
@Autowired
private ChatClientProviderService chatClientProviderService;
@Autowired
private OpenAiConfigService openAiConfigService;
@Override
public ConversationDto createConversation() {
String conversationId = createNewConversation();
return getConversation(conversationId);
}
@Override
public Flux<ServerSentEvent<ChatResponseDto>> streamChat(String message, String conversationId) {
// Validate conversation exists
if (!conversationExists(conversationId)) {
ChatResponseDto errorResponse = ChatResponseDto.builder()
.conversationId(conversationId)
.response("Error: Conversation not found: " + conversationId)
.build();
return Flux.just(ServerSentEvent.builder(errorResponse)
.event("error")
.build());
}
// Check if OpenAI is properly configured
if (!openAiConfigService.isConfigured()) {
ChatResponseDto errorResponse = ChatResponseDto.builder()
.conversationId(conversationId)
.response("OpenAI is not configured. Please configure your OpenAI API key in the settings or application.yml file.")
.build();
return Flux.just(ServerSentEvent.builder(errorResponse)
.event("error")
.build());
}
log.info("Starting streaming conversation: {}", conversationId);
// Add user message to conversation
String userMessageId = addMessageToConversation(conversationId, message, "user");
// Get conversation history for context
List<Map<String, Object>> messagesList = conversationMessages.get(conversationId);
List<MessageDto> conversationHistory = new ArrayList<>();
if (messagesList != null && messagesList.size() > 1) {
// Get all messages except the last one (which is the current user message we just added)
for (int i = 0; i < messagesList.size() - 1; i++) {
Map<String, Object> msgMap = messagesList.get(i);
conversationHistory.add(mapToMessageDto(msgMap));
}
}
ChatRequestContext context = ChatRequestContext.builder()
.message(message)
.conversationId(conversationId)
.conversationHistory(conversationHistory)
.build();
// Stream response from AI service
StringBuilder fullResponse = new StringBuilder();
return chatClientProviderService.streamChat(context)
.map(chunk -> {
fullResponse.append(chunk);
ChatResponseDto responseDto = ChatResponseDto.builder()
.conversationId(conversationId)
.response(chunk)
.userMessageId(userMessageId)
.build();
return ServerSentEvent.builder(responseDto)
.event("message")
.build();
})
.concatWith(Flux.defer(() -> {
// Add the complete AI response to conversation
String assistantMessageId = addMessageToConversation(conversationId, fullResponse.toString(), "assistant");
ChatResponseDto finalResponse = ChatResponseDto.builder()
.conversationId(conversationId)
.response("")
.userMessageId(userMessageId)
.assistantMessageId(assistantMessageId)
.build();
return Flux.just(ServerSentEvent.builder(finalResponse)
.event("complete")
.build());
}))
.doOnComplete(() -> log.info("Streaming completed for conversation: {}", conversationId))
.doOnError(error -> log.error("Error in streaming chat for conversation {}: {}", conversationId, error.getMessage(), error))
.onErrorResume(error -> {
ChatResponseDto errorResponse = ChatResponseDto.builder()
.conversationId(conversationId)
.response("An error occurred: " + error.getMessage())
.userMessageId(userMessageId)
.build();
return Flux.just(ServerSentEvent.builder(errorResponse)
.event("error")
.build());
});
}
@Override
public ConversationDto getConversation(String conversationId) {
if (conversationId == null || conversationId.isEmpty()) {
return null;
}
Map<String, Object> conversation = conversations.get(conversationId);
if (conversation == null) {
return null;
}
List<Map<String, Object>> messagesList = conversationMessages.get(conversationId);
List<MessageDto> messages = messagesList != null
? messagesList.stream().map(this::mapToMessageDto).collect(Collectors.toList()) :
new ArrayList<>();
return ConversationDto.builder()
.conversationId((String) conversation.get("conversationId"))
.createdAt((LocalDateTime) conversation.get("createdAt"))
.updatedAt((LocalDateTime) conversation.get("updatedAt"))
.messages(messages)
.build();
}
@Override
public List<ConversationDto> getAllConversations() {
List<ConversationDto> result = new ArrayList<>();
for (Map.Entry<String, Map<String, Object>> entry : conversations.entrySet()) {
Map<String, Object> conv = entry.getValue();
List<Map<String, Object>> messages = conversationMessages.get(entry.getKey());
ConversationDto dto = ConversationDto.builder()
.conversationId((String) conv.get("conversationId"))
.createdAt((LocalDateTime) conv.get("createdAt"))
.updatedAt((LocalDateTime) conv.get("updatedAt"))
.messages(new ArrayList<>()) // Don't include messages in list view for performance
.build();
result.add(dto);
}
result.sort((a, b) -> b.getUpdatedAt().compareTo(a.getUpdatedAt()));
return result;
}
@Override
public boolean deleteConversation(String conversationId) {
if (conversationId == null || conversationId.isEmpty()) {
return false;
}
boolean existed = conversations.containsKey(conversationId);
if (existed) {
conversations.remove(conversationId);
conversationMessages.remove(conversationId);
log.info("Deleted conversation: {}", conversationId);
}
return existed;
}
@Override
public boolean conversationExists(String conversationId) {
return conversationId != null && !conversationId.isEmpty() && conversations.containsKey(conversationId);
}
private String createNewConversation() {
String conversationId = "conv-" + UUID.randomUUID().toString().substring(0, 8);
LocalDateTime now = LocalDateTime.now();
Map<String, Object> conversation = new HashMap<>();
conversation.put("conversationId", conversationId);
conversation.put("createdAt", now);
conversation.put("updatedAt", now);
conversations.put(conversationId, conversation);
conversationMessages.put(conversationId, new ArrayList<>());
log.info("Created new conversation: {}", conversationId);
return conversationId;
}
private MessageDto mapToMessageDto(Map<String, Object> messageMap) {
return MessageDto.builder()
.messageId((String) messageMap.get("messageId"))
.conversationId((String) messageMap.get("conversationId"))
.content((String) messageMap.get("content"))
.role((String) messageMap.get("role"))
.timestamp((LocalDateTime) messageMap.get("timestamp"))
.build();
}
private String addMessageToConversation(String conversationId, String content, String role) {
List<Map<String, Object>> messages = conversationMessages.computeIfAbsent(conversationId, k -> new ArrayList<>());
String messageId = "msg-" + UUID.randomUUID().toString().substring(0, 8);
Map<String, Object> message = new HashMap<>();
message.put("messageId", messageId);
message.put("conversationId", conversationId);
message.put("content", content);
message.put("role", role);
message.put("timestamp", LocalDateTime.now());
messages.add(message);
// Update conversation timestamp
Map<String, Object> conversation = conversations.get(conversationId);
if (conversation != null) {
conversation.put("updatedAt", LocalDateTime.now());
// Auto-generate title from first user message
if ("user".equals(role) && messages.stream().filter(m -> "user".equals(m.get("role"))).count() == 1) {
String title = content.length() > 30 ? content.substring(0, 27) + "..." : content;
conversation.put("title", title);
}
}
return messageId;
}
}
@@ -16,14 +16,14 @@
*/
package org.apache.hertzbeat.ai.service.impl;
package org.apache.hertzbeat.ai.agent.service.impl;
import org.apache.hertzbeat.ai.config.CustomSseServerTransport;
import org.apache.hertzbeat.ai.service.McpServerService;
import org.apache.hertzbeat.ai.tools.AlertDefineTools;
import org.apache.hertzbeat.ai.tools.AlertTools;
import org.apache.hertzbeat.ai.tools.MetricsTools;
import org.apache.hertzbeat.ai.tools.MonitorTools;
import org.apache.hertzbeat.ai.agent.config.CustomSseServerTransport;
import org.apache.hertzbeat.ai.agent.service.McpServerService;
import org.apache.hertzbeat.ai.agent.tools.AlertDefineTools;
import org.apache.hertzbeat.ai.agent.tools.AlertTools;
import org.apache.hertzbeat.ai.agent.tools.MetricsTools;
import org.apache.hertzbeat.ai.agent.tools.MonitorTools;
import org.springframework.ai.mcp.server.autoconfigure.McpServerProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
@@ -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();
}
}
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.ai.tools;
package org.apache.hertzbeat.ai.agent.tools;
/**
* Tools for alert definition and threshold configuration operations
@@ -88,4 +88,4 @@ public interface AlertDefineTools {
* @return Result message indicating success or failure
*/
String bindMonitorsToAlertRule(Long ruleId, String monitorIds);
}
}
@@ -16,7 +16,7 @@
*/
package org.apache.hertzbeat.ai.tools;
package org.apache.hertzbeat.ai.agent.tools;
/**
* Tools for alert operations and alarm data queries
@@ -16,7 +16,7 @@
*/
package org.apache.hertzbeat.ai.tools;
package org.apache.hertzbeat.ai.agent.tools;
/**
* Tools for metrics data operations and queries
@@ -33,17 +33,16 @@ public interface MetricsTools {
/**
* Get historical metrics data for a monitor
*
* @param instance Instance identifier (e.g., "ip:port", "ip", or "domain")
* @param app Monitor type (e.g., "linux", "mysql", "http")
* @param metrics Metrics name (e.g., "system", "cpu", "memory")
* @param metric Specific metric field (e.g., "usage", "used", "available")
* @param label Label filter for specific instances
* @param history Time range (e.g., "1h", "6h", "24h", "7d")
* @param monitorId Monitor ID
* @param app Monitor type (e.g., "linux", "mysql", "http")
* @param metrics Metrics name (e.g., "system", "cpu", "memory")
* @param metric Specific metric field (e.g., "usage", "used", "available")
* @param label Label filter for specific instances
* @param history Time range (e.g., "1h", "6h", "24h", "7d")
* @param interval Whether to aggregate data with intervals
* @return Historical metrics data formatted for display
*/
String getHistoricalMetrics(String instance, String app, String metrics, String metric, String label, String history, Boolean interval);
String getHistoricalMetrics(Long monitorId, String app, String metrics, String metric, String label, String history, Boolean interval);
/**
* Check warehouse storage server status
@@ -16,7 +16,7 @@
*/
package org.apache.hertzbeat.ai.tools;
package org.apache.hertzbeat.ai.agent.tools;
import java.util.List;
@@ -94,4 +94,4 @@ public interface MonitorTools {
* @return Formatted string with parameter definitions including field names, types, and requirements
*/
String getMonitorAdditionalParams(String app);
}
}
@@ -15,20 +15,19 @@
* limitations under the License.
*/
package org.apache.hertzbeat.ai.tools.impl;
package org.apache.hertzbeat.ai.agent.tools.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.config.McpContextHolder;
import org.apache.hertzbeat.alert.service.AlertDefineService;
import org.apache.hertzbeat.manager.service.AppService;
import org.apache.hertzbeat.ai.tools.AlertDefineTools;
import org.apache.hertzbeat.ai.utils.UtilityClass;
import org.apache.hertzbeat.ai.agent.adapters.AlertDefineServiceAdapter;
import org.apache.hertzbeat.ai.agent.pojo.dto.Hierarchy;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.apache.hertzbeat.ai.agent.tools.AlertDefineTools;
import org.apache.hertzbeat.ai.agent.utils.UtilityClass;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.manager.pojo.dto.Hierarchy;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.beans.factory.annotation.Autowired;
@@ -47,9 +46,7 @@ import java.util.Map;
@Service
public class AlertDefineToolsImpl implements AlertDefineTools {
@Autowired
private AlertDefineService alertDefineService;
@Autowired
private AppService appService;
private AlertDefineServiceAdapter alertDefineServiceAdapter;
@Override
@@ -243,19 +240,17 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
.datasource(datasource)
.enable(enable)
.build();
log.debug("Current security subject for addAlertDefine: {}", subjectSum);
alertDefineService.addAlertDefine(alertDefine);
AlertDefine createdAlertDefine = alertDefineServiceAdapter.addAlertDefine(alertDefine);
// Note: Monitor binding is handled separately via bind_monitors_to_alert_rule tool
String bindingNote = String.format(" (Use bind_monitors_to_alert_rule tool to associate specific monitors)");
log.info("Successfully created alert rule '{}' with ID: {}", name, alertDefine.getId());
log.info("Successfully created alert rule '{}' with ID: {}", name, createdAlertDefine.getId());
StringBuilder response = new StringBuilder();
response.append(String.format("Successfully created %s alert rule '%s' with ID: %d\n",
type, name, alertDefine.getId()));
type, name, createdAlertDefine.getId()));
response.append(String.format("Expression: %s\n", expr));
response.append(String.format("Priority: %d (%s)\n", priority, severityLabel));
response.append(String.format("Trigger after: %d consecutive violations\n", times));
@@ -301,11 +296,8 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
pageSize = 10;
}
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAlertDefines: {}", subjectSum);
Page<AlertDefine> result = alertDefineService.getAlertDefines(null,
search, "gmtCreate", "desc", pageIndex, pageSize);
Page<AlertDefine> result = alertDefineServiceAdapter.getAlertDefines(
search, monitorType, enabled, "gmtCreate", "desc", pageIndex, pageSize);
StringBuilder response = new StringBuilder();
response.append("Found ").append(result.getContent().size())
@@ -353,21 +345,7 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
try {
log.info("Toggling alert rule ID: {} to enabled: {}", ruleId, enabled);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for toggleAlertDefineStatus: {}", subjectSum);
// First get the existing AlertDefine
AlertDefine alertDefine = alertDefineService.getAlertDefine(ruleId);
if (alertDefine == null) {
throw new RuntimeException("AlertDefine with ID " + ruleId + " not found");
}
// Update the enable status
alertDefine.setEnable(enabled);
// Use modifyAlertDefine to save the changes
alertDefineService.modifyAlertDefine(alertDefine);
alertDefineServiceAdapter.toggleAlertDefineStatus(ruleId, enabled);
log.info("Successfully toggled alert rule ID: {} to enabled: {}", ruleId, enabled);
return String.format("Successfully %s alert rule ID: %d",
@@ -391,10 +369,7 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
try {
log.info("Getting alert rule details for ID: {}", ruleId);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAlertDefine: {}", subjectSum);
AlertDefine alertDefine = alertDefineService.getAlertDefine(ruleId);
AlertDefine alertDefine = alertDefineServiceAdapter.getAlertDefine(ruleId);
if (alertDefine == null) {
return "Alert rule with ID " + ruleId + " not found";
}
@@ -466,7 +441,7 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
log.debug("Current subject in get_apps_metrics_hierarchy tool: {}", subjectSum);
List<Hierarchy> hierarchies;
hierarchies = appService.getAppHierarchy(app.trim().toLowerCase(), "en-US");
hierarchies = alertDefineServiceAdapter.getAppHierarchy(app.trim().toLowerCase(), "en-US");
ObjectMapper mapper = new ObjectMapper();
@@ -484,7 +459,10 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
result.put("message", "No hierarchy data available");
}
return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(result);
String jsonResult = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(result);
log.info("Hierarchy JSON: {}", jsonResult);
return jsonResult;
} catch (Exception e) {
log.error("Failed to get apps metrics hierarchy: {}", e.getMessage(), e);
@@ -516,9 +494,7 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
}
// Get the existing alert rule
log.debug("Current security subject for getAlertDefine: {}", subjectSum);
AlertDefine existingRule = alertDefineService.getAlertDefine(ruleId);
AlertDefine existingRule = alertDefineServiceAdapter.getAlertDefine(ruleId);
if (existingRule == null) {
return String.format("Error: Alert rule with ID %d not found", ruleId);
}
@@ -595,9 +571,7 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
// Update the alert rule
existingRule.setExpr(newExpr);
log.debug("Current security subject for modifyAlertDefine: {}", subjectSum);
alertDefineService.modifyAlertDefine(existingRule);
alertDefineServiceAdapter.modifyAlertDefine(existingRule);
log.info("Successfully added monitors {} to existing bindings for alert rule ID: {}", validMonitorIds, ruleId);
return String.format("Successfully added %d new monitor(s) to alert rule ID %d.\nTotal bound monitors: %s\nUpdated expression: %s",
@@ -634,8 +608,7 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
// Update the alert rule
existingRule.setExpr(newExpr);
alertDefineService.modifyAlertDefine(existingRule);
alertDefineServiceAdapter.modifyAlertDefine(existingRule);
log.info("Successfully bound monitors {} to alert rule ID: {}", validMonitorIds, ruleId);
return String.format("Successfully bound %d monitor(s) to alert rule ID %d.\nMonitor IDs: %s\nUpdated expression: %s",
@@ -660,10 +633,7 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
log.debug("Validating hierarchy relationships: app={}, metrics={}, fieldConditions={}", app, metrics, fieldConditions);
// Get hierarchy for the specified app
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAppHierarchy: {}", subjectSum);
List<Hierarchy> hierarchies = appService.getAppHierarchy(app.toLowerCase(), "en-US");
List<Hierarchy> hierarchies = alertDefineServiceAdapter.getAppHierarchy(app.toLowerCase(), "en-US");
if (hierarchies == null || hierarchies.isEmpty()) {
return String.format("Error: App '%s' not found in hierarchy. Please use list_monitor_types to get valid app names.", app);
@@ -692,4 +662,4 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
}
}
}
}
@@ -16,15 +16,15 @@
*/
package org.apache.hertzbeat.ai.tools.impl;
package org.apache.hertzbeat.ai.agent.tools.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.config.McpContextHolder;
import org.apache.hertzbeat.ai.tools.AlertTools;
import org.apache.hertzbeat.ai.utils.UtilityClass;
import org.apache.hertzbeat.ai.agent.adapters.AlertServiceAdapter;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.apache.hertzbeat.ai.agent.tools.AlertTools;
import org.apache.hertzbeat.ai.agent.utils.UtilityClass;
import org.apache.hertzbeat.alert.dto.AlertSummary;
import org.apache.hertzbeat.alert.service.AlertService;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.springframework.beans.factory.annotation.Autowired;
@@ -41,7 +41,7 @@ import org.springframework.stereotype.Service;
@Service
public class AlertToolsImpl implements AlertTools {
@Autowired
private AlertService alertService;
private AlertServiceAdapter alertServiceAdapter;
@Override
@Tool(name = "query_alerts", description = """
@@ -116,7 +116,7 @@ public class AlertToolsImpl implements AlertTools {
// Handle different alert types
if ("single".equalsIgnoreCase(alertType) || "both".equalsIgnoreCase(alertType)) {
Page<SingleAlert> singleResult = alertService.getSingleAlerts(status, search, sort, order, pageIndex, pageSize);
Page<SingleAlert> singleResult = alertServiceAdapter.getSingleAlerts(status, search, sort, order, pageIndex, pageSize);
response.append("SINGLE ALERTS:\n");
response.append("Found ").append(singleResult.getContent().size()).append(" single alerts (Total: ").append(singleResult.getTotalElements()).append("):\n\n");
@@ -154,7 +154,7 @@ public class AlertToolsImpl implements AlertTools {
response.append("\n");
}
Page<GroupAlert> groupResult = alertService.getGroupAlerts(status, search, sort, order, pageIndex, pageSize);
Page<GroupAlert> groupResult = alertServiceAdapter.getGroupAlerts(status, search, sort, order, pageIndex, pageSize);
response.append("GROUP ALERTS:\n");
response.append("Found ").append(groupResult.getContent().size()).append(" group alerts (Total: ").append(groupResult.getTotalElements()).append("):\n\n");
@@ -205,7 +205,7 @@ public class AlertToolsImpl implements AlertTools {
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current subject in get_alerts_summary tool: {}", subjectSum);
AlertSummary summary = alertService.getAlertsSummary();
AlertSummary summary = alertServiceAdapter.getAlertsSummary();
StringBuilder response = new StringBuilder();
response.append("ALERTS SUMMARY\n");
@@ -16,13 +16,14 @@
*/
package org.apache.hertzbeat.ai.tools.impl;
package org.apache.hertzbeat.ai.agent.tools.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.config.McpContextHolder;
import org.apache.hertzbeat.warehouse.service.MetricsDataService;
import org.apache.hertzbeat.ai.tools.MetricsTools;
import org.apache.hertzbeat.ai.agent.adapters.MetricsServiceAdapter;
import org.apache.hertzbeat.ai.agent.adapters.MonitorServiceAdapter;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.apache.hertzbeat.ai.agent.tools.MetricsTools;
import org.apache.hertzbeat.common.entity.dto.Field;
import org.apache.hertzbeat.common.entity.dto.MetricsData;
import org.apache.hertzbeat.common.entity.dto.MetricsHistoryData;
@@ -43,7 +44,9 @@ import java.util.Map;
@Service
public class MetricsToolsImpl implements MetricsTools {
@Autowired
private MetricsDataService metricsDataService;
private MetricsServiceAdapter metricsServiceAdapter;
@Autowired
private MonitorServiceAdapter monitorServiceAdapter;
@Override
@Tool(name = "query_realtime_metrics", description = """
@@ -75,7 +78,7 @@ public class MetricsToolsImpl implements MetricsTools {
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current subject in get_realtime_metrics tool: {}", subjectSum);
MetricsData metricsData = metricsDataService.getMetricsData(monitorId, metrics);
MetricsData metricsData = metricsServiceAdapter.getMetricsData(monitorId, metrics);
if (metricsData == null) {
return String.format("No real-time metrics data found for monitor ID %d and metrics '%s'", monitorId, metrics);
@@ -152,7 +155,7 @@ public class MetricsToolsImpl implements MetricsTools {
Ask user to provide the filters for labels, history and interval aggregation
""")
public String getHistoricalMetrics(
@ToolParam(description = "Instance identifier (e.g., 'ip:port', 'ip', or 'domain')") String instance,
@ToolParam(description = "Monitor ID", required = true) Long monitorId,
@ToolParam(description = "Monitor type (e.g., 'linux', 'mysql', 'http')", required = true) String app,
@ToolParam(description = "Metrics name (e.g., 'target', 'cpu', 'memory')", required = true) String metrics,
@ToolParam(description = "Field Parameter (e.g., 'usage', 'used', 'available')", required = false) String fieldParameter,
@@ -161,7 +164,7 @@ public class MetricsToolsImpl implements MetricsTools {
@ToolParam(description = "Whether to aggregate data with intervals", required = false) Boolean interval) {
try {
log.info("Getting historical metrics for monitor instance {} and metrics {}", instance, metrics);
log.info("Getting historical metrics for monitor {} and metrics {}", monitorId, metrics);
if (history == null || history.trim().isEmpty()) {
history = "24h";
@@ -170,15 +173,15 @@ public class MetricsToolsImpl implements MetricsTools {
interval = true;
}
MetricsHistoryData historyData = metricsDataService.getMetricHistoryData(instance,
app, metrics, fieldParameter, history, interval);
MetricsHistoryData historyData = metricsServiceAdapter.getMetricHistoryData(
monitorId, app, metrics, fieldParameter, label, history, interval);
if (historyData == null) {
return String.format("No historical metrics data found for monitor %s and metrics '%s'", instance, metrics);
return String.format("No historical metrics data found for monitor ID %d and metrics '%s'", monitorId, metrics);
}
StringBuilder response = new StringBuilder();
response.append("HISTORICAL METRICS: ").append(metrics).append(" (Monitor ID: ").append(instance).append(")\n");
response.append("HISTORICAL METRICS: ").append(metrics).append(" (Monitor ID: ").append(monitorId).append(")\n");
response.append("Time Range: ").append(history).append(" | Interval Aggregation: ").append(interval).append("\n");
response.append("=".repeat(60)).append("\n\n");
@@ -232,10 +235,8 @@ public class MetricsToolsImpl implements MetricsTools {
public String getWarehouseStatus() {
try {
log.info("Checking warehouse storage status");
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getWarehouseStorageServerStatus: {}", subjectSum);
Boolean status = metricsDataService.getWarehouseStorageServerStatus();
Boolean status = metricsServiceAdapter.getWarehouseStorageServerStatus();
StringBuilder response = new StringBuilder();
response.append("METRICS WAREHOUSE STATUS\n");
@@ -259,4 +260,4 @@ public class MetricsToolsImpl implements MetricsTools {
}
}
}
}
@@ -15,18 +15,17 @@
* limitations under the License.
*/
package org.apache.hertzbeat.ai.tools.impl;
package org.apache.hertzbeat.ai.agent.tools.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.config.McpContextHolder;
import org.apache.hertzbeat.manager.service.MonitorService;
import org.apache.hertzbeat.manager.service.AppService;
import org.apache.hertzbeat.ai.utils.UtilityClass;
import org.apache.hertzbeat.ai.agent.adapters.MonitorServiceAdapter;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.apache.hertzbeat.ai.agent.utils.UtilityClass;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.apache.hertzbeat.ai.tools.MonitorTools;
import org.apache.hertzbeat.ai.agent.tools.MonitorTools;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.apache.hertzbeat.common.entity.manager.Monitor;
@@ -36,7 +35,6 @@ import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* Implementation of Monitoring Tools functionality
@@ -45,9 +43,7 @@ import java.util.Objects;
@Service
public class MonitorToolsImpl implements MonitorTools {
@Autowired
private MonitorService monitorService;
@Autowired
private AppService appService;
private MonitorServiceAdapter monitorServiceAdapter;
/**
@@ -127,12 +123,8 @@ public class MonitorToolsImpl implements MonitorTools {
includeStats = false;
}
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject: {}", subjectSum);
Page<Monitor> result = monitorService.getMonitors(
ids, app, search, status, sort, order, pageIndex, pageSize, labels);
log.debug("MonitorService.getMonitors result: {}", result);
Page<Monitor> result = monitorServiceAdapter.getMonitors(ids, app, search, status, sort, order, pageIndex, pageSize, labels);
log.debug("MonitorServiceAdapter.getMonitors result: {}", result);
StringBuilder response = new StringBuilder();
response.append("MONITOR QUERY RESULTS\n");
@@ -141,10 +133,10 @@ public class MonitorToolsImpl implements MonitorTools {
// Include statistics if requested
if (includeStats) {
// Get status distribution by calling with different status values
long onlineCount = monitorService.getMonitors(null, app, search, (byte) 1, null, null, 0, 1000, labels).getTotalElements();
long offlineCount = monitorService.getMonitors(null, app, search, (byte) 2, null, null, 0, 1000, labels).getTotalElements();
long unreachableCount = monitorService.getMonitors(null, app, search, (byte) 3, null, null, 0, 1000, labels).getTotalElements();
long pausedCount = monitorService.getMonitors(null, app, search, (byte) 0, null, null, 0, 1000, labels).getTotalElements();
long onlineCount = monitorServiceAdapter.getMonitors(null, app, search, (byte) 1, null, null, 0, 1000, labels).getTotalElements();
long offlineCount = monitorServiceAdapter.getMonitors(null, app, search, (byte) 2, null, null, 0, 1000, labels).getTotalElements();
long unreachableCount = monitorServiceAdapter.getMonitors(null, app, search, (byte) 3, null, null, 0, 1000, labels).getTotalElements();
long pausedCount = monitorServiceAdapter.getMonitors(null, app, search, (byte) 0, null, null, 0, 1000, labels).getTotalElements();
response.append("STATUS OVERVIEW:\n");
response.append("- Online: ").append(onlineCount).append("\n");
@@ -172,7 +164,7 @@ public class MonitorToolsImpl implements MonitorTools {
response.append("ID: ").append(monitor.getId())
.append(" | Name: ").append(monitor.getName())
.append(" | Type: ").append(monitor.getApp())
.append(" | Instance: ").append(monitor.getInstance())
.append(" | Host: ").append(monitor.getHost())
.append(" | Status: ").append(UtilityClass.getStatusText(monitor.getStatus()));
// Add creation date for better context
@@ -258,14 +250,12 @@ public class MonitorToolsImpl implements MonitorTools {
if (intervals == null || intervals < 10) {
intervals = 600;
}
String instance = Objects.nonNull(port) ? host.trim() + ":" + port : host.trim();
// Create Monitor entity
Monitor monitor = Monitor.builder()
.name(name.trim())
.app(app.toLowerCase().trim())
.instance(instance)
.host(host.trim())
.intervals(intervals)
.status((byte) 1)
.type((byte) 0)
@@ -276,10 +266,7 @@ public class MonitorToolsImpl implements MonitorTools {
// Validate that all required parameters for this monitor type are provided
try {
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getMonitorParamDefines: {}", subjectSum);
List<ParamDefine> requiredParams = appService.getAppParamDefines(app.toLowerCase().trim());
List<ParamDefine> requiredParams = monitorServiceAdapter.getMonitorParamDefines(app);
log.info("Checking required parameters for monitor type '{}': {}", app, requiredParams);
List<String> missingParams = new ArrayList<>();
@@ -303,12 +290,8 @@ public class MonitorToolsImpl implements MonitorTools {
log.warn("Could not validate required parameters for monitor type '{}': {}", app, e.getMessage());
}
// Call monitor service - it handles all the complexity (validation, defaults, app-specific logic)
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for addMonitor: {}", subjectSum);
monitorService.addMonitor(monitor, params, null, null);
Long monitorId = monitor.getId();
// Call adapter - it handles all the complexity (validation, defaults, app-specific logic)
Long monitorId = monitorServiceAdapter.addMonitor(monitor, params, null);
log.info("Successfully added monitor '{}' with ID: {}", name, monitorId);
return String.format("Successfully added %s monitor '%s' with ID: %d (Host: %s, Interval: %d seconds)",
@@ -378,14 +361,14 @@ public class MonitorToolsImpl implements MonitorTools {
log.info("Listing available monitor types for language: {}", language);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current subject in list_monitor_types tool: {}", subjectSum);
// Set default language if not provided
if (language == null || language.trim().isEmpty()) {
language = "en-US";
}
// Get available monitor types from app service
Map<String, String> monitorTypes = appService.getI18nApps(language);
// Get available monitor types from adapter
Map<String, String> monitorTypes = monitorServiceAdapter.getAvailableMonitorTypes(language);
if (monitorTypes == null || monitorTypes.isEmpty()) {
return "No monitor types are currently available.";
@@ -433,14 +416,14 @@ public class MonitorToolsImpl implements MonitorTools {
log.info("Getting parameter definitions for monitor type: {}", app);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current subject in get_monitor_param_defines tool: {}", subjectSum);
// Validate required parameter
if (app == null || app.trim().isEmpty()) {
return "Error: Monitor type/application parameter is required";
}
// Get parameter definitions from app service
List<ParamDefine> paramDefines = appService.getAppParamDefines(app.toLowerCase().trim());
// Get parameter definitions from adapter
List<ParamDefine> paramDefines = monitorServiceAdapter.getMonitorParamDefines(app);
if (paramDefines == null || paramDefines.isEmpty()) {
return String.format("No parameter definitions found for monitor type '%s'. "
@@ -15,14 +15,16 @@
* limitations under the License.
*/
package org.apache.hertzbeat.ai.utils;
package org.apache.hertzbeat.ai.agent.utils;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.pojo.dto.Hierarchy;
import java.lang.reflect.Method;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
@@ -31,7 +33,6 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.hertzbeat.manager.pojo.dto.Hierarchy;
/**
* Utility class providing helper methods for field expression validation, parsing,
@@ -226,6 +227,51 @@ public class UtilityClass {
return "VALID";
}
/**
* Helper method to validate operator
*/
public boolean isValidOperator(String operator) {
return operator != null && (operator.equals(">") || operator.equals("<")
|| operator.equals(">=") || operator.equals("<=")
|| operator.equals("==") || operator.equals("!="));
}
/**
* Helper method to validate priority
*/
public boolean isValidPriority(String priority) {
return priority != null && (priority.equalsIgnoreCase("critical")
|| priority.equalsIgnoreCase("warning") || priority.equalsIgnoreCase("info"));
}
/**
* Helper method to build expression
*/
public String buildExpression(String metric, String operator, String threshold) {
return String.format("%s %s %s", metric, operator, threshold);
}
/**
* Helper method to parse existing expression into components
*/
public String[] parseExpression(String expression) {
if (expression == null || expression.trim().isEmpty()) {
return null;
}
// Simple parsing for basic expressions like "metric > value"
String[] operators = {">", "<", ">=", "<=", "==", "!="};
for (String op : operators) {
if (expression.contains(" " + op + " ")) {
String[] parts = expression.split(" " + op + " ");
if (parts.length == 2) {
return new String[]{parts[0].trim(), op, parts[1].trim()};
}
}
}
return null;
}
/**
* Helper method to parse key-value pairs from a string
* Format: "key1:value1, key2:value2, ..."
@@ -427,6 +473,19 @@ public class UtilityClass {
return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
/**
* Parse time range string to milliseconds
*/
public long parseTimeRangeToMillis(String timeRange) {
return switch (timeRange.toLowerCase()) {
case "1h" -> 60 * 60 * 1000L;
case "6h" -> 6 * 60 * 60 * 1000L;
case "24h" -> 24 * 60 * 60 * 1000L;
case "7d" -> 7 * 24 * 60 * 60 * 1000L;
default -> 24 * 60 * 60 * 1000L; // default to 24h
};
}
/**
* Helper method to convert monitor status byte to readable text
* @param status The status byte from monitor
@@ -445,6 +504,143 @@ public class UtilityClass {
};
}
/**
* Helper method to get metrics name for a metric type
*/
public String getMetricsNameForType(String metricType) {
return switch (metricType.toLowerCase()) {
case "cpu" -> "cpu";
case "memory" -> "memory";
case "disk" -> "disk";
case "network" -> "network";
default -> "system";
};
}
/**
* Helper method to check if a field represents usage for a metric type
*/
public boolean isUsageField(String field, String metricType) {
if (field == null) return false;
String fieldLower = field.toLowerCase();
String typeLower = metricType.toLowerCase();
return fieldLower.contains("usage")
|| fieldLower.contains("percent")
|| fieldLower.contains("util")
|| (typeLower.equals("cpu") && (fieldLower.contains("cpu") || fieldLower.contains("idle")))
|| (typeLower.equals("memory") && fieldLower.contains("memory"))
|| (typeLower.equals("disk") && fieldLower.contains("disk"));
}
/**
* Convert manager module Hierarchy objects to ai-agent module Hierarchy objects
* This handles the cross-module DTO conversion to avoid ClassCastException
*/
public List<Hierarchy> convertToAgentHierarchies(List<?> managerHierarchies) {
List<Hierarchy> agentHierarchies = new ArrayList<>();
for (Object managerHierarchy : managerHierarchies) {
Hierarchy agentHierarchy = convertToAgentHierarchy(managerHierarchy);
agentHierarchies.add(agentHierarchy);
}
return agentHierarchies;
}
/**
* Convert a single manager Hierarchy object to ai-agent Hierarchy object using reflection
*/
public Hierarchy convertToAgentHierarchy(Object managerHierarchy) {
try {
Hierarchy agentHierarchy = new Hierarchy();
// Use reflection to copy properties from manager DTO to agent DTO
Class<?> managerClass = managerHierarchy.getClass();
// Copy basic properties
agentHierarchy.setCategory(getStringField(managerHierarchy, managerClass, "category"));
agentHierarchy.setValue(getStringField(managerHierarchy, managerClass, "value"));
agentHierarchy.setLabel(getStringField(managerHierarchy, managerClass, "label"));
agentHierarchy.setIsLeaf(getBooleanField(managerHierarchy, managerClass, "isLeaf"));
agentHierarchy.setHide(getBooleanField(managerHierarchy, managerClass, "hide"));
agentHierarchy.setType(getByteField(managerHierarchy, managerClass, "type"));
agentHierarchy.setUnit(getStringField(managerHierarchy, managerClass, "unit"));
// Handle children recursively
List<?> managerChildren = getListField(managerHierarchy, managerClass, "children");
if (managerChildren != null && !managerChildren.isEmpty()) {
List<Hierarchy> agentChildren = convertToAgentHierarchies(managerChildren);
agentHierarchy.setChildren(agentChildren);
}
return agentHierarchy;
} catch (Exception e) {
log.error("Failed to convert manager hierarchy to agent hierarchy: {}", e.getMessage(), e);
throw new RuntimeException("Failed to convert hierarchy", e);
}
}
public String getStringField(Object obj, Class<?> clazz, String fieldName) {
try {
Method getter = clazz.getMethod("get" + capitalize(fieldName));
Object value = getter.invoke(obj);
return value != null ? value.toString() : null;
} catch (Exception e) {
log.debug("Could not get string field '{}': {}", fieldName, e.getMessage());
return null;
}
}
public Boolean getBooleanField(Object obj, Class<?> clazz, String fieldName) {
try {
Method getter = clazz.getMethod("get" + capitalize(fieldName));
Object value = getter.invoke(obj);
return value instanceof Boolean ? (Boolean) value : null;
} catch (Exception e) {
try {
// Try alternative getter pattern for boolean fields
Method isGetter = clazz.getMethod("is" + capitalize(fieldName));
Object value = isGetter.invoke(obj);
return value instanceof Boolean ? (Boolean) value : null;
} catch (Exception e2) {
log.debug("Could not get boolean field '{}': {}", fieldName, e.getMessage());
return null;
}
}
}
public Byte getByteField(Object obj, Class<?> clazz, String fieldName) {
try {
Method getter = clazz.getMethod("get" + capitalize(fieldName));
Object value = getter.invoke(obj);
return value instanceof Byte ? (Byte) value : null;
} catch (Exception e) {
log.debug("Could not get byte field '{}': {}", fieldName, e.getMessage());
return null;
}
}
public List<?> getListField(Object obj, Class<?> clazz, String fieldName) {
try {
Method getter = clazz.getMethod("get" + capitalize(fieldName));
Object value = getter.invoke(obj);
return value instanceof List ? (List<?>) value : null;
} catch (Exception e) {
log.debug("Could not get list field '{}': {}", fieldName, e.getMessage());
return null;
}
}
public String capitalize(String str) {
if (str == null || str.isEmpty()) {
return str;
}
return str.substring(0, 1).toUpperCase() + str.substring(1);
}
/**
* Extract existing monitor IDs from the alert expression
@@ -1,156 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.config;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.support.event.AiProviderConfigChangeEvent;
import org.apache.hertzbeat.common.entity.dto.ModelProviderConfig;
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
import org.apache.hertzbeat.common.entity.manager.GeneralConfig;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
/**
* Configuration class for Large Language Model (LLM) settings.
*/
@Configuration
@Slf4j
public class LlmConfig {
private final GeneralConfigDao generalConfigDao;
private ApplicationContext applicationContext;
public LlmConfig(GeneralConfigDao generalConfigDao, ApplicationContext applicationContext) {
this.generalConfigDao = generalConfigDao;
this.applicationContext = applicationContext;
}
/**
* Create ChatClient bean with all dependencies created internally
*/
@Bean
public ChatClient openAiChatClient() {
return createChatClient();
}
/**
* Create ChatClient with all necessary components
*/
private ChatClient createChatClient() {
GeneralConfig providerConfig = generalConfigDao.findByType("provider");
if (providerConfig == null || providerConfig.getContent() == null) {
log.warn("LLM Provider is not set, ChatClient bean will not be created");
return null;
}
ModelProviderConfig modelProviderConfig = JsonUtil.fromJson(providerConfig.getContent(), ModelProviderConfig.class);
if (modelProviderConfig == null || modelProviderConfig.getApiKey() == null) {
log.warn("LLM Provider configuration is incomplete, ChatClient bean will not be created");
return null;
}
if (modelProviderConfig.getBaseUrl() == null) {
if ("openai".equals(modelProviderConfig.getCode())) {
modelProviderConfig.setBaseUrl("https://api.openai.com/v1");
} else if ("zhipu".equals(modelProviderConfig.getCode())) {
modelProviderConfig.setBaseUrl("https://open.bigmodel.cn/api/paas/v4");
} else if ("zai".equals(modelProviderConfig.getCode())) {
modelProviderConfig.setBaseUrl("https://api.z.ai/api/paas/v4");
} else {
modelProviderConfig.setBaseUrl("https://api.openai.com/v1");
}
}
if (modelProviderConfig.getModel() == null) {
if ("openai".equals(modelProviderConfig.getCode())) {
modelProviderConfig.setModel("gpt-5");
} else if ("zhipu".equals(modelProviderConfig.getCode())) {
modelProviderConfig.setModel("glm-4.6");
} else if ("zai".equals(modelProviderConfig.getCode())) {
modelProviderConfig.setModel("glm-4.6");
} else {
modelProviderConfig.setModel("gpt-5");
}
}
OpenAiApi.Builder builder = new OpenAiApi.Builder();
builder.baseUrl(modelProviderConfig.getBaseUrl());
builder.apiKey(modelProviderConfig.getApiKey());
builder.completionsPath("/chat/completions");
// Create Chat Options
OpenAiChatOptions openAiChatOptions = OpenAiChatOptions.builder()
.model(modelProviderConfig.getModel())
.temperature(0.3)
.build();
// Create Chat Model
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
.openAiApi(builder.build())
.defaultOptions(openAiChatOptions)
.build();
// Create and return ChatClient
return ChatClient.create(openAiChatModel);
}
/**
* AI configuration change event listener
* Uses ApplicationContext to unregister and re-register the ChatClient bean
*/
@EventListener(AiProviderConfigChangeEvent.class)
public void onAiProviderConfigChange(AiProviderConfigChangeEvent event) {
log.info("Provider configuration change event received, refreshing ChatClient bean");
try {
ConfigurableApplicationContext configurableContext = (ConfigurableApplicationContext) applicationContext;
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) configurableContext.getBeanFactory();
// Remove the existing ChatClient bean
if (beanFactory.containsSingleton("openAiChatClient")) {
beanFactory.destroySingleton("openAiChatClient");
log.info("Existing ChatClient bean destroyed");
}
// Create new ChatClient with updated configuration
ChatClient newChatClient = createChatClient();
// Register the new ChatClient bean
beanFactory.registerSingleton("openAiChatClient", newChatClient);
log.info("ChatClient bean refreshed successfully with new AI provider configuration");
} catch (Exception e) {
log.error("Failed to refresh ChatClient bean after configuration change", e);
}
}
}
@@ -1,160 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.dao.ChatConversationDao;
import org.apache.hertzbeat.ai.dao.ChatMessageDao;
import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext;
import org.apache.hertzbeat.ai.pojo.dto.ChatResponseChunk;
import org.apache.hertzbeat.ai.service.ChatClientProviderService;
import org.apache.hertzbeat.ai.service.ConversationService;
import org.apache.hertzbeat.common.entity.ai.ChatConversation;
import org.apache.hertzbeat.common.entity.ai.ChatMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import reactor.core.publisher.Flux;
import java.util.List;
import java.util.UUID;
/**
* Implementation of the ConversationService interface for managing chat conversations.
*/
@Slf4j
@Service
public class ConversationServiceImpl implements ConversationService {
@Autowired
private ChatConversationDao conversationDao;
@Autowired
private ChatMessageDao messageDao;
@Autowired
private ChatClientProviderService chatClientProviderService;
@Override
public Flux<ServerSentEvent<ChatResponseChunk>> streamChat(String message, Long conversationId) {
// Check if provider is properly configured
if (!chatClientProviderService.isConfigured()) {
ChatResponseChunk errorResponse = ChatResponseChunk.builder()
.conversationId(conversationId)
.response("Provider is not configured. Please configure your AI Provider.")
.build();
return Flux.just(ServerSentEvent.builder(errorResponse)
.event("error")
.build());
}
log.info("Starting streaming conversation: {}", conversationId);
// Add user message to conversation
ChatMessage chatMessage = ChatMessage.builder().conversationId(conversationId)
.content(message).role("user").build();
chatMessage = messageDao.save(chatMessage);
ChatConversation conversation = conversationDao.getReferenceById(conversationId);
if (conversation.getTitle().startsWith("conversation")) {
// Auto-generate title from first user message
String title = message.length() > 30 ? message.substring(0, 27) + "..." : message;
conversation.setTitle(title);
conversationDao.save(conversation);
}
ChatRequestContext context = ChatRequestContext.builder()
.message(message)
.conversationId(conversationId)
.conversationHistory(CollectionUtils.isEmpty(conversation.getMessages()) ? null
: conversation.getMessages().subList(0, conversation.getMessages().size() - 1))
.build();
// Stream response from AI service
StringBuilder fullResponse = new StringBuilder();
ChatMessage finalChatMessage = chatMessage;
return chatClientProviderService.streamChat(context)
.map(chunk -> {
fullResponse.append(chunk);
ChatResponseChunk responseChunk = ChatResponseChunk.builder()
.conversationId(conversationId)
.userMessageId(finalChatMessage.getId())
.response(chunk)
.build();
return ServerSentEvent.builder(responseChunk)
.event("message")
.build();
})
.concatWith(Flux.defer(() -> {
// Add the complete AI response to conversation
ChatMessage assistantMessage = ChatMessage.builder().conversationId(conversationId)
.content(fullResponse.toString()).role("assistant").build();
assistantMessage = messageDao.save(assistantMessage);
ChatResponseChunk finalResponse = ChatResponseChunk.builder()
.conversationId(conversationId)
.response("")
.assistantMessageId(assistantMessage.getId())
.build();
return Flux.just(ServerSentEvent.builder(finalResponse)
.event("complete")
.build());
}))
.doOnComplete(() -> log.info("Streaming completed for conversation: {}", conversationId))
.doOnError(error -> log.error("Error in streaming chat for conversation {}: {}", conversationId, error.getMessage(), error))
.onErrorResume(error -> {
ChatResponseChunk errorResponse = ChatResponseChunk.builder()
.conversationId(conversationId)
.response("An error occurred: " + error.getMessage())
.userMessageId(finalChatMessage.getId())
.build();
return Flux.just(ServerSentEvent.builder(errorResponse)
.event("error")
.build());
});
}
@Override
public ChatConversation createConversation() {
ChatConversation conversation = new ChatConversation();
conversation.setTitle("conversation-" + UUID.randomUUID().toString().substring(0, 4));
return conversationDao.save(conversation);
}
@Override
public ChatConversation getConversation(Long conversationId) {
if (conversationId == null) {
return null;
}
return conversationDao.getReferenceById(conversationId);
}
@Override
public List<ChatConversation> getAllConversations() {
return conversationDao.findAll(Sort.by(Sort.Direction.DESC, "id"));
}
@Override
public void deleteConversation(Long conversationId) {
conversationDao.deleteById(conversationId);
}
}
@@ -35,12 +35,10 @@ public class AlerterWorkerPool {
private ThreadPoolExecutor workerExecutor;
private ThreadPoolExecutor notifyExecutor;
private ThreadPoolExecutor logWorkerExecutor;
public AlerterWorkerPool() {
initWorkExecutor();
initNotifyExecutor();
initLogWorkerExecutor();
}
private void initWorkExecutor() {
@@ -79,21 +77,6 @@ public class AlerterWorkerPool {
new ThreadPoolExecutor.AbortPolicy());
}
private void initLogWorkerExecutor() {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("Alerter logWorkerExecutor has uncaughtException.");
log.error(throwable.getMessage(), throwable);
})
.setDaemon(true)
.setNameFormat("log-worker-%d")
.build();
logWorkerExecutor = new ThreadPoolExecutor(10, 10, 10, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
threadFactory,
new ThreadPoolExecutor.AbortPolicy());
}
/**
* Run the alerter task
* @param runnable task
@@ -113,13 +96,4 @@ public class AlerterWorkerPool {
notifyExecutor.execute(runnable);
}
/**
* Executes the given runnable task using the logWorkerExecutor.
*
* @param runnable the task to be executed
* @throws RejectedExecutionException if the task cannot be accepted for execution
*/
public void executeLogJob(Runnable runnable) throws RejectedExecutionException {
logWorkerExecutor.execute(runnable);
}
}
@@ -1,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;
}
}
@@ -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);
}
}
}
@@ -15,13 +15,10 @@
* limitations under the License.
*/
package org.apache.hertzbeat.alert.calculate.periodic;
package org.apache.hertzbeat.alert.calculate;
import static org.apache.hertzbeat.common.constants.CommonConstants.LOG_ALERT_THRESHOLD_TYPE_PERIODIC;
import static org.apache.hertzbeat.common.constants.CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_PERIODIC;
import static org.apache.hertzbeat.common.constants.CommonConstants.ALERT_THRESHOLD_TYPE_PERIODIC;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -42,15 +39,13 @@ import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
@Component
public class PeriodicAlertRuleScheduler implements CommandLineRunner {
private final MetricsPeriodicAlertCalculator metricsCalculator;
private final LogPeriodicAlertCalculator logCalculator;
private final PeriodicAlertCalculator calculator;
private final AlertDefineDao alertDefineDao;
private final ScheduledExecutorService scheduledExecutor;
private final Map<Long, ScheduledFuture<?>> scheduledFutures;
public PeriodicAlertRuleScheduler(MetricsPeriodicAlertCalculator metricsCalculator, LogPeriodicAlertCalculator logCalculator, AlertDefineDao alertDefineDao) {
this.metricsCalculator = metricsCalculator;
this.logCalculator = logCalculator;
public PeriodicAlertRuleScheduler(PeriodicAlertCalculator calculator, AlertDefineDao alertDefineDao) {
this.calculator = calculator;
this.alertDefineDao = alertDefineDao;
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
@@ -81,14 +76,9 @@ public class PeriodicAlertRuleScheduler implements CommandLineRunner {
return;
}
cancelSchedule(rule.getId());
if (rule.getType().equals(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC)
|| rule.getType().equals(LOG_ALERT_THRESHOLD_TYPE_PERIODIC)) {
if (rule.getType().equals(ALERT_THRESHOLD_TYPE_PERIODIC)) {
ScheduledFuture<?> future = scheduledExecutor.scheduleAtFixedRate(() -> {
if (rule.getType().equals(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC)) {
metricsCalculator.calculate(rule);
} else if (rule.getType().equals(LOG_ALERT_THRESHOLD_TYPE_PERIODIC)) {
logCalculator.calculate(rule);
}
calculator.calculate(rule);
}, 0, rule.getPeriod(), java.util.concurrent.TimeUnit.SECONDS);
scheduledFutures.put(rule.getId(), future);
}
@@ -97,11 +87,7 @@ public class PeriodicAlertRuleScheduler implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
log.info("Starting periodic alert rule scheduler...");
List<AlertDefine> metricsPeriodicRules = alertDefineDao.findAlertDefinesByTypeAndEnableTrue(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC);
List<AlertDefine> logPeriodicRules = alertDefineDao.findAlertDefinesByTypeAndEnableTrue(LOG_ALERT_THRESHOLD_TYPE_PERIODIC);
List<AlertDefine> periodicRules = new ArrayList<>(metricsPeriodicRules.size() + logPeriodicRules.size());
periodicRules.addAll(metricsPeriodicRules);
periodicRules.addAll(logPeriodicRules);
List<AlertDefine> periodicRules = alertDefineDao.findAlertDefinesByTypeAndEnableTrue(ALERT_THRESHOLD_TYPE_PERIODIC);
for (AlertDefine rule : periodicRules) {
updateSchedule(rule);
}
@@ -15,30 +15,8 @@
* limitations under the License.
*/
package org.apache.hertzbeat.alert.calculate.realtime;
package org.apache.hertzbeat.alert.calculate;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.alert.AlerterWorkerPool;
import org.apache.hertzbeat.alert.calculate.AlarmCacheManager;
import org.apache.hertzbeat.alert.calculate.JexlExprCalculator;
import org.apache.hertzbeat.alert.dao.SingleAlertDao;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.alert.service.AlertDefineService;
import org.apache.hertzbeat.alert.util.AlertTemplateUtil;
import org.apache.hertzbeat.alert.util.AlertUtil;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.queue.CommonDataQueue;
import org.apache.hertzbeat.common.support.exception.CommonDataQueueUnknownException;
import org.apache.hertzbeat.common.util.BackoffUtils;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.hertzbeat.common.util.ExponentialBackoff;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.Collections;
import java.util.HashMap;
@@ -49,16 +27,36 @@ import java.util.Set;
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.dao.SingleAlertDao;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.alert.service.AlertDefineService;
import org.apache.hertzbeat.alert.util.AlertTemplateUtil;
import org.apache.hertzbeat.alert.util.AlertUtil;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.queue.CommonDataQueue;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.hertzbeat.common.util.JexlExpressionRunner;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
/**
* Calculate alarms based on the alarm definition rules and collected data
*/
@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__";
@@ -83,65 +81,47 @@ 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 = () -> {
ExponentialBackoff backoff = new ExponentialBackoff(50L, 1000L);
while (!Thread.currentThread().isInterrupted()) {
try {
CollectRep.MetricsData metricsData = dataQueue.pollMetricsDataToAlerter();
if (metricsData == null) {
continue;
}
backoff.reset();
calculate(metricsData);
dataQueue.sendMetricsDataToStorage(metricsData);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
} catch (CommonDataQueueUnknownException ue) {
if (!BackoffUtils.shouldContinueAfterBackoff(backoff)) {
break;
}
} catch (Exception e) {
log.error("calculate alarm error: {}.", e.getMessage(), e);
}
@@ -152,11 +132,11 @@ public class MetricsRealTimeAlertCalculator {
}
}
protected void calculate(CollectRep.MetricsData metricsData) {
private void calculate(CollectRep.MetricsData metricsData) {
long currentTimeMilli = System.currentTimeMillis();
String instance = String.valueOf(metricsData.getId());
String instanceName = metricsData.getInstanceName();
String instanceHost = metricsData.getInstance();
String instanceHost = metricsData.getInstanceHost();
String app = metricsData.getApp();
String metrics = metricsData.getMetrics();
if ((CommonConstants.PROMETHEUS_APP_PREFIX + instanceName).equals(metricsData.getApp())) {
@@ -166,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,11 +185,12 @@ public class MetricsRealTimeAlertCalculator {
}
Long defineId = define.getId();
Map<String, String> commonFingerPrints = new HashMap<>(8);
commonFingerPrints.put(CommonConstants.LABEL_INSTANCE, instance);
// here use the alert name as finger, not care the alert name may be changed
commonFingerPrints.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(define.getId()));
commonFingerPrints.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
commonFingerPrints.put(CommonConstants.LABEL_INSTANCE_NAME, instanceName);
commonFingerPrints.put(CommonConstants.LABEL_INSTANCE, instanceHost);
commonFingerPrints.put(CommonConstants.LABEL_INSTANCE_HOST, instanceHost);
commonFingerPrints.putAll(define.getLabels());
if (labels != null) {
commonFingerPrints.putAll(labels);
@@ -217,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
@@ -272,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);
@@ -293,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) {
@@ -311,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);
@@ -333,7 +313,7 @@ public class MetricsRealTimeAlertCalculator {
if (!instanceMatcher.find() && !labelMatcher.find()) {
return true;
}
// Reset matcher to check all instances
instanceMatcher.reset();
labelMatcher.reset();
@@ -356,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);
@@ -372,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());
@@ -405,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);
@@ -423,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
@@ -435,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("");
@@ -1,232 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.calculate.periodic;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.alert.service.DataSourceService;
import org.apache.hertzbeat.alert.util.AlertTemplateUtil;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Log Periodic Alert Calculator
*/
@Slf4j
@Component
public class LogPeriodicAlertCalculator {
private static final String ROWS = "__rows__";
private final DataSourceService dataSourceService;
private final AlarmCommonReduce alarmCommonReduce;
public LogPeriodicAlertCalculator(DataSourceService dataSourceService, AlarmCommonReduce alarmCommonReduce) {
this.alarmCommonReduce = alarmCommonReduce;
this.dataSourceService = dataSourceService;
}
public void calculate(AlertDefine define) {
if (!define.isEnable() || StringUtils.isEmpty(define.getExpr())) {
log.error("Log define {} is disabled or expression is empty", define.getName());
return;
}
try {
doCalculate(define);
} catch (Exception e) {
log.error("Calculate periodic define {} failed: {}", define.getName(), e.getMessage());
}
}
private void doCalculate(AlertDefine define) {
try {
// Log-based queries are SQL queries with log-specific expressions
List<Map<String, Object>> results = dataSourceService.query(define.getDatasource(), define.getExpr());
if (CollectionUtils.isEmpty(results)) {
return;
}
afterThresholdRuleMatch(results, define);
} catch (Exception ignored) {
// Ignore the query exception eg: no result, timeout, etc
}
}
String getAlertMode(AlertDefine alertDefine) {
String mode = null;
if (alertDefine.getLabels() != null) {
mode = alertDefine.getLabels().get(CommonConstants.ALERT_MODE_LABEL);
}
if (mode == null || mode.isEmpty()) {
return CommonConstants.ALERT_MODE_GROUP; // Default to group mode if not specified
} else {
return mode;
}
}
/**
* Handle alert after threshold rule match
*/
private void afterThresholdRuleMatch(List<Map<String, Object>> alertContext, AlertDefine define) {
// Determine alert mode from configuration
String alertMode = getAlertMode(define);
long currentTime = System.currentTimeMillis();
switch (alertMode) {
case CommonConstants.ALERT_MODE_INDIVIDUAL:
// Generate individual alerts for each matching log
for (Map<String, Object> context : alertContext) {
generateIndividualAlert(define, context, currentTime);
}
break;
case CommonConstants.ALERT_MODE_GROUP:
// Generate a single alert group for all matching logs
generateGroupAlert(define, alertContext, currentTime);
break;
default:
log.warn("Unknown alert mode for define {}: {}", define.getName(), alertMode);
}
}
private void generateIndividualAlert(AlertDefine define, Map<String, Object> context, long currentTime) {
Map<String, String> alertLabels = new HashMap<>(8);
Map<String, String> commonFingerPrints = createCommonFingerprints(define);
alertLabels.putAll(commonFingerPrints);
addContextToMap(context, alertLabels);
Map<String, Object> fieldValueMap = createFieldValueMap(context, define);
Map<String, String> alertAnnotations = createAlertAnnotations(define, fieldValueMap);
// Create and send group alert
SingleAlert alert = SingleAlert.builder()
.labels(alertLabels)
.annotations(alertAnnotations)
.content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap))
.status(CommonConstants.ALERT_STATUS_FIRING)
.triggerTimes(1)
.startAt(currentTime)
.activeAt(currentTime)
.build();
alarmCommonReduce.reduceAndSendAlarm(alert.clone());
log.debug("Generated individual alert for define: {}", define.getName());
}
private void addContextToMap(Map<String, Object> context, Map<String, String> alertLabels) {
for (Map.Entry<String, Object> entry : context.entrySet()) {
if (entry.getValue() != null) {
alertLabels.put(entry.getKey(), entry.getValue().toString());
}
}
}
private void generateGroupAlert(AlertDefine define, List<Map<String, Object>> alertContext, long currentTime) {
List<SingleAlert> alerts = new ArrayList<>(alertContext.size());
// Create fingerprints for group alert
Map<String, String> commonFingerPrints = createCommonFingerprints(define);
// Add context information to fingerprints
commonFingerPrints.put(ROWS, String.valueOf(alertContext.size()));
commonFingerPrints.put(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_GROUP);
for (Map<String, Object> context : alertContext) {
Map<String, String> alertLabels = new HashMap<>(8);
alertLabels.putAll(commonFingerPrints);
// add the context to commonFingerPrints
addContextToMap(context, alertLabels);
Map<String, Object> fieldValueMap = createFieldValueMap(context, define);
Map<String, String> alertAnnotations = createAlertAnnotations(define, fieldValueMap);
// Create and send group alert
SingleAlert alert = SingleAlert.builder()
.labels(alertLabels)
.annotations(alertAnnotations)
.content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap))
.status(CommonConstants.ALERT_STATUS_FIRING)
.triggerTimes(alertContext.size())
.startAt(currentTime)
.activeAt(currentTime)
.build();
alerts.add(alert.clone());
}
alarmCommonReduce.reduceAndSendAlarmGroup(commonFingerPrints, alerts);
log.debug("Generated group alert for define: {} with {} matching data",
define.getName(), alertContext.size());
}
private Map<String, String> createCommonFingerprints(AlertDefine define) {
Map<String, String> fingerprints = new HashMap<>(8);
fingerprints.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
fingerprints.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(define.getId()));
if (define.getLabels() != null) {
fingerprints.putAll(define.getLabels());
}
return fingerprints;
}
private Map<String, Object> createFieldValueMap(Map<String, Object> context, AlertDefine define) {
Map<String, Object> fieldValueMap = new HashMap<>(8);
for (Map.Entry<String, Object> entry : context.entrySet()) {
if (entry.getValue() != null) {
fieldValueMap.put(entry.getKey(), entry.getValue().toString());
}
}
if (define.getLabels() != null) {
fieldValueMap.putAll(define.getLabels());
}
return fieldValueMap;
}
private Map<String, String> createAlertAnnotations(AlertDefine define, Map<String, Object> fieldValueMap) {
Map<String, String> annotations = new HashMap<>(8);
if (define.getAnnotations() != null) {
for (Map.Entry<String, String> entry : define.getAnnotations().entrySet()) {
annotations.put(entry.getKey(),
AlertTemplateUtil.render(entry.getValue(), fieldValueMap));
}
}
return annotations;
}
}
@@ -1,150 +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 com.google.common.util.concurrent.ThreadFactoryBuilder;
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.apache.hertzbeat.common.support.exception.CommonDataQueueUnknownException;
import org.apache.hertzbeat.common.util.BackoffUtils;
import org.apache.hertzbeat.common.util.ExponentialBackoff;
import org.springframework.stereotype.Component;
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() {
ExponentialBackoff backoff = new ExponentialBackoff(50L, 1000L);
while (!Thread.currentThread().isInterrupted()) {
try {
LogEntry logEntry = dataQueue.pollLogEntry();
if (logEntry == null) {
continue;
}
backoff.reset();
processLogEntry(logEntry);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (CommonDataQueueUnknownException ue) {
if (!BackoffUtils.shouldContinueAfterBackoff(backoff)) {
break;
}
} catch (Exception e) {
log.error("Error in log dispatch loop: {}", e.getMessage(), e);
}
}
}
private void processLogEntry(LogEntry logEntry) {
// Extract event timestamp
long eventTimestamp = extractEventTimestamp(logEntry);
// Check if timestamp is within reasonable range
if (!timeService.isValidTimestamp(eventTimestamp)) {
log.warn("Dropping log with invalid timestamp: {}", eventTimestamp);
return;
}
// Check if this is late data based on current watermark
if (timeService.isLateData(eventTimestamp)) {
log.warn("Dropping late data, timestamp: {}, watermark: {}",
eventTimestamp, timeService.getCurrentWatermark());
return;
}
// Update max timestamp (only validated timestamps can update watermark)
timeService.updateMaxTimestamp(eventTimestamp);
logWorker.reduceAndSendLogTask(logEntry);
}
private long extractEventTimestamp(LogEntry logEntry) {
if (logEntry.getTimeUnixNano() != null && logEntry.getTimeUnixNano() != 0) {
return logEntry.getTimeUnixNano() / 1_000_000; // Convert to milliseconds
}
if (logEntry.getObservedTimeUnixNano() != null && logEntry.getObservedTimeUnixNano() != 0) {
return logEntry.getObservedTimeUnixNano() / 1_000_000; // Convert to milliseconds
}
return System.currentTimeMillis();
}
@PostConstruct
public void start() {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("Alerter workerExecutor has uncaughtException.");
log.error(throwable.getMessage(), throwable);
})
.setDaemon(true)
.setNameFormat("log-dispatcher-%d")
.build();
// Create dispatcher thread executor
this.dispatcherExecutor = new ThreadPoolExecutor(
CALCULATE_THREADS,
CALCULATE_THREADS,
10,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
threadFactory,
new ThreadPoolExecutor.AbortPolicy()
);
for (int i = 0; i < CALCULATE_THREADS; i++) {
dispatcherExecutor.execute(this);
}
}
@PreDestroy
public void stop() {
if (dispatcherExecutor != null) {
dispatcherExecutor.shutdownNow();
}
}
}
@@ -1,317 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.calculate.realtime.window;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.springframework.stereotype.Component;
import org.apache.hertzbeat.alert.util.AlertTemplateUtil;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.apache.hertzbeat.common.entity.log.LogEntry;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* Alarm Evaluator - Final alarm logic trigger
* Responsible for:
* 1. Receiving closed window data from WindowAggregator
* 2. Applying alert logic (count threshold, alert mode)
* 3. Generating individual alerts or alert groups
* 4. Sending alerts to AlarmCommonReduce
*/
@Component
@Slf4j
public class AlarmEvaluator {
private static final String WINDOW_START_TIME = "window_start_time";
private static final String WINDOW_END_TIME = "window_end_time";
private static final String MATCHING_LOGS_COUNT = "matching_logs_count";
private final AlarmCommonReduce alarmCommonReduce;
private ThreadPoolExecutor workerExecutor;
public AlarmEvaluator(AlarmCommonReduce alarmCommonReduce) {
this.alarmCommonReduce = alarmCommonReduce;
initAlarmEvaluator();
}
public void initAlarmEvaluator() {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("alerter-reduce-worker has uncaughtException.");
log.error(throwable.getMessage(), throwable);
})
.setDaemon(true)
.setNameFormat("alerter-reduce-worker-%d")
.build();
workerExecutor = new ThreadPoolExecutor(2,
10,
10,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
threadFactory,
new ThreadPoolExecutor.AbortPolicy());
}
public void sendAndProcessWindowData(WindowAggregator.WindowData windowData) {
workerExecutor.execute(processWindowData(windowData));
}
private Runnable processWindowData(WindowAggregator.WindowData windowData) {
return () -> {
AlertDefine alertDefine = windowData.getAlertDefine();
List<MatchingLogEvent> matchingLogs = windowData.getMatchingLogs();
if (matchingLogs.isEmpty()) {
return;
}
// Check if count threshold is met
int requiredTimes = alertDefine.getTimes() != null ? alertDefine.getTimes() : 1;
if (matchingLogs.size() < requiredTimes) {
log.debug("Window {} has {} matching logs, but requires {} times",
windowData.getWindowKey(), matchingLogs.size(), requiredTimes);
return;
}
// Determine alert mode from configuration
String alertMode = getAlertMode(alertDefine);
long currentTime = System.currentTimeMillis();
switch (alertMode) {
case CommonConstants.ALERT_MODE_INDIVIDUAL:
// Generate individual alerts for each matching log
for (MatchingLogEvent matchingLog : matchingLogs) {
generateIndividualAlert(matchingLog, currentTime);
}
break;
case CommonConstants.ALERT_MODE_GROUP:
// Generate a single alert group for all matching logs
generateGroupAlert(windowData, matchingLogs, currentTime);
break;
default:
log.warn("Unknown alert mode for define {}: {}", alertDefine.getName(), alertMode);
}
};
}
private void generateIndividualAlert(MatchingLogEvent matchingLog, long currentTime) {
AlertDefine define = matchingLog.getAlertDefine();
LogEntry logEntry = matchingLog.getLogEntry();
Map<String, String> alertLabels = new HashMap<>(8);
// Create fingerprints for group alert
Map<String, String> commonFingerPrints = createCommonFingerprints(define);
alertLabels.putAll(commonFingerPrints);
// add the log data to commonFingerPrints
addLogEntryToMap(logEntry, alertLabels);
Map<String, Object> fieldValueMap = createFieldValueMap(logEntry, define);
Map<String, String> alertAnnotations = createAlertAnnotations(define, fieldValueMap);
// Create and send group alert
SingleAlert alert = SingleAlert.builder()
.labels(alertLabels)
.annotations(alertAnnotations)
.content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap))
.status(CommonConstants.ALERT_STATUS_FIRING)
.triggerTimes(1)
.startAt(currentTime)
.activeAt(currentTime)
.build();
alarmCommonReduce.reduceAndSendAlarm(alert.clone());
log.debug("Generated individual alert for define: {}", define.getName());
}
private void generateGroupAlert(WindowAggregator.WindowData windowData,
List<MatchingLogEvent> matchingLogs, long currentTime) {
List<SingleAlert> alerts = new ArrayList<>(matchingLogs.size());
AlertDefine define = windowData.getAlertDefine();
// Create fingerprints for group alert
Map<String, String> commonFingerPrints = createCommonFingerprints(define);
// Add window information to fingerprints
commonFingerPrints.put(WINDOW_START_TIME, String.valueOf(windowData.getStartTime()));
commonFingerPrints.put(WINDOW_END_TIME, String.valueOf(windowData.getEndTime()));
commonFingerPrints.put(CommonConstants.ALERT_MODE_LABEL, CommonConstants.ALERT_MODE_GROUP);
commonFingerPrints.put(MATCHING_LOGS_COUNT, String.valueOf(matchingLogs.size()));
for (MatchingLogEvent event: matchingLogs) {
LogEntry logEntry = event.getLogEntry();
Map<String, String> alertLabels = new HashMap<>(8);
alertLabels.putAll(commonFingerPrints);
// add the log data to commonFingerPrints
addLogEntryToMap(logEntry, alertLabels);
Map<String, Object> fieldValueMap = createFieldValueMap(logEntry, define);
Map<String, String> alertAnnotations = createAlertAnnotations(define, fieldValueMap);
// Create and send group alert
SingleAlert alert = SingleAlert.builder()
.labels(alertLabels)
.annotations(alertAnnotations)
.content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap))
.status(CommonConstants.ALERT_STATUS_FIRING)
.triggerTimes(matchingLogs.size())
.startAt(currentTime)
.activeAt(currentTime)
.build();
alerts.add(alert.clone());
}
alarmCommonReduce.reduceAndSendAlarmGroup(commonFingerPrints, alerts);
log.debug("Generated group alert for define: {} with {} matching logs",
define.getName(), matchingLogs.size());
}
private String getAlertMode(AlertDefine alertDefine) {
String mode = null;
if (alertDefine.getLabels() != null) {
mode = alertDefine.getLabels().get(CommonConstants.ALERT_MODE_LABEL);
}
if (mode == null || mode.isEmpty()) {
return CommonConstants.ALERT_MODE_GROUP; // Default to group mode if not specified
} else {
return mode;
}
}
private Map<String, String> createCommonFingerprints(AlertDefine define) {
Map<String, String> fingerprints = new HashMap<>(8);
fingerprints.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
fingerprints.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(define.getId()));
if (define.getLabels() != null) {
fingerprints.putAll(define.getLabels());
}
return fingerprints;
}
private Map<String, Object> createFieldValueMap(LogEntry logEntry, AlertDefine define) {
Map<String, Object> fieldValueMap = new HashMap<>(8);
fieldValueMap.put("log", logEntry);
if (define.getLabels() != null) {
fieldValueMap.putAll(define.getLabels());
}
return fieldValueMap;
}
private Map<String, String> createAlertAnnotations(AlertDefine define, Map<String, Object> fieldValueMap) {
Map<String, String> annotations = new HashMap<>(8);
if (define.getAnnotations() != null) {
for (Map.Entry<String, String> entry : define.getAnnotations().entrySet()) {
annotations.put(entry.getKey(),
AlertTemplateUtil.render(entry.getValue(), fieldValueMap));
}
}
return annotations;
}
/**
* Add the content from LogEntry object (except timestamp) to commonFingerPrints
*
* @param logEntry log entry object
* @param context context
*/
private void addLogEntryToMap(LogEntry logEntry, Map<String, String> context) {
// Add basic fields
if (logEntry.getSeverityNumber() != null) {
context.put("severityNumber", String.valueOf(logEntry.getSeverityNumber()));
}
if (logEntry.getSeverityText() != null) {
context.put("severityText", logEntry.getSeverityText());
}
if (logEntry.getBody() != null) {
context.put("body", String.valueOf(logEntry.getBody()));
}
if (logEntry.getDroppedAttributesCount() != null) {
context.put("droppedAttributesCount", String.valueOf(logEntry.getDroppedAttributesCount()));
}
if (logEntry.getTraceId() != null) {
context.put("traceId", logEntry.getTraceId());
}
if (logEntry.getSpanId() != null) {
context.put("spanId", logEntry.getSpanId());
}
if (logEntry.getTraceFlags() != null) {
context.put("traceFlags", String.valueOf(logEntry.getTraceFlags()));
}
// Add attributes
if (logEntry.getAttributes() != null && !logEntry.getAttributes().isEmpty()) {
for (Map.Entry<String, Object> entry : logEntry.getAttributes().entrySet()) {
if (entry.getValue() != null) {
context.put("attr_" + entry.getKey(), String.valueOf(entry.getValue()));
}
}
}
// Add resource
if (logEntry.getResource() != null && !logEntry.getResource().isEmpty()) {
for (Map.Entry<String, Object> entry : logEntry.getResource().entrySet()) {
if (entry.getValue() != null) {
context.put("resource_" + entry.getKey(), String.valueOf(entry.getValue()));
}
}
}
// Add instrumentationScope
if (logEntry.getInstrumentationScope() != null) {
LogEntry.InstrumentationScope scope = logEntry.getInstrumentationScope();
if (scope.getName() != null) {
context.put("scope_name", scope.getName());
}
if (scope.getVersion() != null) {
context.put("scope_version", scope.getVersion());
}
if (scope.getDroppedAttributesCount() != null) {
context.put("scope_droppedAttributesCount", String.valueOf(scope.getDroppedAttributesCount()));
}
if (scope.getAttributes() != null && !scope.getAttributes().isEmpty()) {
for (Map.Entry<String, Object> entry : scope.getAttributes().entrySet()) {
if (entry.getValue() != null) {
context.put("scope_attr_" + entry.getKey(), String.valueOf(entry.getValue()));
}
}
}
}
}
}
@@ -1,123 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.calculate.realtime.window;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.AlerterWorkerPool;
import org.apache.hertzbeat.alert.calculate.JexlExprCalculator;
import org.apache.hertzbeat.alert.service.AlertDefineService;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.entity.log.LogEntry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* LogWorker Responsible for:
* 1. Receiving logs from WindowedLogRealTimeAlertCalculator
* 2. Evaluating alert expressions against logs
* 3. Sending matching logs to WindowAggregator
*/
@Slf4j
@Component
public class LogWorker {
private static final String LOG_PREFIX = "log";
private final AlertDefineService alertDefineService;
private final JexlExprCalculator jexlExprCalculator;
private final WindowAggregator windowAggregator;
private final AlerterWorkerPool workerPool;
@Autowired
public LogWorker(AlertDefineService alertDefineService,
JexlExprCalculator jexlExprCalculator, WindowAggregator windowAggregator,
AlerterWorkerPool workerPool) {
this.alertDefineService = alertDefineService;
this.jexlExprCalculator = jexlExprCalculator;
this.windowAggregator = windowAggregator;
this.workerPool = workerPool;
}
public void reduceAndSendLogTask(LogEntry logEntry) {
workerPool.executeLogJob(reduceLogTask(logEntry));
}
public Runnable reduceLogTask(LogEntry logEntry) {
return () -> {
try {
processLogEntry(logEntry);
} catch (Exception e) {
log.error("Error processing log entry in worker: {}", e.getMessage(), e);
}
};
}
private void processLogEntry(LogEntry logEntry) {
// Get all log alert definitions
List<AlertDefine> alertDefines = alertDefineService.getLogRealTimeAlertDefines();
// Create context for expression evaluation
Map<String, Object> context = new HashMap<>(8);
context.put(LOG_PREFIX, logEntry);
// Process each alert definition
for (AlertDefine define : alertDefines) {
if (define.getExpr() == null || define.getExpr().trim().isEmpty()) {
continue;
}
try {
// Evaluate alert expression
boolean match = jexlExprCalculator.execAlertExpression(context, define.getExpr(), false);
if (match) {
// Create matching log event and send to WindowAggregator
MatchingLogEvent event = createMatchingLogEvent(logEntry, define);
windowAggregator.addMatchingLog(event);
}
} catch (Exception e) {
log.warn("Error evaluating expression for alert define {}: {}", define.getName(), e.getMessage());
}
}
}
private MatchingLogEvent createMatchingLogEvent(LogEntry logEntry, AlertDefine define) {
long eventTimestamp = extractEventTimestamp(logEntry);
return MatchingLogEvent.builder()
.logEntry(logEntry)
.alertDefine(define)
.eventTimestamp(eventTimestamp)
.workerTimestamp(System.currentTimeMillis())
.build();
}
private long extractEventTimestamp(LogEntry logEntry) {
if (logEntry.getTimeUnixNano() != null && logEntry.getTimeUnixNano() != 0) {
return logEntry.getTimeUnixNano() / 1_000_000; // Convert to milliseconds
}
if (logEntry.getObservedTimeUnixNano() != null && logEntry.getObservedTimeUnixNano() != 0) {
return logEntry.getObservedTimeUnixNano() / 1_000_000; // Convert to milliseconds
}
return System.currentTimeMillis();
}
}
@@ -1,213 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.calculate.realtime.window;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
/**
* Watermark Generator responsible for:
* 1. Receiving maxTimestamp updates from WindowedLogRealTimeAlertCalculator
* 2. Calculating watermarks based on configurable delay
* 3. Broadcasting watermarks to all subscribers (WindowAggregator)
*/
@Component
@Slf4j
public class TimeService {
static final long DEFAULT_WATERMARK_DELAY_MS = 30_000; // 30 seconds
private static final long WATERMARK_BROADCAST_INTERVAL_MS = 5_000; // 5 seconds
// Define acceptable timestamp range to filter abnormal timestamps
private static final long MAX_FUTURE_TIME_MS = 60_000; // Allow 1 minute future time
private static final long MAX_PAST_TIME_MS = 24 * 60 * 60 * 1000; // Allow 24 hours past time
private final AtomicLong maxTimestamp = new AtomicLong(0);
private final AtomicLong currentWatermark = new AtomicLong(0);
private final CopyOnWriteArrayList<WatermarkListener> listeners = new CopyOnWriteArrayList<>();
private ScheduledExecutorService scheduler;
public TimeService(List<WatermarkListener> initialListeners) {
listeners.addAll(initialListeners);
}
@PostConstruct
public void start() {
// Create internal scheduled executor
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("TimeService scheduler has uncaughtException.");
log.error(throwable.getMessage(), throwable);
})
.setDaemon(true)
.setNameFormat("timeservice-scheduler-%d")
.build();
this.scheduler = Executors.newSingleThreadScheduledExecutor(threadFactory);
// Start watermark broadcast scheduler
scheduler.scheduleAtFixedRate(
this::broadcastWatermark,
0,
WATERMARK_BROADCAST_INTERVAL_MS,
TimeUnit.MILLISECONDS
);
log.info("TimeService started with watermark delay: {}ms", DEFAULT_WATERMARK_DELAY_MS);
}
@PreDestroy
public void stop() {
if (scheduler != null && !scheduler.isShutdown()) {
log.info("Shutting down TimeService scheduler...");
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(10, TimeUnit.SECONDS)) {
log.warn("TimeService scheduler did not terminate within 10 seconds, forcing shutdown");
scheduler.shutdownNow();
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
log.error("TimeService scheduler did not terminate");
}
}
} catch (InterruptedException e) {
log.warn("Interrupted while waiting for TimeService scheduler to terminate");
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
}
log.info("TimeService stopped");
}
/**
* Check if timestamp is within acceptable range
*/
public boolean isValidTimestamp(long timestamp) {
long currentTime = System.currentTimeMillis();
return timestamp >= (currentTime - MAX_PAST_TIME_MS) && timestamp <= (currentTime + MAX_FUTURE_TIME_MS);
}
/**
* Check if data is late based on current watermark
*/
public boolean isLateData(long timestamp) {
return timestamp < getCurrentWatermark();
}
/**
* Update max timestamp from WindowedLogRealTimeAlertCalculator
*/
public void updateMaxTimestamp(long timestamp) {
maxTimestamp.getAndUpdate(current -> Math.max(current, timestamp));
}
/**
* Add watermark listener
*/
public void addWatermarkListener(WatermarkListener listener) {
listeners.add(listener);
}
/**
* Remove watermark listener
*/
public void removeWatermarkListener(WatermarkListener listener) {
listeners.remove(listener);
}
/**
* Get current watermark
*/
public long getCurrentWatermark() {
return currentWatermark.get();
}
/**
* Get max timestamp
*/
public long getMaxTimestamp() {
return maxTimestamp.get();
}
/**
* Calculate and broadcast watermark
*/
private void broadcastWatermark() {
try {
long maxTs = maxTimestamp.get();
if (maxTs <= 0) {
return;
}
// Calculate watermark: maxTimestamp - delay
long newWatermark = maxTs - DEFAULT_WATERMARK_DELAY_MS;
long currentWm = currentWatermark.get();
// Only advance watermark (monotonic property)
if (newWatermark <= currentWm) {
return;
}
if (currentWatermark.compareAndSet(currentWm, newWatermark)) {
// Broadcast to all listeners
Watermark watermark = new Watermark(newWatermark);
for (WatermarkListener listener : listeners) {
try {
listener.onWatermark(watermark);
} catch (Exception e) {
log.error("Error notifying watermark listener: {}", e.getMessage(), e);
}
}
log.debug("Broadcast watermark: {} (maxTimestamp: {})", newWatermark, maxTs);
}
} catch (Exception e) {
log.error("Error in watermark broadcast: {}", e.getMessage(), e);
}
}
/**
* Watermark data class
*/
@Data
@AllArgsConstructor
@Getter
public static class Watermark {
private final long timestamp;
}
/**
* Interface for watermark listeners
*/
public interface WatermarkListener {
void onWatermark(Watermark watermark);
}
}
@@ -1,251 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.calculate.realtime.window;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
/**
* Window Aggregator Responsible for:
* 1. Managing window data structure for all active windows
* 2. Receiving matching logs from Workers
* 3. Receiving watermarks from TimeService
* 4. Sending closed windows to AlarmEvaluator
*/
@Component
@Slf4j
public class WindowAggregator implements TimeService.WatermarkListener, Runnable {
private static final long DEFAULT_WINDOW_SIZE_MS = 1 * 60 * 1000; // 1 minutes
private final AlarmEvaluator alarmEvaluator;
private final BlockingQueue<MatchingLogEvent> eventQueue = new LinkedBlockingQueue<>();
private final Map<WindowKey, WindowData> activeWindows = new HashMap<>();
private final Object windowLock = new Object();
private ExecutorService aggregatorExecutor;
public WindowAggregator(AlarmEvaluator alarmEvaluator) {
this.alarmEvaluator = alarmEvaluator;
}
public void addMatchingLog(MatchingLogEvent event) {
try {
eventQueue.put(event);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("Interrupted while adding matching log to aggregator");
}
}
@Override
public void onWatermark(TimeService.Watermark watermark) {
List<WindowData> closedWindows;
synchronized (windowLock) {
closedWindows = new ArrayList<>();
// Find windows that should be closed based on watermark
Iterator<Map.Entry<WindowKey, WindowData>> iterator = activeWindows.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<WindowKey, WindowData> entry = iterator.next();
WindowData windowData = entry.getValue();
// Close window if its end time <= watermark timestamp
if (windowData.getEndTime() <= watermark.getTimestamp()) {
closedWindows.add(windowData);
iterator.remove();
log.debug("Closing window: {} with {} matching logs",
entry.getKey(), windowData.getMatchingLogs().size());
}
}
}
for (WindowData windowData : closedWindows) {
alarmEvaluator.sendAndProcessWindowData(windowData);
}
}
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
MatchingLogEvent event = eventQueue.take();
processMatchingLogEvent(event);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (Exception e) {
log.error("Error processing matching log event: {}", e.getMessage(), e);
}
}
}
private void processMatchingLogEvent(MatchingLogEvent event) {
// Determine window size from alert define (if specified) or use default
long windowSizeMs = getWindowSize(event.getAlertDefine());
// Calculate window boundaries
long eventTime = event.getEventTimestamp();
long windowStart = (eventTime / windowSizeMs) * windowSizeMs;
long windowEnd = windowStart + windowSizeMs;
// Create window key
WindowKey windowKey = new WindowKey(
event.getAlertDefine().getId(),
windowStart,
windowEnd
);
synchronized (windowLock) {
// Get or create window data
WindowData windowData = activeWindows.computeIfAbsent(windowKey,
key -> new WindowData(key, event.getAlertDefine()));
// Add matching log to window
windowData.addMatchingLog(event);
log.debug("Added matching log to window: {} (total logs: {})",
windowKey, windowData.getMatchingLogs().size());
}
}
private long getWindowSize(AlertDefine alertDefine) {
// Check if alert define has custom window size configuration
if (alertDefine.getPeriod() != null) {
return alertDefine.getPeriod() * 1000; // Convert seconds to milliseconds
}
log.info("Using default window size of {} ms for alert define: {}",
DEFAULT_WINDOW_SIZE_MS, alertDefine.getName());
return DEFAULT_WINDOW_SIZE_MS;
}
@PostConstruct
public void start() {
// Create internal executor
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("WindowAggregator executor has uncaughtException.");
log.error(throwable.getMessage(), throwable);
})
.setDaemon(true)
.setNameFormat("window-aggregator-%d")
.build();
aggregatorExecutor = Executors.newSingleThreadExecutor(threadFactory);
// Submit aggregation task
aggregatorExecutor.submit(this);
log.info("WindowAggregator started");
}
@PreDestroy
public void stop() {
if (aggregatorExecutor != null && !aggregatorExecutor.isShutdown()) {
log.info("Shutting down WindowAggregator executor...");
aggregatorExecutor.shutdown();
try {
if (!aggregatorExecutor.awaitTermination(10, TimeUnit.SECONDS)) {
log.warn("WindowAggregator executor did not terminate within 10 seconds, forcing shutdown");
aggregatorExecutor.shutdownNow();
if (!aggregatorExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
log.error("WindowAggregator executor did not terminate");
}
}
} catch (InterruptedException e) {
log.warn("Interrupted while waiting for WindowAggregator executor to terminate");
aggregatorExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
}
log.info("WindowAggregator stopped");
}
/**
* Window key for identifying unique windows
*/
@Data
@Getter
@AllArgsConstructor
public static class WindowKey {
private final long alertDefineId;
private final long startTime;
private final long endTime;
}
/**
* Window data container
*/
public static class WindowData {
@Getter
private final WindowKey windowKey;
@Getter
private final AlertDefine alertDefine;
private final List<MatchingLogEvent> matchingLogs = new ArrayList<>();
@Getter
private final long createdTime;
public WindowData(WindowKey windowKey, AlertDefine alertDefine) {
this.windowKey = windowKey;
this.alertDefine = alertDefine;
this.createdTime = System.currentTimeMillis();
}
public void addMatchingLog(MatchingLogEvent event) {
matchingLogs.add(event);
}
public List<MatchingLogEvent> getMatchingLogs() {
return new ArrayList<>(matchingLogs);
}
/**
* Get start time of the window
* @return start time
*/
public long getStartTime() { return windowKey.getStartTime(); }
/**
* Get end time of the window
* @return end time
*/
public long getEndTime() { return windowKey.getEndTime(); }
}
}
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.common.entity.dto.sms;
package org.apache.hertzbeat.alert.config;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.common.entity.dto.sms;
package org.apache.hertzbeat.alert.config;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.common.entity.dto.sms;
package org.apache.hertzbeat.alert.config;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
@@ -74,4 +74,4 @@ public class SmsConfig {
* Smslocal SMS configuration
*/
private SmslocalSmsProperties smslocal;
}
}
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.common.entity.dto.sms;
package org.apache.hertzbeat.alert.config;
import lombok.AllArgsConstructor;
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.common.entity.dto.sms;
package org.apache.hertzbeat.alert.config;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
@@ -58,4 +58,4 @@ public class TencentSmsProperties {
*/
@NotBlank(message = "templateId cannot be null")
private String templateId;
}
}
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.common.entity.dto.sms;
package org.apache.hertzbeat.alert.config;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
@@ -46,4 +46,4 @@ public class TwilioSmsProperties {
*/
@NotBlank(message = "Twilio Phone Number cannot be empty")
private String twilioPhoneNumber;
}
}
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.common.entity.dto.sms;
package org.apache.hertzbeat.alert.config;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
@@ -57,4 +57,4 @@ public class UniSmsProperties {
*/
@NotBlank(message = "authMode cannot be null")
private String authMode = "simple";
}
}
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.common.entity.dto;
package org.apache.hertzbeat.alert.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Max;
@@ -18,17 +18,12 @@
package org.apache.hertzbeat.alert.notice.impl;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.alert.notice.AlertNoticeException;
import org.apache.hertzbeat.alert.util.CryptoUtils;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
@@ -59,13 +54,7 @@ final class DingTalkRobotAlertNotifyHandlerImpl extends AbstractAlertNotifyHandl
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<DingTalkWebHookDto> httpEntity = new HttpEntity<>(dingTalkWebHookDto, headers);
StringBuilder webHookUrlBuilder = new StringBuilder()
.append(alerterProperties.getDingTalkWebhookUrl())
.append(receiver.getAccessToken());
if (StringUtils.isNotBlank(receiver.getAppSecret())) {
webHookUrlBuilder.append(signSecret(receiver.getAppSecret()));
}
String webHookUrl = webHookUrlBuilder.toString();
String webHookUrl = alerterProperties.getDingTalkWebhookUrl() + receiver.getAccessToken();
ResponseEntity<CommonRobotNotifyResp> responseEntity = restTemplate.postForEntity(webHookUrl,
httpEntity, CommonRobotNotifyResp.class);
if (responseEntity.getStatusCode() == HttpStatus.OK) {
@@ -113,13 +102,6 @@ final class DingTalkRobotAlertNotifyHandlerImpl extends AbstractAlertNotifyHandl
return dingTalkWebHookDto;
}
private String signSecret(String secret) throws Exception {
Long timestamp = System.currentTimeMillis();
String sign = URLEncoder.encode(CryptoUtils.hmacSha256Base64(secret, timestamp + "\n" + secret),
StandardCharsets.UTF_8);
return String.format("&timestamp=%s&sign=%s", timestamp, sign);
}
@Override
public byte type() {
return 5;
@@ -127,7 +109,6 @@ final class DingTalkRobotAlertNotifyHandlerImpl extends AbstractAlertNotifyHandl
/**
* DingTalk robot request body
*
* @version 1.0
*/
@Data
@@ -206,4 +187,5 @@ final class DingTalkRobotAlertNotifyHandlerImpl extends AbstractAlertNotifyHandl
}
}
@@ -24,7 +24,7 @@ import java.util.Properties;
import java.util.ResourceBundle;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.entity.dto.MailServerConfig;
import org.apache.hertzbeat.alert.dto.MailServerConfig;
import org.apache.hertzbeat.alert.notice.AlertNoticeException;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.manager.GeneralConfig;
@@ -1,487 +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.notice.impl;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.alert.AlerterProperties;
import org.apache.hertzbeat.alert.notice.AlertNoticeException;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* FeiShu app alert notify impl
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class FeiShuAppAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl {
/**
* get tenant access_token url
*/
private static final String TENANT_ACCESS_TOKEN_URL = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal";
/**
* get FeiShu app employee url
*/
private static final String EMPLOYEE_URL = "https://open.feishu.cn/open-apis/ehr/v1/employees?status=2&status=4&user_id_type=user_id&page_size=100";
/**
* send FeiShu app message url
*/
private static final String APP_MESSAGE_URL = "https://open.feishu.cn/open-apis/im/v1/messages";
/**
* send FeiShu app batch message url
*/
private static final String APP_BATCH_MESSAGE_URL = "https://open.feishu.cn/open-apis/message/v4/batch_send/";
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final byte USER_RECEIVE_TYPE = 0;
private static final byte CHAT_RECEIVE_TYPE = 1;
private static final byte PART_RECEIVE_TYPE = 2;
private static final byte ALL_RECEIVE_TYPE = 3;
private final RestTemplate restTemplate;
private final AlerterProperties alerterProperties;
@Override
public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) throws AlertNoticeException {
var appId = receiver.getAppId();
var appSecret = receiver.getAppSecret();
var larkReceiveIdType = receiver.getLarkReceiveType();
try {
var accessToken = getAccessToken(appId, appSecret);
var notificationContent = JsonUtil.toJson(renderContent(noticeTemplate, alert));
JsonNode messageContent = createLarkMessage(receiver, notificationContent);
switch (larkReceiveIdType) {
case USER_RECEIVE_TYPE -> {
String[] userIds = receiver.getUserId().split(",");
if (userIds.length == 1) {
sendLarkMessage(accessToken, "user_id", userIds[0], messageContent);
} else {
sendLarkUserBatchMessage(accessToken, userIds, messageContent);
}
}
case CHAT_RECEIVE_TYPE -> sendLarkMessage(accessToken, "chat_id", receiver.getChatId(), messageContent);
case PART_RECEIVE_TYPE ->
sendLarkDepartmentBatchMessage(accessToken, receiver.getPartyId().split(","), messageContent);
case ALL_RECEIVE_TYPE -> {
List<String> userIds = new ArrayList<>();
getLarkEmployeeUserIds(accessToken, null, userIds);
sendLarkUserBatchMessage(accessToken, userIds.toArray(new String[0]), messageContent);
}
default -> throw new AlertNoticeException("Invalid larkReceiveIdType: " + larkReceiveIdType);
}
} catch (Exception e) {
throw new AlertNoticeException("[FeiShu App Notify Error] " + e.getMessage());
}
}
@Override
public byte type() {
return 14;
}
/**
* Send FeiShu app message to chat or designated personnel
*
* @param accessToken Tenant access token
* @param receiverIdType FeiShu app send message receiver id type: user_id, chat_id
* @param receiverId FeiShu app user id or chat id
* @param messageContent Message content
* @see <a href="https://open.feishu.cn/document/server-docs/im-v1/batch_message/send-messages-in-batches">send message</a>
*/
private void sendLarkMessage(String accessToken, String receiverIdType, String receiverId, JsonNode messageContent) throws JsonProcessingException {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(accessToken);
FeiShuAppMessageDto messageDto = FeiShuAppMessageDto.builder()
.receiveId(receiverId)
.content(escapedCompactJson(messageContent))
.build();
HttpEntity<FeiShuAppMessageDto> request = new HttpEntity<>(messageDto, headers);
call(APP_MESSAGE_URL + "?receive_id_type=" + receiverIdType, request, HttpMethod.POST, FeiShuAppResponse.class);
}
/**
* Send FeiShu app department batch message
*
* @param accessToken Tenant access token
* @param partyIds FeiShu app department ids
* @param messageContent Message content
* @see <a href="https://open.feishu.cn/document/server-docs/im-v1/batch_message/send-messages-in-batches">send batch message</a>
*/
private void sendLarkDepartmentBatchMessage(String accessToken, String[] partyIds, JsonNode messageContent) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(accessToken);
FeiShuAppBatchMessageDto batchMessageDto = FeiShuAppBatchMessageDto.builder()
.departmentIds(partyIds)
.card(messageContent)
.build();
HttpEntity<FeiShuAppBatchMessageDto> request = new HttpEntity<>(batchMessageDto, headers);
call(APP_BATCH_MESSAGE_URL, request, HttpMethod.POST, FeiShuAppResponse.class);
}
/**
* Send FeiShu app batch message to user
*
* @param accessToken Tenant access token
* @param userIds FeiShu app user ids
* @param messageContent Message content
* @see <a href="https://open.feishu.cn/document/server-docs/im-v1/batch_message/send-messages-in-batches">send batch message</a>
*/
private void sendLarkUserBatchMessage(String accessToken, String[] userIds, JsonNode messageContent) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(accessToken);
FeiShuAppBatchMessageDto batchMessageDto = FeiShuAppBatchMessageDto.builder()
.userIds(userIds)
.card(messageContent)
.build();
HttpEntity<FeiShuAppBatchMessageDto> request = new HttpEntity<>(batchMessageDto, headers);
FeiShuAppResponse call = call(APP_BATCH_MESSAGE_URL, request, HttpMethod.POST, FeiShuAppResponse.class);
}
/**
* Get FeiShu app tenant access token
*
* @param appId Unique identifier for the application, obtained after creating the application
* @param appSecret Application key, obtained after creating the application
* @return Tenant access token
* @see <a href="https://open.feishu.cn/document/server-docs/authentication-management/access-token/tenant_access_token_internal">tenant_access_token</a>
*/
private String getAccessToken(String appId, String appSecret) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
FeiShuAppAccessTokenDto accessTokenRequest = FeiShuAppAccessTokenDto.builder()
.appId(appId)
.appSecret(appSecret)
.build();
HttpEntity<FeiShuAppAccessTokenDto> request = new HttpEntity<>(accessTokenRequest, headers);
FeiShuAppAccessTokenResponse data = call(TENANT_ACCESS_TOKEN_URL, request, HttpMethod.POST, FeiShuAppAccessTokenResponse.class);
return data.getTenantAccessToken();
}
/**
* Get FeiShu app employee user ids
*
* @param accessToken FeiShu app tenant access token
* @param pageToken Paging marker, left blank for the first request, indicating traversal from scratch;
* When there are more items in the pagination query result, a new page_token will be returned at the same time.
* The next iteration can use this page_token to obtain the query result
* @param userIds Collection for recursive padding
* @see <a href="https://open.feishu.cn/document/server-docs/ehr-v1/list?appId=cli_a999532b1f52900b">https://open.feishu.cn/open-apis/ehr/v1/employees</a>
*/
private void getLarkEmployeeUserIds(String accessToken, String pageToken, List<String> userIds) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(accessToken);
HttpEntity<String> request = new HttpEntity<>(headers);
var url = StringUtils.isNotBlank(pageToken) ? EMPLOYEE_URL + "&page_token=" + pageToken : EMPLOYEE_URL;
FeiShuAppEmployeeResponse employeeResponse = call(url,
request,
HttpMethod.GET,
FeiShuAppEmployeeResponse.class);
if (Objects.equals(employeeResponse.getCode(), 0)) {
userIds.addAll(employeeResponse.getData().getItems().stream().map(FeiShuAppEmployeeResponse.Employee::getUserId).toList());
var hasMore = employeeResponse.getData().getHasMore();
if (Boolean.TRUE.equals(hasMore)) {
getLarkEmployeeUserIds(accessToken, employeeResponse.getData().getPageToken(), userIds);
}
}
}
private <R extends FeiShuAppResponse, E> R call(String url, HttpEntity<E> request, HttpMethod httpMethod, Class<R> responseType) {
ResponseEntity<R> response = restTemplate.exchange(url, httpMethod, request, responseType);
if (Objects.nonNull(response.getBody()) && !Objects.equals(response.getBody().getCode(), 0)) {
log.warn("Send FeiShu App Error: {}", response.getBody().getMsg());
throw new AlertNoticeException("Http StatusCode " + response.getStatusCode() + " Error: " + response.getBody().getMsg());
}
return response.getBody();
}
private JsonNode createLarkMessage(NoticeReceiver receiver, String notificationContent) throws JsonProcessingException {
String larkCardMessage = """
{
"schema": "2.0",
"config": {
"update_multi": true,
"locales": [
"en_us",
"zh_cn"
],
"style": {
"text_size": {
"normal_v2": {
"default": "normal",
"pc": "normal",
"mobile": "heading"
}
}
}
},
"body": {
"direction": "vertical",
"padding": "12px 12px 12px 12px",
"elements": [
{
"tag": "markdown",
"content": "%s",
"i18n_content": {
"en_us": ""
},
"text_align": "left",
"text_size": "normal_v2",
"margin": "0px 0px 0px 0px"
},
{
"tag": "hr",
"margin": "0px 0px 0px 0px"
},
{
"tag": "column_set",
"horizontal_align": "left",
"columns": [
{
"tag": "column",
"width": "weighted",
"elements": [
{
"tag": "button",
"text": {
"tag": "plain_text",
"content": "登入控制台",
"i18n_content": {
"en_us": "Login In"
}
},
"type": "default",
"width": "default",
"size": "medium",
"behaviors": [
{
"type": "open_url",
"default_url": "%s",
"pc_url": "",
"ios_url": "",
"android_url": ""
}
]
}
],
"direction": "horizontal",
"vertical_spacing": "8px",
"horizontal_align": "left",
"vertical_align": "top",
"weight": 1
}
],
"margin": "0px 0px 0px 0px"
}
]
},
"header": {
"title": {
"tag": "plain_text",
"content": "HertzBeat 告警",
"i18n_content": {
"en_us": "HertzBeat Alarm"
}
},
"subtitle": {
"tag": "plain_text",
"content": ""
},
"template": "red",
"padding": "12px 12px 12px 12px"
}
}
""";
Byte larkReceiveIdType = receiver.getLarkReceiveType();
String userId = receiver.getUserId();
String atUserElement = "";
if (Objects.equals(larkReceiveIdType, CHAT_RECEIVE_TYPE) && StringUtils.isNotBlank(userId)) {
atUserElement = "\\n" + Arrays.stream(userId.split(","))
.map(id -> "<at id=" + id + "></at>")
.collect(Collectors.joining(" "));
}
if (notificationContent.startsWith("\"") && notificationContent.endsWith("\"")) {
notificationContent = StringUtils.removeStart(notificationContent, "\"");
notificationContent = StringUtils.removeEnd(notificationContent, "\"");
}
String jsonStr = String.format(larkCardMessage,
notificationContent.replace("\"", "\\\"") + atUserElement,
alerterProperties.getConsoleUrl());
return OBJECT_MAPPER.readTree(jsonStr);
}
private String escapedCompactJson(JsonNode json) throws JsonProcessingException {
return OBJECT_MAPPER.writeValueAsString(json);
}
/**
* feiShu app response
*/
@Data
protected static class FeiShuAppResponse {
private Integer code;
private String msg;
}
/**
* FeiShu app message get tenant access token request
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
protected static class FeiShuAppAccessTokenDto {
@JsonProperty("app_id")
private String appId;
@JsonProperty("app_secret")
private String appSecret;
}
/**
* FeiShu app message get tenant access token response
*/
@EqualsAndHashCode(callSuper = true)
@Data
protected static class FeiShuAppAccessTokenResponse extends FeiShuAppResponse {
@JsonProperty("tenant_access_token")
private String tenantAccessToken;
}
/**
* FeiShu app employee response
*/
@EqualsAndHashCode(callSuper = true)
@Data
protected static class FeiShuAppEmployeeResponse extends FeiShuAppResponse {
private EmployeeResponseData data;
@Data
private static class EmployeeResponseData {
@JsonProperty("page_token")
private String pageToken;
@JsonProperty("has_more")
private Boolean hasMore;
private List<Employee> items;
}
@Data
private static class Employee {
@JsonProperty("user_id")
private String userId;
}
}
/**
* FeiShu app message request
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
protected static class FeiShuAppMessageDto {
@JsonProperty("receive_id")
private String receiveId;
@Builder.Default
@JsonProperty("msg_type")
private String msgType = "interactive";
@JsonProperty("content")
private String content;
@Builder.Default
private final String uuid = UUID.randomUUID().toString();
}
/**
* FeiShu app batch message request
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
protected static class FeiShuAppBatchMessageDto {
@Builder.Default
@JsonProperty("msg_type")
private final String msgType = "interactive";
private JsonNode card;
@JsonProperty("department_ids")
private String[] departmentIds;
@JsonProperty("user_ids")
private String[] userIds;
}
}
@@ -48,11 +48,6 @@ final class WebHookAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl
}
HttpHeaders headers = new HttpHeaders();
if ("Basic".equalsIgnoreCase(receiver.getHookAuthType())) {
headers.setBasicAuth(receiver.getHookAuthToken());
} else if ("Bearer".equalsIgnoreCase(receiver.getHookAuthType())) {
headers.setBearerAuth(receiver.getHookAuthToken());
}
headers.setContentType(MediaType.APPLICATION_JSON);
// alert.setContent(escapeJsonStr(alert.getContent()));
String webhookJson = renderContent(noticeTemplate, alert);
@@ -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 () -> {
@@ -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);
}
@@ -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
@@ -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);
}
@@ -19,7 +19,7 @@ package org.apache.hertzbeat.alert.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.entity.dto.sms.SmsConfig;
import org.apache.hertzbeat.alert.config.SmsConfig;
import org.apache.hertzbeat.alert.service.impl.SmsLocalSmsClientImpl;
import org.apache.hertzbeat.alert.service.impl.AwsSmsClientImpl;
import org.apache.hertzbeat.alert.service.impl.TencentSmsClientImpl;
@@ -151,4 +151,4 @@ public class SmsClientFactory {
break;
}
}
}
}
@@ -24,7 +24,7 @@ import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.Predicate;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.calculate.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);
}
}
@@ -17,9 +17,7 @@
package org.apache.hertzbeat.alert.service.impl;
import com.fasterxml.jackson.core.type.TypeReference;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.alert.dto.AlibabaCloudSlsExternAlert;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
@@ -35,9 +33,7 @@ import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -50,32 +46,19 @@ public class AlibabaCloudSlsExternAlertService implements ExternAlertService {
private final AlarmCommonReduce alarmCommonReduce;
private static final AlibabaCloudSlsConverter CONVERTER = new AlibabaCloudSlsConverter();
public AlibabaCloudSlsExternAlertService(AlarmCommonReduce alarmCommonReduce) {
this.alarmCommonReduce = alarmCommonReduce;
}
@Override
public void addExternAlert(String content) {
List<AlibabaCloudSlsExternAlert> externAlerts = new ArrayList<>();
if (BooleanUtils.isTrue(JsonUtil.isArray(content))) {
TypeReference<List<AlibabaCloudSlsExternAlert>> typeReference = new TypeReference<>() {};
externAlerts = JsonUtil.fromJson(content, typeReference);
} else {
AlibabaCloudSlsExternAlert externAlert = JsonUtil.fromJson(content, AlibabaCloudSlsExternAlert.class);
if (null != externAlert) {
externAlerts.add(externAlert);
}
}
if (null == externAlerts || externAlerts.isEmpty()) {
AlibabaCloudSlsExternAlert externAlert = JsonUtil.fromJson(content, AlibabaCloudSlsExternAlert.class);
if (externAlert == null) {
log.warn("Failure to parse external alert content. content: {}", content);
return;
}
for (AlibabaCloudSlsExternAlert externAlert : externAlerts) {
SingleAlert singleAlert = CONVERTER.convert(externAlert);
alarmCommonReduce.reduceAndSendAlarm(singleAlert);
}
SingleAlert singleAlert = new AlibabaCloudSlsConverter().convert(externAlert);
alarmCommonReduce.reduceAndSendAlarm(singleAlert);
}
@Override
@@ -19,7 +19,7 @@ package org.apache.hertzbeat.alert.service.impl;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.entity.dto.sms.AlibabaSmsProperties;
import org.apache.hertzbeat.alert.config.AlibabaSmsProperties;
import org.apache.hertzbeat.alert.service.SmsClient;
import org.apache.hertzbeat.alert.util.CryptoUtils;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
@@ -236,4 +236,4 @@ public class AlibabaSmsClientImpl implements SmsClient {
public boolean checkConfig() {
return !(accessKeyId.isBlank() || accessKeySecret.isBlank() || signName.isBlank() || templateCode.isBlank());
}
}
}
@@ -20,7 +20,7 @@ package org.apache.hertzbeat.alert.service.impl;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.entity.dto.sms.AwsSmsProperties;
import org.apache.hertzbeat.alert.config.AwsSmsProperties;
import org.apache.hertzbeat.alert.service.SmsClient;
import org.apache.hertzbeat.alert.util.CryptoUtils;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
@@ -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);

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