mirror of
https://github.com/dromara/hertzbeat.git
synced 2026-09-17 18:19:02 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09103ae89c |
+9
-121
@@ -7,6 +7,7 @@
|
||||
"README.md",
|
||||
"README_CN.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"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -2545,8 +2550,7 @@
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/176664901?v=4",
|
||||
"profile": "https://github.com/Saramanda9988",
|
||||
"contributions": [
|
||||
"doc",
|
||||
"code"
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -2585,7 +2589,8 @@
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/95274591?v=4",
|
||||
"profile": "https://github.com/tuzuy",
|
||||
"contributions": [
|
||||
"code"
|
||||
"code",
|
||||
"design"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -2596,123 +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"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "KOYR",
|
||||
"name": "KOYR",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/53216619?v=4",
|
||||
"profile": "https://github.com/KOYR",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Lathika226",
|
||||
"name": "LathikaBaddam",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/178710568?v=4",
|
||||
"profile": "https://github.com/Lathika226",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Sahil-Shadwal",
|
||||
"name": "Sahil Shadwal",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/119167601?v=4",
|
||||
"profile": "https://shadwal.space/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "BhanuNidumolu",
|
||||
"name": "N.Bhanu Prasad",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/180380413?v=4",
|
||||
"profile": "https://github.com/BhanuNidumolu",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Prakash1185",
|
||||
"name": "Prakash Kumar",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/183058331?v=4",
|
||||
"profile": "https://prakashh-portfolio.vercel.app/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
{
|
||||
"name": "HertzBeat DevContainer",
|
||||
"name": "Hertzbeat DevContainer",
|
||||
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/java:1": {
|
||||
@@ -15,6 +15,7 @@
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"settings": {},
|
||||
// same extensions as Gitpod, should match /.gitpod.yml
|
||||
"extensions": [
|
||||
"vscjava.vscode-java-pack",
|
||||
"editorconfig.editorconfig",
|
||||
@@ -29,7 +30,7 @@
|
||||
},
|
||||
"portsAttributes": {
|
||||
"4200": {
|
||||
"label": "HertzBeat Web",
|
||||
"label": "Hertzbeat Web",
|
||||
"onAutoForward": "notify"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
# contributor license agreements. See the NOTICE file distributed with
|
||||
# this work for additional information regarding copyright ownership.
|
||||
# The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
# (the "License"); you may not use this file except in compliance with
|
||||
# the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
# The line max length is 120, match checkstyle rules.
|
||||
max_line_length = 120
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.json]
|
||||
tab_width = 2
|
||||
indent_size = 2
|
||||
|
||||
[*.{yml,yaml}]
|
||||
tab_width = 2
|
||||
indent_size = 2
|
||||
|
||||
[*.xml]
|
||||
ij_xml_attribute_wrap = off
|
||||
ij_xml_text_wrap = off
|
||||
ij_xml_keep_blank_lines = 1
|
||||
|
||||
[pom.xml]
|
||||
indent_size = 2
|
||||
|
||||
[*.java]
|
||||
indent_size = 4
|
||||
tab_width = 4
|
||||
|
||||
[*.js]
|
||||
indent_size = 2
|
||||
|
||||
[*.ts]
|
||||
indent_size = 2
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
/home export-ignore
|
||||
/hip export-ignore
|
||||
/template-marketplace export-ignore
|
||||
/.github export-ignore
|
||||
/.idea export-ignore
|
||||
/.devcontainer export-ignore
|
||||
|
||||
+10
-10
@@ -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:
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 194 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 315 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 282 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 262 KiB |
@@ -38,10 +38,6 @@ on:
|
||||
- 'script/**'
|
||||
- 'material/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
backend-build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -50,58 +46,16 @@ jobs:
|
||||
- uses: ./script/ci/github-actions/setup-deps
|
||||
|
||||
- name: Build with Maven
|
||||
run: mvnd clean -B package -pl '!hertzbeat-e2e' -Prelease -Dmaven.test.skip=false --file pom.xml
|
||||
run: mvnd clean -B package -Prelease -Dmaven.test.skip=false --file pom.xml
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v4.0.1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: false
|
||||
verbose: true
|
||||
|
||||
- name: Upload backend distribution
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: backend-dist-${{ github.run_id }}
|
||||
path: dist/
|
||||
retention-days: 1
|
||||
compression-level: 0
|
||||
|
||||
backend-maven-e2e:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./script/ci/github-actions/setup-deps
|
||||
|
||||
- name: Build backend Maven E2E modules
|
||||
run: |
|
||||
mvnd clean -B package \
|
||||
-pl hertzbeat-e2e/hertzbeat-collector-common-e2e,hertzbeat-e2e/hertzbeat-collector-kafka-e2e,hertzbeat-e2e/hertzbeat-collector-basic-e2e,hertzbeat-e2e/hertzbeat-collector-mysql-r2dbc-e2e,hertzbeat-e2e/hertzbeat-log-e2e \
|
||||
-am \
|
||||
-Dmaven.test.skip=false \
|
||||
--file pom.xml
|
||||
|
||||
- name: Upload Maven E2E coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v4.0.1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: false
|
||||
verbose: true
|
||||
|
||||
backend-image-e2e:
|
||||
needs: backend-build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download backend distribution
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: backend-dist-${{ github.run_id }}
|
||||
path: dist/
|
||||
|
||||
- name: Build Image
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: ./dist
|
||||
file: ./script/docker/server/Dockerfile
|
||||
@@ -110,10 +64,13 @@ jobs:
|
||||
|
||||
- name: Run E2E
|
||||
run: |
|
||||
sudo curl -L https://github.com/docker/compose/releases/download/v2.23.0/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose
|
||||
sudo chmod u+x /usr/local/bin/docker-compose
|
||||
|
||||
cd e2e
|
||||
docker compose version
|
||||
docker compose up --exit-code-from testing --remove-orphans
|
||||
|
||||
sudo docker-compose version
|
||||
sudo docker-compose up --exit-code-from testing --remove-orphans
|
||||
|
||||
# upload application logs
|
||||
- name: Upload logs & API test reports
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -123,3 +80,4 @@ jobs:
|
||||
path: |
|
||||
e2e/logs/
|
||||
e2e/report/
|
||||
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
name: Collector Native Release
|
||||
|
||||
run-name: Native collector release build (${{ github.ref_name }})
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-native-collector:
|
||||
name: Native collector (${{ matrix.platform }})
|
||||
permissions:
|
||||
contents: read
|
||||
timeout-minutes: 120
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux-amd64
|
||||
runner: ubuntu-24.04
|
||||
archive_ext: tar.gz
|
||||
- platform: linux-arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
archive_ext: tar.gz
|
||||
- platform: macos-amd64
|
||||
runner: macos-13
|
||||
archive_ext: tar.gz
|
||||
- platform: macos-arm64
|
||||
runner: macos-14
|
||||
archive_ext: tar.gz
|
||||
- platform: windows-amd64
|
||||
runner: windows-latest
|
||||
archive_ext: zip
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up GraalVM JDK 25
|
||||
uses: graalvm/setup-graalvm@186d0493a2df5eb62df5ecc498883d18fd58c303 # v1.6.2
|
||||
with:
|
||||
distribution: graalvm-community
|
||||
java-version: '25'
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
cache: maven
|
||||
native-image-job-reports: 'true'
|
||||
|
||||
- name: Verify toolchain
|
||||
shell: pwsh
|
||||
run: |
|
||||
java -version
|
||||
native-image --version
|
||||
mvn -version
|
||||
|
||||
- name: Build native collector package
|
||||
run: mvn -B -pl hertzbeat-collector/hertzbeat-collector-collector -am -Pnative -DskipTests package
|
||||
|
||||
- name: Locate native collector package
|
||||
id: package
|
||||
shell: pwsh
|
||||
run: |
|
||||
$package = Get-ChildItem -Path "dist/apache-hertzbeat-collector-native-*-${{ matrix.platform }}-bin.${{ matrix.archive_ext }}" | Select-Object -First 1
|
||||
if (-not $package) {
|
||||
throw "Native collector package not found for ${{ matrix.platform }}"
|
||||
}
|
||||
"archive=$($package.FullName)" >> $env:GITHUB_OUTPUT
|
||||
|
||||
- name: Upload native collector package
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: apache-hertzbeat-collector-native-${{ matrix.platform }}
|
||||
path: ${{ steps.package.outputs.archive }}
|
||||
retention-days: 14
|
||||
@@ -27,10 +27,6 @@ on:
|
||||
paths:
|
||||
- 'home/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
docs-build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -38,27 +34,14 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: pnpm
|
||||
cache-dependency-path: home/pnpm-lock.yaml
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.8'
|
||||
|
||||
- name: Check Markdown
|
||||
uses: DavidAnson/markdownlint-cli2-action@8de2aa07cae85fd17c0b35642db70cf5495f1d25 # v24.0.0
|
||||
uses: DavidAnson/markdownlint-cli2-action@v16
|
||||
with:
|
||||
globs: |
|
||||
./home/README.md
|
||||
./home/blog/**/*.md
|
||||
./home/docs/**/*.md
|
||||
./home/i18n/zh-cn/docusaurus-plugin-content-blog/**/*.md
|
||||
./home/i18n/zh-cn/docusaurus-plugin-content-docs/current/**/*.md
|
||||
globs: './home/**/*.md'
|
||||
|
||||
- name: Check filenames
|
||||
run: python ./script/ci/docs/check_file_name.py ./script/ci/docs/check_file_name.json
|
||||
@@ -66,15 +49,14 @@ jobs:
|
||||
- name: Dead Link Check
|
||||
run: |
|
||||
sudo npm install -g markdown-link-check@3.8.7
|
||||
find ./home/docs ./home/blog ./home/i18n/zh-cn/docusaurus-plugin-content-docs/current ./home/i18n/zh-cn/docusaurus-plugin-content-blog -name "*.md" > all_md_files.txt
|
||||
printf '%s\n' ./home/README.md >> all_md_files.txt
|
||||
find ./home -name "*.md" > all_md_files.txt
|
||||
grep -vFf ./script/ci/exclude_files.txt all_md_files.txt > to_check.txt
|
||||
xargs -P 8 -a to_check.txt -I{} markdown-link-check -c ./script/ci/link_check.json -q "{}"
|
||||
|
||||
- name: PNPM INSTALL
|
||||
- name: NPM INSTALL
|
||||
working-directory: home
|
||||
run: pnpm install
|
||||
run: npm install
|
||||
|
||||
- name: PNPM BUILD
|
||||
- name: NPM BUILD
|
||||
working-directory: home
|
||||
run: pnpm run build
|
||||
run: npm run build
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
name: DOC Deploy
|
||||
|
||||
on:
|
||||
# Trigger on push to master branch when home directory files are changed
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- 'home/**'
|
||||
|
||||
# Allow manual workflow trigger
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# Set permissions for deployment
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy Documentation to asf-site Branch
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# Checkout the repository code
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Setup Node.js environment
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: pnpm
|
||||
cache-dependency-path: home/pnpm-lock.yaml
|
||||
|
||||
# Setup pnpm
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: latest
|
||||
|
||||
# Install dependencies in home directory
|
||||
- name: Install Dependencies
|
||||
working-directory: home
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
# Build the documentation site
|
||||
- name: Build Documentation
|
||||
working-directory: home
|
||||
run: pnpm run build
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
|
||||
# Deploy to asf-site branch using peaceiris/actions-gh-pages
|
||||
- name: Deploy to asf-site Branch
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./home/build
|
||||
publish_branch: asf-site
|
||||
force_orphan: true
|
||||
user_name: 'github-actions[bot]'
|
||||
user_email: 'github-actions[bot]@users.noreply.github.com'
|
||||
commit_message: 'Deploy documentation to asf-site branch'
|
||||
|
||||
# Output deployment status
|
||||
- name: Deployment Summary
|
||||
run: |
|
||||
echo "Documentation deployed successfully!"
|
||||
echo "Deployment branch: asf-site"
|
||||
echo "Source branch: ${{ github.ref_name }}"
|
||||
echo "Commit SHA: ${{ github.sha }}"
|
||||
@@ -30,26 +30,14 @@ on:
|
||||
paths:
|
||||
- 'web-app/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: pnpm
|
||||
cache-dependency-path: web-app/pnpm-lock.yaml
|
||||
- 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
|
||||
|
||||
@@ -26,10 +26,6 @@ on:
|
||||
pull_request:
|
||||
branches: [ master, dev ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check-license-header:
|
||||
name: check-license-header
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
name: MCP Bash Server CI
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'mcp-servers/mcp-bash-server/**'
|
||||
- '.github/workflows/mcp-bashserver-test.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'mcp-servers/mcp-bash-server/**'
|
||||
- '.github/workflows/mcp-bashserver-test.yml'
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUST_VERSION: 1.88.0
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test MCP Bash Server
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
profile: [dev, release]
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable
|
||||
with:
|
||||
toolchain: ${{ env.RUST_VERSION }}
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Cache cargo registry
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-registry-
|
||||
|
||||
- name: Cache cargo build
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: target/
|
||||
key: ${{ runner.os }}-cargo-build-${{ matrix.profile }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-build-${{ matrix.profile }}-
|
||||
${{ runner.os }}-cargo-build-
|
||||
|
||||
- name: Check code format
|
||||
run: cargo fmt --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml --check
|
||||
|
||||
- name: Run cargo check
|
||||
run: |
|
||||
if [ "${{ matrix.profile }}" = "release" ]; then
|
||||
cargo check --release --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml
|
||||
else
|
||||
cargo check --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml
|
||||
fi
|
||||
|
||||
- name: Run clippy
|
||||
run: |
|
||||
if [ "${{ matrix.profile }}" = "release" ]; then
|
||||
cargo clippy --release --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml -- -D warnings
|
||||
else
|
||||
cargo clippy --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml -- -D warnings
|
||||
fi
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
if [ "${{ matrix.profile }}" = "release" ]; then
|
||||
cargo test --release --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml
|
||||
else
|
||||
cargo test --manifest-path ./mcp-servers/mcp-bash-server/Cargo.toml
|
||||
fi
|
||||
@@ -27,11 +27,6 @@ on:
|
||||
- cron: '0 1 * * *'
|
||||
push:
|
||||
branches: [ action* ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check-license-header:
|
||||
name: Monitor E2E Test
|
||||
@@ -41,9 +36,9 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./script/ci/github-actions/setup-deps
|
||||
- name: Build with Maven
|
||||
run: mvnd -B clean package -Prelease -Dmaven.test.skip=true --file pom.xml
|
||||
run: mvn clean -B package -Prelease -Dmaven.test.skip=true --file pom.xml
|
||||
- name: Build Image
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: ./dist
|
||||
file: ./script/docker/server/Dockerfile
|
||||
@@ -51,6 +46,8 @@ jobs:
|
||||
tags: apache/hertzbeat:test
|
||||
- name: Run K8s Monitor E2E Test
|
||||
run: |
|
||||
sudo curl -L https://github.com/docker/compose/releases/download/v2.23.0/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose
|
||||
sudo chmod u+x /usr/local/bin/docker-compose
|
||||
cd e2e/k8s
|
||||
docker compose version
|
||||
docker compose up --exit-code-from testing --remove-orphans
|
||||
sudo docker-compose version
|
||||
sudo docker-compose up --exit-code-from testing --remove-orphans
|
||||
|
||||
@@ -24,10 +24,6 @@ on:
|
||||
push:
|
||||
branches: [ action* ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -35,44 +31,39 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./script/ci/github-actions/setup-deps
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: pnpm
|
||||
cache-dependency-path: web-app/pnpm-lock.yaml
|
||||
|
||||
|
||||
- name: Build the Frontend
|
||||
run: |
|
||||
cd web-app
|
||||
pnpm install
|
||||
pnpm package
|
||||
|
||||
yarn install
|
||||
yarn package
|
||||
|
||||
- name: Build the Backend
|
||||
run: |
|
||||
mvnd -B clean package -Prelease,cluster -Dmaven.test.skip=false --file pom.xml
|
||||
mvn clean install
|
||||
mvn clean package -Prelease -DskipTests
|
||||
cd hertzbeat-collector
|
||||
mvn clean package -Pcluster -DskipTests
|
||||
|
||||
- uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a
|
||||
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USER }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
|
||||
- name: Build and Push Server
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./dist
|
||||
file: ./script/docker/server/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: apache/hertzbeat:nightly
|
||||
|
||||
|
||||
- name: Build and Push Collector
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./dist
|
||||
file: ./script/docker/collector/Dockerfile
|
||||
|
||||
Vendored
+2
-2
@@ -3,5 +3,5 @@ FROM gitpod/workspace-full
|
||||
USER gitpod
|
||||
|
||||
RUN bash -c ". /home/gitpod/.sdkman/bin/sdkman-init.sh && \
|
||||
sdk install java 21.0.2-tem && \
|
||||
sdk default java 21.0.2-tem"
|
||||
sdk install java 17.0.3-ms && \
|
||||
sdk default java 17.0.3-ms"
|
||||
|
||||
+4
-4
@@ -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
|
||||
@@ -44,7 +44,7 @@ vscode:
|
||||
|
||||
ports:
|
||||
- port: 4200
|
||||
name: HertzBeat
|
||||
name: Hertzbeat
|
||||
onOpen: open-browser
|
||||
|
||||
- port: 1157
|
||||
|
||||
@@ -39,7 +39,6 @@ header:
|
||||
- '**/*.js'
|
||||
- '**/*.less'
|
||||
- '**/*.txt'
|
||||
- '**/*.st'
|
||||
- '**/target/**'
|
||||
- '.gitattributes'
|
||||
- '**/.gitignore'
|
||||
@@ -67,7 +66,6 @@ header:
|
||||
- '.gitpod.yml'
|
||||
- 'karma.conf.js'
|
||||
- 'proxy.conf.js'
|
||||
- 'pnpm-lock.yaml'
|
||||
- '.helmignore'
|
||||
- 'script/helm/**'
|
||||
- 'web-app/src/app/core/**'
|
||||
|
||||
@@ -15,13 +15,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
{
|
||||
"globs": [
|
||||
"home/README.md",
|
||||
"home/blog/**/*.md",
|
||||
"home/docs/**/*.md",
|
||||
"home/i18n/zh-cn/docusaurus-plugin-content-blog/**/*.md",
|
||||
"home/i18n/zh-cn/docusaurus-plugin-content-docs/current/**/*.md"
|
||||
],
|
||||
"globs": ["home/**/*.md"],
|
||||
"config": {
|
||||
"default": true,
|
||||
"MD001": true,
|
||||
@@ -37,15 +31,14 @@
|
||||
"MD025": true,
|
||||
"MD029": true,
|
||||
"MD033": {
|
||||
"allowed_elements": ["table", "tr", "td", "a", "img", "sub", "b", "br", "img", "tbody", "mark", "font", "Tabs", "TabItem"]
|
||||
"allowed_elements": ["table", "tr", "td", "a", "img", "sub", "b", "br", "img", "tbody", "mark", "font"]
|
||||
},
|
||||
"MD036": false,
|
||||
"MD040": true,
|
||||
"MD045": true,
|
||||
"MD046": true,
|
||||
"MD047": true,
|
||||
"MD052": true,
|
||||
"MD060": false
|
||||
"MD052": true
|
||||
},
|
||||
"ignores": [
|
||||
"**/node_modules/**",
|
||||
|
||||
+111
-85
@@ -1,10 +1,10 @@
|
||||
## Contributor Guide [中文贡献者指南](#贡献者指南)
|
||||
## Contributor Guide [中文贡献者指南](#贡献者指南)
|
||||
|
||||
> We are committed to maintaining a happy community that helps each other, welcome every contributor to join us!
|
||||
> We are committed to maintaining a happy community that helps each other, welcome every contributor to join us!
|
||||
|
||||
### Kinds of Contributions
|
||||
### Kinds of Contributions
|
||||
|
||||
> In the HertzBeat community, there are many ways to contribute:
|
||||
> In the HertzBeat community, there are many ways to contribute:
|
||||
|
||||
- 💻**Code**: Can help the community complete some tasks, write new features or fix some bugs;
|
||||
|
||||
@@ -20,88 +20,90 @@
|
||||
|
||||
- 💬**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/en/reference/emoji-key/)
|
||||
More see [Contribution Types](https://allcontributors.org/docs/en/emoji-key)
|
||||
|
||||
Even small corrections to typos are very welcome :)
|
||||
Even small corrections to typos are very welcome :)
|
||||
|
||||
### Getting HertzBeat up and running
|
||||
### 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+`, `java25` and `lombok` environments
|
||||
1. Requires `maven3+`, `java17` and `lombok` environments
|
||||
|
||||
2. (Optional) Modify the configuration file: `hertzbeat-startup/src/main/resources/application.yml`
|
||||
2. (Optional) Modify the configuration file: `manager/src/main/resources/application.yml`
|
||||
|
||||
3. Execute under the project root directory: `mvn clean install -DskipTests`
|
||||
|
||||
4. Add VM Options: `--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED`
|
||||
|
||||
5. Start `springboot startup` service: `hertzbeat-startup/src/main/java/org/apache/hertzbeat/startup/HertzBeatApplication.java`
|
||||
5. Start `springboot manager` service: `manager/src/main/java/org/apache/hertzbeat/hertzbeat-manager/Manager.java`
|
||||
|
||||
#### Frontend start
|
||||
|
||||
1. Need `Node 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
|
||||
### Find tasks
|
||||
|
||||
Find the issue you are interested in! On our GitHub repo issue list, we often publish some issues with the label good first issue or status: volunteer wanted.
|
||||
These issues welcome the help of contributors. Among them, good first issues tend to have low thresholds and are suitable for novices.
|
||||
Find the issue you are interested in! On our GitHub repo issue list, we often publish some issues with the label good first issue or status: volunteer wanted.
|
||||
These issues welcome the help of contributors. Among them, good first issues tend to have low thresholds and are suitable for novices.
|
||||
|
||||
Of course, if you have a good idea, you can also propose it directly on GitHub Discussion or contact with community.
|
||||
Of course, if you have a good idea, you can also propose it directly on GitHub Discussion or contact with community.
|
||||
|
||||
### Submit Pull Request
|
||||
### Submit Pull Request
|
||||
|
||||
1. First you need to fork your target [hertzbeat repository](https://github.com/apache/hertzbeat).
|
||||
1. First you need to fork your target [hertzbeat repository](https://github.com/apache/hertzbeat).
|
||||
2. Then download the code locally with git command:
|
||||
```shell
|
||||
git clone git@github.com:${YOUR_USERNAME}/hertzbeat.git #Recommended
|
||||
git clone git@github.com:${YOUR_USERNAME}/hertzbeat.git #Recommended
|
||||
```
|
||||
3. After the download is complete, please refer to the getting started guide or README file of the target repository to initialize the project.
|
||||
3. After the download is complete, please refer to the getting started guide or README file of the target repository to initialize the project.
|
||||
4. Then, you can refer to the following command to submit the code:
|
||||
```shell
|
||||
git checkout -b a-feature-branch #Recommended
|
||||
git checkout -b a-feature-branch #Recommended
|
||||
```
|
||||
5. Submit the code as a commit, the commit message format specification required: [module name or type name]feature or bugfix or doc: custom message.
|
||||
5. Submit the coed as a commit, the commit message format specification required: [module name or type name]feature or bugfix or doc: custom message.
|
||||
```shell
|
||||
git add <modified file/path>
|
||||
git commit -m '[docs]feature: necessary instructions' #Recommended
|
||||
git add <modified file/path>
|
||||
git commit -m '[docs]feature: necessary instructions' #Recommended
|
||||
```
|
||||
6. Push to the remote repository
|
||||
6. Push to the remote repository
|
||||
```shell
|
||||
git push origin a-feature-branch
|
||||
git push origin a-feature-branch
|
||||
```
|
||||
7. Then you can initiate a new PR (Pull Request) on GitHub.
|
||||
7. Then you can initiate a new PR (Pull Request) on GitHub.
|
||||
|
||||
Please note that the title of the PR needs to conform to our spec, and write the necessary description in the PR to facilitate code review by Committers and other contributors.
|
||||
Please note that the title of the PR needs to conform to our spec, and write the necessary description in the PR to facilitate code review by Committers and other contributors.
|
||||
|
||||
### Wait for the code to be merged
|
||||
### Wait for the code to be merged
|
||||
|
||||
After submitting the PR, the Committee or the community's friends will review the code you submitted (Code Review), and will propose some modification suggestions or conduct some discussions. Please pay attention to your PR in time.
|
||||
After submitting the PR, the Committee or the community's friends will review the code you submitted (Code Review), and will propose some modification suggestions or conduct some discussions. Please pay attention to your PR in time.
|
||||
|
||||
If subsequent changes are required, there is no need to initiate a new PR. After submitting a commit on the original branch and pushing it to the remote repository, the PR will be automatically updated.
|
||||
If subsequent changes are required, there is no need to initiate a new PR. After submitting a commit on the original branch and pushing it to the remote repository, the PR will be automatically updated.
|
||||
|
||||
In addition, our project has a relatively standardized and strict CI inspection process. After submitting PR, CI will be triggered. Please pay attention to whether it passes the CI inspection.
|
||||
In addition, our project has a relatively standardized and strict CI inspection process. After submitting PR, CI will be triggered. Please pay attention to whether it passes the CI inspection.
|
||||
|
||||
Finally, the Committers can merge the PR into the master branch.
|
||||
Finally, the Committers can merge the PR into the master branch.
|
||||
|
||||
### After the code is merged
|
||||
### After the code is merged
|
||||
|
||||
After the code has been merged, you can delete the development branch on both the local and remote repositories:
|
||||
After the code has been merged, you can delete the development branch on both the local and remote repositories:
|
||||
|
||||
```shell
|
||||
git branch -d a-dev-branch
|
||||
@@ -112,18 +114,18 @@ On the master/main branch, you can do the following to sync the upstream reposit
|
||||
|
||||
```shell
|
||||
git remote add upstream https://github.com/apache/hertzbeat.git #Bind the remote warehouse, if it has been executed, it does not need to be executed again
|
||||
git checkout master
|
||||
git checkout master
|
||||
git pull upstream master
|
||||
```
|
||||
|
||||
### HertzBeat Improvement Proposal (HIP)
|
||||
If you have major new features(e.g., support metrics push gateway, support logs monitoring), you need to write a design document known as a HertzBeat Improvement Proposal (HIP). Before starting to write a HIP, make sure you follow the process [here](https://github.com/apache/hertzbeat/tree/master/hip).
|
||||
|
||||
### How to become a Committer?
|
||||
### How to become a Committer?
|
||||
|
||||
With the above steps, you are a contributor to HertzBeat. Repeat the previous steps to stay active in the community, keep at, you can become a Committer!
|
||||
With the above steps, you are a contributor to HertzBeat. Repeat the previous steps to stay active in the community, keep at, you can become a Committer!
|
||||
|
||||
### Join Discussion
|
||||
### Join Discussion
|
||||
|
||||
[Join the Mailing Lists](https://lists.apache.org/list.html?dev@hertzbeat.apache.org) : Mail to `dev-subscribe@hertzbeat.apache.org` to subscribe mailing lists.
|
||||
|
||||
@@ -131,21 +133,32 @@ 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.
|
||||
|
||||

|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
------
|
||||
|
||||
<br>
|
||||
|
||||
## 贡献者指南
|
||||
## 贡献者指南
|
||||
|
||||
> 非常欢迎参与项目贡献,我们致力于维护一个互相帮助的快乐社区。
|
||||
> 非常欢迎参与项目贡献,我们致力于维护一个互相帮助的快乐社区。
|
||||
|
||||
### 贡献方式
|
||||
### 贡献方式
|
||||
|
||||
> 在 HertzBeat 社区,贡献方式有很多:
|
||||
> 在 HertzBeat 社区,贡献方式有很多:
|
||||
|
||||
- 💻**代码**:可以帮助社区完成一些任务、编写新的feature或者是修复一些bug;
|
||||
|
||||
@@ -159,109 +172,111 @@ Add WeChat account `ahertzbeat` to pull you into the WeChat group.
|
||||
|
||||
- 💡**布道**:可以帮助宣传或推广 HertzBeat 社区,在 meetup 或 summit 中演讲;
|
||||
|
||||
- 💬**建议**:也可以对项目或者社区提出一些建议,促进社区的良性发展;
|
||||
- 💬**建议**:也可以对项目或者社区提出一些建议,促进社区的良性发展;
|
||||
|
||||
更多贡献方式参见 [Contribution Types](https://allcontributors.org/en/reference/emoji-key/)
|
||||
更多贡献方式参见 [Contribution Types](https://allcontributors.org/docs/en/emoji-key)
|
||||
|
||||
即便是小到错别字的修正我们也都非常欢迎 :)
|
||||
即便是小到错别字的修正我们也都非常欢迎 :)
|
||||
|
||||
### 让 HertzBeat 运行起来
|
||||
### 让 HertzBeat 运行起来
|
||||
|
||||
> 让 HertzBeat 的代码在您的开发工具上运行起来,并且能够断点调试。
|
||||
> 此为前后端分离项目,本地代码启动需将后端[hertzbeat-startup](hertzbeat-startup)和前端[web-app](web-app)分别启动生效。
|
||||
> 让 HertzBeat 的代码在您的开发工具上运行起来,并且能够断点调试。
|
||||
> 此为前后端分离项目,本地代码启动需将后端[manager](manager)和前端[web-app](web-app)分别启动生效。
|
||||
|
||||
#### 后端启动
|
||||
|
||||
1. 需要 `maven3+`, `java25` 和 `lombok` 环境
|
||||
1. 需要 `maven3+`, `java17` 和 `lombok` 环境
|
||||
|
||||
2. (可选)修改配置文件配置信息-`hertzbeat-startup/src/main/resources/application.yml`
|
||||
2. (可选)修改配置文件配置信息-`manager/src/main/resources/application.yml`
|
||||
|
||||
3. 在项目根目录运行编译: `mvn clean install -DskipTests`
|
||||
|
||||
4. 在 `jvm` 加入参数 `--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED`
|
||||
|
||||
5. 启动`springboot startup`服务 `hertzbeat-startup/src/main/java/org/apache/hertzbeat/startup/HertzBeatApplication.java`
|
||||
5. 启动`springboot manager`服务 `manager/src/main/java/org/apache/hertzbeat/hertzbeat-manager/Manager.java`
|
||||
|
||||
|
||||
#### 前端启动
|
||||
|
||||
1. 需要 `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*
|
||||
|
||||
### 寻找任务
|
||||
|
||||
寻找您感兴趣的Issue!在我们的GitHub仓库和邮件列表中,我们经常会发布一些带有 good first issue 或者 status: volunteer wanted 标签的issue,这些issue都欢迎贡献者的帮助。
|
||||
其中good first issue往往门槛较低、适合新手。
|
||||
其中good first issue往往门槛较低、适合新手。
|
||||
|
||||
当然,如果您有好的想法,也可以直接在GitHub Discussion 中提出或者联系社区。
|
||||
当然,如果您有好的想法,也可以直接在GitHub Discussion 中提出或者联系社区。
|
||||
|
||||
### 提交 Pull Request
|
||||
|
||||
1. 首先您需要 Fork 目标仓库 [hertzbeat repository](https://github.com/apache/hertzbeat).
|
||||
2. 然后 用git命令 将代码下载到本地:
|
||||
2. 然后 用git命令 将代码下载到本地:
|
||||
```shell
|
||||
git clone git@github.com:${YOUR_USERNAME}/hertzbeat.git #Recommended
|
||||
git clone git@github.com:${YOUR_USERNAME}/hertzbeat.git #Recommended
|
||||
```
|
||||
3. 下载完成后,请参考目标仓库的入门指南或者 README 文件对项目进行初始化。
|
||||
4. 接着,您可以参考如下命令进行代码的提交, 切换新的分支, 进行开发:
|
||||
4. 接着,您可以参考如下命令进行代码的提交, 切换新的分支, 进行开发:
|
||||
```shell
|
||||
git checkout -b a-feature-branch #Recommended
|
||||
git checkout -b a-feature-branch #Recommended
|
||||
```
|
||||
5. 提交 commit , commit 描述信息需要符合约定格式: [module name or type name]feature or bugfix or doc: custom message.
|
||||
5. 提交 commit , commit 描述信息需要符合约定格式: [module name or type name]feature or bugfix or doc: custom message.
|
||||
```shell
|
||||
git add <modified file/path>
|
||||
git commit -m '[docs]feature: necessary instructions' #Recommended
|
||||
git add <modified file/path>
|
||||
git commit -m '[docs]feature: necessary instructions' #Recommended
|
||||
```
|
||||
6. 推送到远程仓库
|
||||
6. 推送到远程仓库
|
||||
```shell
|
||||
git push origin a-feature-branch
|
||||
git push origin a-feature-branch
|
||||
```
|
||||
7. 然后您就可以在 GitHub 上发起新的 PR (Pull Request)。
|
||||
|
||||
请注意 PR 的标题需要符合我们的规范,并且在 PR 中写上必要的说明,来方便 Committer 和其他贡献者进行代码审查。
|
||||
请注意 PR 的标题需要符合我们的规范,并且在 PR 中写上必要的说明,来方便 Committer 和其他贡献者进行代码审查。
|
||||
|
||||
### 等待PR代码被合并
|
||||
### 等待PR代码被合并
|
||||
|
||||
在提交了 PR 后,Committer 或者社区的小伙伴们会对您提交的代码进行审查(Code Review),会提出一些修改建议,或者是进行一些讨论,请及时关注您的PR。
|
||||
在提交了 PR 后,Committer 或者社区的小伙伴们会对您提交的代码进行审查(Code Review),会提出一些修改建议,或者是进行一些讨论,请及时关注您的PR。
|
||||
|
||||
若后续需要改动,不需要发起一个新的 PR,在原有的分支上提交 commit 并推送到远程仓库后,PR会自动更新。
|
||||
|
||||
另外,我们的项目有比较规范和严格的 CI 检查流程,在提交 PR 之后会触发 CI,请注意是否通过 CI 检查。
|
||||
|
||||
最后,Committer 可以将 PR 合并入 master 主分支。
|
||||
最后,Committer 可以将 PR 合并入 master 主分支。
|
||||
|
||||
### 代码被合并后
|
||||
### 代码被合并后
|
||||
|
||||
在代码被合并后,您就可以在本地和远程仓库删除这个开发分支了:
|
||||
在代码被合并后,您就可以在本地和远程仓库删除这个开发分支了:
|
||||
|
||||
```shell
|
||||
git branch -d a-dev-branch
|
||||
git push origin --delete a-dev-branch
|
||||
```
|
||||
|
||||
在主分支上,您可以执行以下操作来同步上游仓库:
|
||||
在主分支上,您可以执行以下操作来同步上游仓库:
|
||||
|
||||
```shell
|
||||
git remote add upstream https://github.com/apache/hertzbeat.git #Bind the remote warehouse, if it has been executed, it does not need to be executed again
|
||||
git checkout master
|
||||
git checkout master
|
||||
git pull upstream master
|
||||
```
|
||||
|
||||
### HertzBeat 改进提案 (HIP)
|
||||
如果您有重大的新特性(例如,支持指标推送网关,日志监控等),您需要编写一个被称为 HertzBeat 改进提案(HertzBeat Improvement Proposal,HIP)的设计文档。在开始编写 HIP 之前,请确保您遵循了[这里](https://github.com/apache/hertzbeat/tree/master/hip)的流程。
|
||||
|
||||
### 如何成为Committer?
|
||||
### 如何成为Committer?
|
||||
|
||||
通过上述步骤,您就是 HertzBeat 的贡献者了。重复前面的步骤,在社区中保持活跃,坚持下去,您就能成为 Committer!
|
||||
通过上述步骤,您就是 HertzBeat 的贡献者了。重复前面的步骤,在社区中保持活跃,坚持下去,您就能成为 Committer!
|
||||
|
||||
### 加入讨论交流
|
||||
|
||||
@@ -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)** 提供可视化控制台页面
|
||||
> 监控告警系统可视化控制台前端
|
||||
|
||||

|
||||
|
||||
@@ -9,7 +9,10 @@
|
||||
<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.
|
||||
|
||||
[](https://discord.gg/Fb6M73htGr)
|
||||
[](https://www.reddit.com/r/hertzbeat/)
|
||||
[](https://x.com/hertzbeat1024)
|
||||
[](https://www.bestpractices.dev/projects/8139)
|
||||
[](https://app.codecov.io/gh/apache/hertzbeat)
|
||||
@@ -18,41 +21,45 @@
|
||||
[](https://www.youtube.com/channel/UCri75zfWX0GHqJFPENEbLow)
|
||||
[](https://gitpod.io/#https://github.com/apache/hertzbeat)
|
||||
[](https://deepwiki.com/apache/hertzbeat)
|
||||
[](https://zread.ai/apache/hertzbeat)
|
||||
|
||||
**Home: [hertzbeat.apache.org](https://hertzbeat.apache.org)**
|
||||
**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**
|
||||
|
||||
|
||||
## 🎡 <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.
|
||||
|
||||
----
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
----
|
||||
|
||||
## 🥐 Architecture
|
||||
## 🥐 Architecture
|
||||
|
||||

|
||||

|
||||
|
||||
## ⛄ Supported
|
||||
## ⛄ 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),
|
||||
@@ -92,18 +99,18 @@
|
||||
[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)
|
||||
- And More Your Custom Template.
|
||||
- Notified Support `Discord` `Slack` `Telegram` `Email` `DingTalk` `WeChat` `FeiShu` `Webhook` `SMS` `ServerChan`.
|
||||
- Notified Support `Discord` `Slack` `Telegram` `Email` `Dingtalk` `WeChat` `FeiShu` `Webhook` `SMS` `ServerChan`.
|
||||
|
||||
|
||||
## 🐕 Quick Start
|
||||
## 🐕 Quick Start
|
||||
|
||||
- If you wish to deploy HertzBeat locally, please refer to the following Deployment Documentation for instructions.
|
||||
|
||||
### 🍞 Install HertzBeat
|
||||
### 🍞 Install HertzBeat
|
||||
|
||||
> HertzBeat supports installation through source code, docker or package, cpu support x86/arm64.
|
||||
> HertzBeat supports installation through source code, docker or package, cpu support x86/arm64.
|
||||
|
||||
##### 1:Install quickly via docker
|
||||
##### 1:Install quickly via docker
|
||||
|
||||
1. Just one command to get started
|
||||
|
||||
@@ -111,7 +118,7 @@
|
||||
docker run -d -p 1157:1157 -p 1158:1158 --name hertzbeat apache/hertzbeat
|
||||
```
|
||||
|
||||
2. Access `http://localhost:1157` to start, default account: `admin/hertzbeat`
|
||||
2. Access `http://localhost:1157` to start, default account: `admin/hertzbeat`
|
||||
|
||||
3. Deploy collector clusters (Optional)
|
||||
|
||||
@@ -125,16 +132,16 @@
|
||||
- `-e MANAGER_PORT=1158` : set the main hertzbeat server port, default 1158.
|
||||
|
||||
|
||||
Detailed config refer to [Install HertzBeat via Docker](https://hertzbeat.apache.org/docs/start/docker-deploy)
|
||||
Detailed config refer to [Install HertzBeat via Docker](https://hertzbeat.apache.org/docs/start/docker-deploy)
|
||||
|
||||
##### 2:Install via package
|
||||
##### 2:Install via package
|
||||
|
||||
1. Download the release package `apache-hertzbeat-xx-bin.tar.gz` [Download](https://hertzbeat.apache.org/docs/download)
|
||||
2. Configure the HertzBeat configuration yml file `hertzbeat/config/application.yml` (optional)
|
||||
3. Run command `$ ./bin/startup.sh ` or `bin/startup.bat`
|
||||
4. Access `http://localhost:1157` to start, default account: `admin/hertzbeat`
|
||||
1. Download the release package `hertzbeat-xx.tar.gz` [Download](https://hertzbeat.apache.org/docs/download)
|
||||
2. Configure the HertzBeat configuration yml file `hertzbeat/config/application.yml` (optional)
|
||||
3. Run command `$ ./bin/startup.sh ` or `bin/startup.bat`
|
||||
4. Access `http://localhost:1157` to start, default account: `admin/hertzbeat`
|
||||
5. Deploy collector clusters (Optional)
|
||||
- Download the release package `apache-hertzbeat-collector-xx-bin.tar.gz` (JVM collector) or the native collector package for your platform, such as `apache-hertzbeat-collector-native-xx-linux-amd64-bin.tar.gz` or `apache-hertzbeat-collector-native-xx-windows-amd64-bin.zip`, to the new machine [Download](https://hertzbeat.apache.org/docs/download)
|
||||
- Download the release package `hertzbeat-collector-xx.tar.gz` to new machine [Download](https://hertzbeat.apache.org/docs/download)
|
||||
- Configure the collector configuration yml file `hertzbeat-collector/config/application.yml`: unique `identity` name, running `mode` (public or private), hertzbeat `manager-host`, hertzbeat `manager-port`
|
||||
```yaml
|
||||
collector:
|
||||
@@ -147,27 +154,25 @@ Detailed config refer to [Install HertzBeat via Docker](https://hertzbeat.apache
|
||||
manager-host: ${MANAGER_HOST:127.0.0.1}
|
||||
manager-port: ${MANAGER_PORT:1158}
|
||||
```
|
||||
- If you do not provide JDBC drivers in `ext-lib`, MySQL, MariaDB, and OceanBase can use the built-in query engine and run on the native collector package as well. TiDB follows the same rule for its SQL query metric set.
|
||||
- If `mysql-connector-j` is present in `ext-lib`, the built-in server collector or JVM collector automatically prefers JDBC after restart for MySQL, MariaDB, and OceanBase. TiDB follows the same rule for its SQL query metric set, while its HTTP metrics are unchanged. Oracle and DB2 still require the JVM collector package because they depend on external JDBC drivers.
|
||||
- Run `$ ./bin/startup.sh ` or `bin/startup.bat` for the JVM collector package. Run `$ ./bin/startup.sh ` for Linux or macOS native collector packages, and `bin\\startup.bat` for the Windows native collector package.
|
||||
- Access `http://localhost:1157` and you will see the registered new collector in dashboard
|
||||
- Run command `$ ./bin/startup.sh ` or `bin/startup.bat`
|
||||
- Access `http://localhost:1157` and you will see the registered new collector in dashboard
|
||||
|
||||
Detailed config refer to [Install HertzBeat via Package](https://hertzbeat.apache.org/docs/start/package-deploy)
|
||||
Detailed config refer to [Install HertzBeat via Package](https://hertzbeat.apache.org/docs/start/package-deploy)
|
||||
|
||||
##### 3:Start via source code
|
||||
##### 3:Start via source code
|
||||
|
||||
1. Local source code debugging needs to start the back-end project `hertzbeat-startup` and the front-end project `web-app`.
|
||||
2. Backend:need `maven3+`, `java25`, `lombok`, add VM options in IDE: ` --add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED `, then start the `hertzbeat-startup` service.
|
||||
3. Web:need `nodejs` and `pnpm` environment, run `pnpm install` then `pnpm start` in `web-app` directory after backend startup.
|
||||
4. Access `http://localhost:4200` to start, default account: `admin/hertzbeat`
|
||||
1. Local source code debugging needs to start the back-end project `manager` and the front-end project `web-app`.
|
||||
2. Backend:need `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. Web:need `nodejs npm angular-cli` environment, Run `ng serve --open` in `web-app` directory after backend startup.
|
||||
4. Access `http://localhost:4200` to start, default account: `admin/hertzbeat`
|
||||
|
||||
Detailed steps refer to [CONTRIBUTING](CONTRIBUTING.md)
|
||||
Detailed steps refer to [CONTRIBUTING](CONTRIBUTING.md)
|
||||
|
||||
##### 4:Install All(hertzbeat+postgresql+tsdb) via Docker-compose
|
||||
##### 4:Install All(hertzbeat+postgresql+tsdb) via Docker-compose
|
||||
|
||||
Install the postgresql/mysql database, victoria-metrics/iotdb/tdengine database and hertzbeat at one time through [docker-compose deployment script](script/docker-compose).
|
||||
|
||||
Detailed steps refer to [Install via Docker-Compose](script/docker-compose/README.md)
|
||||
Detailed steps refer to [Install via Docker-Compose](script/docker-compose/README.md)
|
||||
|
||||
##### 5: Install All(hertzbeat+collector+postgresql+tsdb) via kubernetes helm charts
|
||||
|
||||
@@ -179,8 +184,8 @@ Detailed steps refer to [Artifact Hub](https://artifacthub.io/packages/helm/hert
|
||||
|
||||
## ✨ Contributors
|
||||
|
||||
Thanks to these wonderful people, welcome to join us:
|
||||
[Contributor Guide](CONTRIBUTING.md)
|
||||
Thanks to these wonderful people, welcome to join us:
|
||||
[Contributor Guide](CONTRIBUTING.md)
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
<!-- prettier-ignore-start -->
|
||||
@@ -356,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/a-little-fool"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="小笨蛋"/><br /><sub><b>小笨蛋</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=a-little-fool" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=a-little-fool" title="Documentation">📖</a> <a href="#blog-a-little-fool" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aa-little-fool" title="Bug reports">🐛</a> <a href="#design-a-little-fool" title="Design">🎨</a></td>
|
||||
</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>
|
||||
@@ -380,7 +385,7 @@ Thanks to these wonderful people, welcome to join us:
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1036664317"><img src="https://avatars.githubusercontent.com/u/7696697?v=4?s=100" width="100px;" alt="moghn"/><br /><sub><b>moghn</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1036664317" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaoguolong"><img src="https://avatars.githubusercontent.com/u/33684988?v=4?s=100" width="100px;" alt="xiaoguolong"/><br /><sub><b>xiaoguolong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaoguolong" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Clownsw"><img src="https://avatars.githubusercontent.com/u/28394742?v=4?s=100" width="100px;" alt="Smliexx"/><br /><sub><b>Smliexx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Clownsw" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AClownsw" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Calvin979"><img src="https://avatars.githubusercontent.com/u/131688897?v=4?s=100" width="100px;" alt="Calvin"/><br /><sub><b>Calvin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Code">💻</a> <a href="#design-Calvin979" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACalvin979" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Calvin979"><img src="https://avatars.githubusercontent.com/u/131688897?v=4?s=100" width="100px;" alt="Naruse"/><br /><sub><b>Naruse</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Code">💻</a> <a href="#design-Calvin979" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACalvin979" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bbelide2"><img src="https://avatars.githubusercontent.com/u/26840796?v=4?s=100" width="100px;" alt="Bala Sukesh"/><br /><sub><b>Bala Sukesh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bbelide2" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jinyaoMa"><img src="https://avatars.githubusercontent.com/u/25066570?v=4?s=100" width="100px;" alt="Jinyao Ma"/><br /><sub><b>Jinyao Ma</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jinyaoMa" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://linuxsuren.github.io/open-source-best-practice/"><img src="https://avatars.githubusercontent.com/u/1450685?v=4?s=100" width="100px;" alt="Rick"/><br /><sub><b>Rick</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Tests">⚠️</a></td>
|
||||
@@ -524,33 +529,6 @@ Thanks to these wonderful people, welcome to join us:
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaomizhou2"><img src="https://avatars.githubusercontent.com/u/47807926?v=4?s=100" width="100px;" alt="zhangyaxi"/><br /><sub><b>zhangyaxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RainBondsongyg"><img src="https://avatars.githubusercontent.com/u/94501396?v=4?s=100" width="100px;" alt="songyg"/><br /><sub><b>songyg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=RainBondsongyg" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lx1229"><img src="https://avatars.githubusercontent.com/u/44620005?v=4?s=100" width="100px;" alt="Liuxin"/><br /><sub><b>Liuxin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lx1229" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yy549159265"><img src="https://avatars.githubusercontent.com/u/40821310?v=4?s=100" width="100px;" alt="yy549159265"/><br /><sub><b>yy549159265</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Tests">⚠️</a> <a href="#design-yy549159265" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cto-huhang"><img src="https://avatars.githubusercontent.com/u/53338629?v=4?s=100" width="100px;" alt="cto-huhang"/><br /><sub><b>cto-huhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cto-huhang" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Saramanda9988"><img src="https://avatars.githubusercontent.com/u/176664901?v=4?s=100" width="100px;" alt="LunaRain_079"/><br /><sub><b>LunaRain_079</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/delei"><img src="https://avatars.githubusercontent.com/u/17263766?v=4?s=100" width="100px;" alt="DeleiGuo"/><br /><sub><b>DeleiGuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Adelei" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chingjustwe"><img src="https://avatars.githubusercontent.com/u/13643747?v=4?s=100" width="100px;" alt="Rocky, Chi"/><br /><sub><b>Rocky, Chi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=chingjustwe" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rowankid"><img src="https://avatars.githubusercontent.com/u/18652781?v=4?s=100" width="100px;" alt="Wenqi Luo"/><br /><sub><b>Wenqi Luo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3Arowankid" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tuzuy"><img src="https://avatars.githubusercontent.com/u/95274591?v=4?s=100" width="100px;" alt="tuzuy"/><br /><sub><b>tuzuy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tuzuy" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/carlpinto25"><img src="https://avatars.githubusercontent.com/u/117299909?v=4?s=100" width="100px;" alt="carl pinto"/><br /><sub><b>carl pinto</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=carlpinto25" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://cxhello.top/"><img src="https://avatars.githubusercontent.com/u/49056040?v=4?s=100" width="100px;" alt="cxhello"/><br /><sub><b>cxhello</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cxhello" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jl15988"><img src="https://avatars.githubusercontent.com/u/70638770?v=4?s=100" width="100px;" alt="会功夫的李白"/><br /><sub><b>会功夫的李白</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jl15988" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://blog.aytop.cloud/"><img src="https://avatars.githubusercontent.com/u/37127008?v=4?s=100" width="100px;" alt="Albert.Yang"/><br /><sub><b>Albert.Yang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=AlbertYang0801" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://blog.tokenlen.top/"><img src="https://avatars.githubusercontent.com/u/150590575?v=4?s=100" width="100px;" alt="zhou yong kang"/><br /><sub><b>zhou yong kang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mengnankkkk" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/warrobe"><img src="https://avatars.githubusercontent.com/u/89446159?v=4?s=100" width="100px;" alt="warrobe"/><br /><sub><b>warrobe</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=warrobe" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Jetiaime"><img src="https://avatars.githubusercontent.com/u/93769000?v=4?s=100" width="100px;" alt="TeAmo"/><br /><sub><b>TeAmo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Jetiaime" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pentium100"><img src="https://avatars.githubusercontent.com/u/27917?v=4?s=100" width="100px;" alt="pentium100"/><br /><sub><b>pentium100</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pentium100" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dedyks"><img src="https://avatars.githubusercontent.com/u/23741665?v=4?s=100" width="100px;" alt="Dedy Kurniawan Santoso"/><br /><sub><b>Dedy Kurniawan Santoso</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dedyks" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KOYR"><img src="https://avatars.githubusercontent.com/u/53216619?v=4?s=100" width="100px;" alt="KOYR"/><br /><sub><b>KOYR</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=KOYR" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Lathika226"><img src="https://avatars.githubusercontent.com/u/178710568?v=4?s=100" width="100px;" alt="LathikaBaddam"/><br /><sub><b>LathikaBaddam</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Lathika226" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://shadwal.space/"><img src="https://avatars.githubusercontent.com/u/119167601?v=4?s=100" width="100px;" alt="Sahil Shadwal"/><br /><sub><b>Sahil Shadwal</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Sahil-Shadwal" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BhanuNidumolu"><img src="https://avatars.githubusercontent.com/u/180380413?v=4?s=100" width="100px;" alt="N.Bhanu Prasad"/><br /><sub><b>N.Bhanu Prasad</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=BhanuNidumolu" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://prakashh-portfolio.vercel.app/"><img src="https://avatars.githubusercontent.com/u/183058331?v=4?s=100" width="100px;" alt="Prakash Kumar"/><br /><sub><b>Prakash Kumar</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Prakash1185" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -558,7 +536,7 @@ Thanks to these wonderful people, welcome to join us:
|
||||
<!-- markdownlint-restore -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
## 💬 Join discussion
|
||||
|
||||
@@ -570,10 +548,14 @@ Thanks to these wonderful people, welcome to join us:
|
||||
|
||||
WeChat Group : Add friend `ahertzbeat` and invite to the group.
|
||||
|
||||
WeChat Official Account : Search ID `usthecom`.
|
||||
WeChat Public : Search ID `usthecom`.
|
||||
|
||||
[QQ Group](https://qm.qq.com/q/xxqecSC2cw) : Group num `1035688434`
|
||||
|
||||
[Github Discussion](https://github.com/apache/hertzbeat/discussions)
|
||||
|
||||
[Reddit Community](https://www.reddit.com/r/hertzbeat/)
|
||||
|
||||
[Follow Us Twitter](https://x.com/hertzbeat1024)
|
||||
|
||||
[Subscribe YouTube](https://www.youtube.com/channel/UCri75zfWX0GHqJFPENEbLow)
|
||||
|
||||
+31
-48
@@ -9,7 +9,10 @@
|
||||
<a href="README.md">English</a> | <b><a href="README_CN.md">中文</a></b> | <a href="README_JP.md">日本語</a>
|
||||
</p>
|
||||
|
||||
> 实时观测系统,无需 Agent,性能集群,兼容 Prometheus,自定义监控和状态页构建能力。
|
||||
|
||||
[](https://discord.gg/Fb6M73htGr)
|
||||
[](https://www.reddit.com/r/hertzbeat/)
|
||||
[](https://x.com/hertzbeat1024)
|
||||
[](https://www.bestpractices.dev/projects/8139)
|
||||
[](https://app.codecov.io/gh/apache/hertzbeat)
|
||||
@@ -25,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`的强大自定义,多类型支持,高性能,易扩展,低耦合,希望能帮助用户快速搭建自有监控系统。
|
||||
|
||||
----
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
----
|
||||
|
||||
## 🥐 模块
|
||||
@@ -51,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),
|
||||
@@ -90,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酱`。
|
||||
|
||||
## 🐕 快速开始
|
||||
@@ -126,12 +134,12 @@
|
||||
|
||||
##### 方式二:通过安装包安装
|
||||
|
||||
1. 下载您系统环境对应的安装包 `apache-hertzbeat-xx-bin.tar.gz` [Download](https://hertzbeat.apache.org/docs/download)
|
||||
1. 下载您系统环境对应的安装包 `hertzbeat-xx.tar.gz` [Download](https://hertzbeat.apache.org/docs/download)
|
||||
2. 配置 HertzBeat 的配置文件 `hertzbeat/config/application.yml` (可选)
|
||||
3. 部署启动 `$ ./bin/startup.sh ` 或 `bin/startup.bat`
|
||||
4. 浏览器访问 `http://localhost:1157` 即可开始,默认账号密码 `admin/hertzbeat`
|
||||
5. 部署采集器集群(可选)
|
||||
- 下载采集器安装包 `apache-hertzbeat-collector-xx-bin.tar.gz`(JVM 采集器)或与你目标平台匹配的 Native 采集器安装包,例如 `apache-hertzbeat-collector-native-xx-linux-amd64-bin.tar.gz`、`apache-hertzbeat-collector-native-xx-windows-amd64-bin.zip`,到规划的另一台部署主机上 [Download](https://hertzbeat.apache.org/docs/download)
|
||||
- 下载采集器安装包 `hertzbeat-collector-xx.tar.gz` 到规划的另一台部署主机上 [Download](https://hertzbeat.apache.org/docs/download)
|
||||
- 配置采集器的配置文件 `hertzbeat-collector/config/application.yml` 里面的连接主 HertzBeat 服务的对外 IP,端口,当前采集器名称(需保证唯一性)等参数 `identity` `mode` (public or private) `manager-host` `manager-port`
|
||||
```yaml
|
||||
collector:
|
||||
@@ -144,18 +152,16 @@
|
||||
manager-host: ${MANAGER_HOST:127.0.0.1}
|
||||
manager-port: ${MANAGER_PORT:1158}
|
||||
```
|
||||
- 如果没有在 `ext-lib` 中提供 JDBC 驱动,MySQL、MariaDB、OceanBase 可以直接使用内置查询引擎,也可以使用 Native 采集器安装包;TiDB 的 SQL 查询指标也遵循同样规则。
|
||||
- 如果在 `ext-lib` 中放入了 `mysql-connector-j`,主程序内置采集器或 JVM 采集器会在重启后自动优先走 JDBC;这一点现在适用于 MySQL、MariaDB、OceanBase,TiDB 的 SQL 查询指标也遵循同样规则,而它的 HTTP 指标不受影响。Oracle、DB2 仍然必须使用 JVM 采集器安装包,因为它们依赖外置 JDBC 驱动。
|
||||
- JVM 采集器安装包使用 `$ ./bin/startup.sh ` 或 `bin/startup.bat` 启动。Linux 或 macOS 的 Native 采集器安装包使用 `$ ./bin/startup.sh ` 启动,Windows 的 Native 采集器安装包使用 `bin\\startup.bat` 启动
|
||||
- 启动 `$ ./bin/startup.sh ` 或 `bin/startup.bat`
|
||||
- 浏览器访问主 HertzBeat 服务 `http://localhost:1157` 查看概览页面即可看到注册上来的新采集器
|
||||
|
||||
更多配置详细步骤参考 [通过安装包安装HertzBeat](https://hertzbeat.apache.org/docs/start/package-deploy)
|
||||
|
||||
##### 方式三:本地代码启动
|
||||
|
||||
1. 此为前后端分离项目,本地代码调试需要分别启动后端工程 `hertzbeat-startup` 和前端工程 `web-app`
|
||||
2. 后端:需要 `maven3+`, `java25` 和 `lombok` 环境,修改 `YML` 配置信息,添加JVM参数`--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED`后启动 `hertzbeat-startup` 服务即可。
|
||||
3. 前端:需要 `nodejs` 和 `pnpm` 环境,待本地后端启动后,在 `web-app` 目录下执行 `pnpm install` 再执行 `pnpm start`
|
||||
1. 此为前后端分离项目,本地代码调试需要分别启动后端工程 `manager` 和前端工程 `web-app`
|
||||
2. 后端:需要 `maven3+`, `java17` 和 `lombok` 环境,修改 `YML` 配置信息,添加JVM参数`--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED`后启动 `manager` 服务即可。
|
||||
3. 前端:需要 `nodejs npm angular-cli`环境,待本地后端启动后,在 `web-app` 目录下启动 `ng serve --open`
|
||||
4. 浏览器访问 `http://localhost:4200` 即可开始,默认账号密码 `admin/hertzbeat`
|
||||
|
||||
详细步骤参考 [参与贡献之本地代码启动](CONTRIBUTING.md)
|
||||
@@ -353,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/a-little-fool"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="小笨蛋"/><br /><sub><b>小笨蛋</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=a-little-fool" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=a-little-fool" title="Documentation">📖</a> <a href="#blog-a-little-fool" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aa-little-fool" title="Bug reports">🐛</a> <a href="#design-a-little-fool" title="Design">🎨</a></td>
|
||||
</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>
|
||||
@@ -377,7 +383,7 @@ Thanks these wonderful people, welcome to join us:
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1036664317"><img src="https://avatars.githubusercontent.com/u/7696697?v=4?s=100" width="100px;" alt="moghn"/><br /><sub><b>moghn</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1036664317" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaoguolong"><img src="https://avatars.githubusercontent.com/u/33684988?v=4?s=100" width="100px;" alt="xiaoguolong"/><br /><sub><b>xiaoguolong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaoguolong" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Clownsw"><img src="https://avatars.githubusercontent.com/u/28394742?v=4?s=100" width="100px;" alt="Smliexx"/><br /><sub><b>Smliexx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Clownsw" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AClownsw" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Calvin979"><img src="https://avatars.githubusercontent.com/u/131688897?v=4?s=100" width="100px;" alt="Calvin"/><br /><sub><b>Calvin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Code">💻</a> <a href="#design-Calvin979" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACalvin979" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Calvin979"><img src="https://avatars.githubusercontent.com/u/131688897?v=4?s=100" width="100px;" alt="Naruse"/><br /><sub><b>Naruse</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Code">💻</a> <a href="#design-Calvin979" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACalvin979" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bbelide2"><img src="https://avatars.githubusercontent.com/u/26840796?v=4?s=100" width="100px;" alt="Bala Sukesh"/><br /><sub><b>Bala Sukesh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bbelide2" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jinyaoMa"><img src="https://avatars.githubusercontent.com/u/25066570?v=4?s=100" width="100px;" alt="Jinyao Ma"/><br /><sub><b>Jinyao Ma</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jinyaoMa" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://linuxsuren.github.io/open-source-best-practice/"><img src="https://avatars.githubusercontent.com/u/1450685?v=4?s=100" width="100px;" alt="Rick"/><br /><sub><b>Rick</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Tests">⚠️</a></td>
|
||||
@@ -521,33 +527,6 @@ Thanks these wonderful people, welcome to join us:
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaomizhou2"><img src="https://avatars.githubusercontent.com/u/47807926?v=4?s=100" width="100px;" alt="zhangyaxi"/><br /><sub><b>zhangyaxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RainBondsongyg"><img src="https://avatars.githubusercontent.com/u/94501396?v=4?s=100" width="100px;" alt="songyg"/><br /><sub><b>songyg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=RainBondsongyg" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lx1229"><img src="https://avatars.githubusercontent.com/u/44620005?v=4?s=100" width="100px;" alt="Liuxin"/><br /><sub><b>Liuxin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lx1229" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yy549159265"><img src="https://avatars.githubusercontent.com/u/40821310?v=4?s=100" width="100px;" alt="yy549159265"/><br /><sub><b>yy549159265</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Tests">⚠️</a> <a href="#design-yy549159265" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cto-huhang"><img src="https://avatars.githubusercontent.com/u/53338629?v=4?s=100" width="100px;" alt="cto-huhang"/><br /><sub><b>cto-huhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cto-huhang" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Saramanda9988"><img src="https://avatars.githubusercontent.com/u/176664901?v=4?s=100" width="100px;" alt="LunaRain_079"/><br /><sub><b>LunaRain_079</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/delei"><img src="https://avatars.githubusercontent.com/u/17263766?v=4?s=100" width="100px;" alt="DeleiGuo"/><br /><sub><b>DeleiGuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Adelei" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chingjustwe"><img src="https://avatars.githubusercontent.com/u/13643747?v=4?s=100" width="100px;" alt="Rocky, Chi"/><br /><sub><b>Rocky, Chi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=chingjustwe" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rowankid"><img src="https://avatars.githubusercontent.com/u/18652781?v=4?s=100" width="100px;" alt="Wenqi Luo"/><br /><sub><b>Wenqi Luo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3Arowankid" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tuzuy"><img src="https://avatars.githubusercontent.com/u/95274591?v=4?s=100" width="100px;" alt="tuzuy"/><br /><sub><b>tuzuy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tuzuy" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/carlpinto25"><img src="https://avatars.githubusercontent.com/u/117299909?v=4?s=100" width="100px;" alt="carl pinto"/><br /><sub><b>carl pinto</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=carlpinto25" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://cxhello.top/"><img src="https://avatars.githubusercontent.com/u/49056040?v=4?s=100" width="100px;" alt="cxhello"/><br /><sub><b>cxhello</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cxhello" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jl15988"><img src="https://avatars.githubusercontent.com/u/70638770?v=4?s=100" width="100px;" alt="会功夫的李白"/><br /><sub><b>会功夫的李白</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jl15988" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://blog.aytop.cloud/"><img src="https://avatars.githubusercontent.com/u/37127008?v=4?s=100" width="100px;" alt="Albert.Yang"/><br /><sub><b>Albert.Yang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=AlbertYang0801" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://blog.tokenlen.top/"><img src="https://avatars.githubusercontent.com/u/150590575?v=4?s=100" width="100px;" alt="zhou yong kang"/><br /><sub><b>zhou yong kang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mengnankkkk" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/warrobe"><img src="https://avatars.githubusercontent.com/u/89446159?v=4?s=100" width="100px;" alt="warrobe"/><br /><sub><b>warrobe</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=warrobe" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Jetiaime"><img src="https://avatars.githubusercontent.com/u/93769000?v=4?s=100" width="100px;" alt="TeAmo"/><br /><sub><b>TeAmo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Jetiaime" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pentium100"><img src="https://avatars.githubusercontent.com/u/27917?v=4?s=100" width="100px;" alt="pentium100"/><br /><sub><b>pentium100</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pentium100" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dedyks"><img src="https://avatars.githubusercontent.com/u/23741665?v=4?s=100" width="100px;" alt="Dedy Kurniawan Santoso"/><br /><sub><b>Dedy Kurniawan Santoso</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dedyks" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KOYR"><img src="https://avatars.githubusercontent.com/u/53216619?v=4?s=100" width="100px;" alt="KOYR"/><br /><sub><b>KOYR</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=KOYR" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Lathika226"><img src="https://avatars.githubusercontent.com/u/178710568?v=4?s=100" width="100px;" alt="LathikaBaddam"/><br /><sub><b>LathikaBaddam</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Lathika226" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://shadwal.space/"><img src="https://avatars.githubusercontent.com/u/119167601?v=4?s=100" width="100px;" alt="Sahil Shadwal"/><br /><sub><b>Sahil Shadwal</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Sahil-Shadwal" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BhanuNidumolu"><img src="https://avatars.githubusercontent.com/u/180380413?v=4?s=100" width="100px;" alt="N.Bhanu Prasad"/><br /><sub><b>N.Bhanu Prasad</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=BhanuNidumolu" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://prakashh-portfolio.vercel.app/"><img src="https://avatars.githubusercontent.com/u/183058331?v=4?s=100" width="100px;" alt="Prakash Kumar"/><br /><sub><b>Prakash Kumar</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Prakash1185" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -569,8 +548,12 @@ Thanks these wonderful people, welcome to join us:
|
||||
|
||||
微信公众号 : 搜索 ID `usthecom`.
|
||||
|
||||
[QQ交流群](https://qm.qq.com/q/xxqecSC2cw) : 群号 `1035688434`
|
||||
|
||||
[Github Discussion](https://github.com/apache/hertzbeat/discussions)
|
||||
|
||||
[Reddit Community](https://www.reddit.com/r/hertzbeat/)
|
||||
|
||||
[Follow Us Twitter](https://x.com/hertzbeat1024)
|
||||
|
||||
[Subscribe YouTube](https://www.youtube.com/channel/UCri75zfWX0GHqJFPENEbLow)
|
||||
|
||||
+53
-69
@@ -7,9 +7,12 @@
|
||||
<p align="center">
|
||||
<b>Readme</b>:
|
||||
<a href="README.md">English</a> | <a href="README_CN.md">中文</a> | <b><a href="README_JP.md">日本語</a></b>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
> オープンソースのリアルタイム監視システム、エージェントレス、クラスタ対応、Prometheus互換、カスタムおよびステータスページ付き。
|
||||
|
||||
[](https://discord.gg/Fb6M73htGr)
|
||||
[](https://www.reddit.com/r/hertzbeat/)
|
||||
[](https://x.com/hertzbeat1024)
|
||||
[](https://www.bestpractices.dev/projects/8139)
|
||||
[](https://app.codecov.io/gh/apache/hertzbeat)
|
||||
@@ -19,38 +22,44 @@
|
||||
[](https://gitpod.io/#https://github.com/apache/hertzbeat)
|
||||
|
||||
|
||||
**公式サイト: [hertzbeat.apache.org](https://hertzbeat.apache.org)**
|
||||
**公式サイト: [hertzbeat.apache.org](https://hertzbeat.apache.org)**
|
||||
**メール:** メーリングリストに登録するために、<a href="mailto:dev-subscribe@hertzbeat.apache.org">dev-subscribe@hertzbeat.apache.org</a>にメールを送ってください。
|
||||
|
||||
|
||||
## 🎡 <font color="green">紹介</font>
|
||||
|
||||
[Apache HertzBeat™](https://github.com/apache/hertzbeat) は AI 駆動の次世代オープンソースリアルタイム観測システムです。メトリクスとログの統一収集、アラートの一元配信、インテリジェント管理分析。エージェント不要、高性能クラスタ、強力なカスタム監視とステータスページ構築機能を提供します。
|
||||
[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`の強力なカスタマイズ、多種類のサポート、高性能および容易な拡張性は、ユーザーが迅速に独自の監視システムを構築するのを支援することを目的としています。
|
||||
|
||||
----
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
----
|
||||
|
||||
## 🥐 モジュール
|
||||
## 🥐 モジュール
|
||||
|
||||

|
||||

|
||||
|
||||
## ⛄ 利用可能
|
||||
## ⛄ 利用可能
|
||||
|
||||
> `mysql`、`Java仮想マシン`、`k8s` などのすべての監視コレクション タイプを `YML` 監視テンプレートとして定義し、ユーザーがそれらをインポートして対応するタイプの監視をサポートできるようにしています。
|
||||
> `mysql`、`Java仮想マシン`、`k8s` などのすべての監視コレクション タイプを `YML` 監視テンプレートとして定義し、ユーザーがそれらをインポートして対応するタイプの監視をサポートできるようにしています。
|
||||
> カスタマイズされた一般的なモニターのテンプレートを提供することを歓迎します。
|
||||
|
||||
- [Website](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-website.yml), [Port Telnet](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-port.yml),
|
||||
@@ -90,9 +99,9 @@
|
||||
[HuaweiSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-huawei_switch.yml), [TpLinkSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-tplink_switch.yml),
|
||||
[H3cSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-h3c_switch.yml)
|
||||
- その他、カスタマイズされたモニターテンプレート。
|
||||
- 通知利用可能 `Discord` `Slack` `Telegram` `Email` `DingTalk` `WeChat` `FeiShu` `Webhook` `SMS` `ServerChan`。
|
||||
- 通知利用可能 `Discord` `Slack` `Telegram` `Email` `Dingtalk` `WeChat` `FeiShu` `Webhook` `SMS` `ServerChan`。
|
||||
|
||||
## 🐕 クイックスタート
|
||||
## 🐕 クイックスタート
|
||||
|
||||
- HertzBeat をローカルに展開する場合は、以下のデプロイメントドキュメントを参照してください。
|
||||
|
||||
@@ -101,13 +110,13 @@
|
||||
|
||||
##### 方式1:Docker
|
||||
|
||||
1. `docker` で以下の指令を実行します:
|
||||
1. `docker` で以下の指令を実行します:
|
||||
|
||||
```shell
|
||||
docker run -d -p 1157:1157 -p 1158:1158 --name hertzbeat apache/hertzbeat
|
||||
```
|
||||
|
||||
2. スタート:`http://localhost:1157`にアクセスします。デフォルトのアカウントとパスワード:`admin/hertzbeat`。
|
||||
2. スタート:`http://localhost:4200`にアクセスします。デフォルトのアカウントとパスワード:`admin/hertzbeat`。
|
||||
|
||||
3. コレクタークラスタのデプロイメント(オプション)
|
||||
|
||||
@@ -121,17 +130,17 @@
|
||||
- `-e MANAGER_PORT=1158` :メインhertzbeatサーバポート。
|
||||
|
||||
|
||||
詳細ステップ [Dockerで HertzBeat をインストール](https://hertzbeat.apache.org/docs/start/docker-deploy)
|
||||
詳細ステップ [通过 Docker 方式安装 HertzBeat](https://hertzbeat.apache.org/docs/start/docker-deploy)
|
||||
|
||||
##### 方式2:インストールパッケージ
|
||||
|
||||
1. リリースパッケージ `apache-hertzbeat-xx-bin.tar.gz` をダウンロードします [Download](https://hertzbeat.apache.org/docs/download)
|
||||
2. HertzBeat の設定ファイル `hertzbeat/config/application.yml` を編集します(任意)
|
||||
3. コマンド `$ ./bin/startup.sh` または `bin/startup.bat` を実行します
|
||||
4. ブラウザで `http://localhost:1157` にアクセスします。デフォルトのアカウントとパスワードは `admin/hertzbeat` です
|
||||
1. リリースパッケージをダウンロード `hertzbeat-xx.tar.gz` [Download](https://hertzbeat.apache.org/docs/download)
|
||||
2. HertzBeatのymlファイルを設定 `hertzbeat/config/application.yml` (オプション)
|
||||
3. コマンド`$ ./bin/startup.sh`または`bin/startup.bat`を実行
|
||||
4. スタート:`http://localhost:4200`にアクセスします。デフォルトのアカウントとパスワード:`admin/hertzbeat`
|
||||
5. コレクタークラスタのデプロイメント(オプション)
|
||||
- 別ホストにコレクターのインストールパッケージ `apache-hertzbeat-collector-xx-bin.tar.gz`(JVM コレクター)または対象プラットフォーム向けの Native コレクターパッケージ(例: `apache-hertzbeat-collector-native-xx-linux-amd64-bin.tar.gz`、`apache-hertzbeat-collector-native-xx-windows-amd64-bin.zip`)をダウンロードします [Download](https://hertzbeat.apache.org/docs/download)
|
||||
- コレクターの設定ファイル `hertzbeat-collector/config/application.yml` を編集します
|
||||
- コレクターパッケージを別のホストにダウンロード `hertzbeat-collector-xx.tar.gz` [Download](https://hertzbeat.apache.org/docs/download)
|
||||
- コレクターのymlファイルを設定 `hertzbeat-collector/config/application.yml`
|
||||
```yaml
|
||||
collector:
|
||||
dispatch:
|
||||
@@ -147,27 +156,25 @@
|
||||
- `mode: ${MODE:public}`:実行モード(パブリッククラスタまたはプライベートクラウドエッジ)。
|
||||
- `manager-host: ${MANAGER_HOST:127.0.0.1}`:メインhertzbeatサーバーのIP。
|
||||
- `manager-port: ${MANAGER_PORT:1158}`:メインhertzbeatサーバポート。
|
||||
- `ext-lib` に JDBC ドライバーを置かない場合、MySQL、MariaDB、OceanBase は組み込みのクエリエンジンを使って Native コレクターパッケージでも監視できます。TiDB も SQL クエリのメトリクスセットについては同じルールです。
|
||||
- `ext-lib` に `mysql-connector-j` を置いた場合は、再起動後に組み込みサーバーコレクターまたは JVM コレクターが MySQL、MariaDB、OceanBase で自動的に JDBC を優先します。TiDB も SQL クエリのメトリクスセットについては同じルールで、HTTP メトリクスは影響を受けません。Oracle と DB2 は引き続き外部 JDBC ドライバーに依存するため、JVM コレクターパッケージを使用してください。
|
||||
- JVM コレクターのインストールパッケージは `$ ./bin/startup.sh` または `bin/startup.bat`、Linux/macOS の Native コレクターパッケージは `$ ./bin/startup.sh`、Windows の Native コレクターパッケージは `bin\\startup.bat` で起動します。
|
||||
- メインの HertzBeat サービス `http://localhost:1157` にアクセスすると、登録された新しいコレクターを確認できます。
|
||||
- コマンド`$ ./bin/startup.sh`または`bin/startup.bat`を実行。
|
||||
- `http://localhost:1157`にアクセスし、登録された新しいコレクターを見ることがでます。
|
||||
|
||||
詳細ステップ [インストールパッケージで HertzBeat をインストール](https://hertzbeat.apache.org/docs/start/package-deploy)
|
||||
詳細ステップ [通过安装包安装HertzBeat](https://hertzbeat.apache.org/docs/start/package-deploy)
|
||||
|
||||
##### 方式3:ローカルの実行
|
||||
|
||||
1. ローカルの実行には、バックエンドのプロジェクト`hertzbeat-startup`とフロントエンドのプロジェクト`web-app`を起動する必要があります。
|
||||
2. バックエンド:`maven3+`、`Java25`、`lombok` の環境が必要です。`YML` 設定を修正し、Java 仮想マシンパラメータに `--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED` を追加して `hertzbeat-startup` を起動します。
|
||||
3. フロントエンド:`nodejs` と `pnpm` の環境が必要です。ローカルのバックエンドが立ち上がったら、`web-app` ディレクトリで `pnpm install` を実行し、続けて `pnpm start` を実行します。
|
||||
1. ローカルの実行には、バックエンドのプロジェクト`manager`とフロントエンドのプロジェクト`web-app`を起動する必要があります。
|
||||
2. バックエンド:`maven3+`、`java17`と`lombok`の環境は必要です。`YML` 設定を修正し、Java仮想マシンパラメータに`--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED` を追加し、`manager` を起動します。
|
||||
3. フロントエンド:`nodejs npm angular-cli`の環境は必要です。ローカルのバックエンドが立ち上がったら、`web-app` ディレクトリで `ng serve --open` というコマンドを実行します。
|
||||
4. スタート:`http://localhost:4200`にアクセスします。デフォルトのアカウントとパスワード:`admin/hertzbeat`。
|
||||
|
||||
詳細ステップ [貢献ガイド](CONTRIBUTING.md)
|
||||
|
||||
##### 方式4:Docker-Compose
|
||||
|
||||
[Docker-Compose 部署脚本](script/docker-compose)でpostgresql/mysqlデータベース、victoria-metrics、iotdb、またはtdengine時系列データベースとHertzBeat一括デプロイ。
|
||||
[Docker-Compose 部署脚本](script/docker-compose)でpostgresql/mysqlデータベース、victoria-metrics、iotdb、またはtdengine時系列データベースとHertzbeat一括デプロイ。
|
||||
|
||||
詳細ステップ [Docker-Compose で HertzBeat をインストール](script/docker-compose/README.md)
|
||||
詳細ステップ [通过 Docker-Compose 安装 HertzBeat](script/docker-compose/README.md)
|
||||
|
||||
##### 方式5:Kubernetes Helm Charts
|
||||
|
||||
@@ -179,8 +186,8 @@ Helm ChartでHertzBeatクラスタコンポーネントをKubernetesクラスタ
|
||||
|
||||
## ✨ Contributors
|
||||
|
||||
Thanks these wonderful people, welcome to join us:
|
||||
[貢献ガイド](CONTRIBUTING.md)
|
||||
Thanks these wonderful people, welcome to join us:
|
||||
[貢献ガイド](CONTRIBUTING.md)
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
<!-- prettier-ignore-start -->
|
||||
@@ -356,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/a-little-fool"><img src="https://avatars.githubusercontent.com/u/105542329?v=4?s=100" width="100px;" alt="小笨蛋"/><br /><sub><b>小笨蛋</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=a-little-fool" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=a-little-fool" title="Documentation">📖</a> <a href="#blog-a-little-fool" title="Blogposts">📝</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Aa-little-fool" title="Bug reports">🐛</a> <a href="#design-a-little-fool" title="Design">🎨</a></td>
|
||||
</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>
|
||||
@@ -380,7 +387,7 @@ Thanks these wonderful people, welcome to join us:
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1036664317"><img src="https://avatars.githubusercontent.com/u/7696697?v=4?s=100" width="100px;" alt="moghn"/><br /><sub><b>moghn</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=1036664317" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaoguolong"><img src="https://avatars.githubusercontent.com/u/33684988?v=4?s=100" width="100px;" alt="xiaoguolong"/><br /><sub><b>xiaoguolong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaoguolong" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Clownsw"><img src="https://avatars.githubusercontent.com/u/28394742?v=4?s=100" width="100px;" alt="Smliexx"/><br /><sub><b>Smliexx</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Clownsw" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3AClownsw" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Calvin979"><img src="https://avatars.githubusercontent.com/u/131688897?v=4?s=100" width="100px;" alt="Calvin"/><br /><sub><b>Calvin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Code">💻</a> <a href="#design-Calvin979" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACalvin979" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Calvin979"><img src="https://avatars.githubusercontent.com/u/131688897?v=4?s=100" width="100px;" alt="Naruse"/><br /><sub><b>Naruse</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Code">💻</a> <a href="#design-Calvin979" title="Design">🎨</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3ACalvin979" title="Bug reports">🐛</a> <a href="https://github.com/apache/hertzbeat/commits?author=Calvin979" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bbelide2"><img src="https://avatars.githubusercontent.com/u/26840796?v=4?s=100" width="100px;" alt="Bala Sukesh"/><br /><sub><b>Bala Sukesh</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bbelide2" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jinyaoMa"><img src="https://avatars.githubusercontent.com/u/25066570?v=4?s=100" width="100px;" alt="Jinyao Ma"/><br /><sub><b>Jinyao Ma</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jinyaoMa" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://linuxsuren.github.io/open-source-best-practice/"><img src="https://avatars.githubusercontent.com/u/1450685?v=4?s=100" width="100px;" alt="Rick"/><br /><sub><b>Rick</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=LinuxSuRen" title="Tests">⚠️</a></td>
|
||||
@@ -524,33 +531,6 @@ Thanks these wonderful people, welcome to join us:
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaomizhou2"><img src="https://avatars.githubusercontent.com/u/47807926?v=4?s=100" width="100px;" alt="zhangyaxi"/><br /><sub><b>zhangyaxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RainBondsongyg"><img src="https://avatars.githubusercontent.com/u/94501396?v=4?s=100" width="100px;" alt="songyg"/><br /><sub><b>songyg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=RainBondsongyg" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lx1229"><img src="https://avatars.githubusercontent.com/u/44620005?v=4?s=100" width="100px;" alt="Liuxin"/><br /><sub><b>Liuxin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lx1229" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yy549159265"><img src="https://avatars.githubusercontent.com/u/40821310?v=4?s=100" width="100px;" alt="yy549159265"/><br /><sub><b>yy549159265</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=yy549159265" title="Tests">⚠️</a> <a href="#design-yy549159265" title="Design">🎨</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cto-huhang"><img src="https://avatars.githubusercontent.com/u/53338629?v=4?s=100" width="100px;" alt="cto-huhang"/><br /><sub><b>cto-huhang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cto-huhang" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Saramanda9988"><img src="https://avatars.githubusercontent.com/u/176664901?v=4?s=100" width="100px;" alt="LunaRain_079"/><br /><sub><b>LunaRain_079</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/delei"><img src="https://avatars.githubusercontent.com/u/17263766?v=4?s=100" width="100px;" alt="DeleiGuo"/><br /><sub><b>DeleiGuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Adelei" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chingjustwe"><img src="https://avatars.githubusercontent.com/u/13643747?v=4?s=100" width="100px;" alt="Rocky, Chi"/><br /><sub><b>Rocky, Chi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=chingjustwe" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rowankid"><img src="https://avatars.githubusercontent.com/u/18652781?v=4?s=100" width="100px;" alt="Wenqi Luo"/><br /><sub><b>Wenqi Luo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/issues?q=author%3Arowankid" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tuzuy"><img src="https://avatars.githubusercontent.com/u/95274591?v=4?s=100" width="100px;" alt="tuzuy"/><br /><sub><b>tuzuy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=tuzuy" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/carlpinto25"><img src="https://avatars.githubusercontent.com/u/117299909?v=4?s=100" width="100px;" alt="carl pinto"/><br /><sub><b>carl pinto</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=carlpinto25" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://cxhello.top/"><img src="https://avatars.githubusercontent.com/u/49056040?v=4?s=100" width="100px;" alt="cxhello"/><br /><sub><b>cxhello</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=cxhello" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jl15988"><img src="https://avatars.githubusercontent.com/u/70638770?v=4?s=100" width="100px;" alt="会功夫的李白"/><br /><sub><b>会功夫的李白</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jl15988" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://blog.aytop.cloud/"><img src="https://avatars.githubusercontent.com/u/37127008?v=4?s=100" width="100px;" alt="Albert.Yang"/><br /><sub><b>Albert.Yang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=AlbertYang0801" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://blog.tokenlen.top/"><img src="https://avatars.githubusercontent.com/u/150590575?v=4?s=100" width="100px;" alt="zhou yong kang"/><br /><sub><b>zhou yong kang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=mengnankkkk" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/warrobe"><img src="https://avatars.githubusercontent.com/u/89446159?v=4?s=100" width="100px;" alt="warrobe"/><br /><sub><b>warrobe</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=warrobe" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Jetiaime"><img src="https://avatars.githubusercontent.com/u/93769000?v=4?s=100" width="100px;" alt="TeAmo"/><br /><sub><b>TeAmo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Jetiaime" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pentium100"><img src="https://avatars.githubusercontent.com/u/27917?v=4?s=100" width="100px;" alt="pentium100"/><br /><sub><b>pentium100</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pentium100" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dedyks"><img src="https://avatars.githubusercontent.com/u/23741665?v=4?s=100" width="100px;" alt="Dedy Kurniawan Santoso"/><br /><sub><b>Dedy Kurniawan Santoso</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=dedyks" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KOYR"><img src="https://avatars.githubusercontent.com/u/53216619?v=4?s=100" width="100px;" alt="KOYR"/><br /><sub><b>KOYR</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=KOYR" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Lathika226"><img src="https://avatars.githubusercontent.com/u/178710568?v=4?s=100" width="100px;" alt="LathikaBaddam"/><br /><sub><b>LathikaBaddam</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Lathika226" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://shadwal.space/"><img src="https://avatars.githubusercontent.com/u/119167601?v=4?s=100" width="100px;" alt="Sahil Shadwal"/><br /><sub><b>Sahil Shadwal</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Sahil-Shadwal" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BhanuNidumolu"><img src="https://avatars.githubusercontent.com/u/180380413?v=4?s=100" width="100px;" alt="N.Bhanu Prasad"/><br /><sub><b>N.Bhanu Prasad</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=BhanuNidumolu" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://prakashh-portfolio.vercel.app/"><img src="https://avatars.githubusercontent.com/u/183058331?v=4?s=100" width="100px;" alt="Prakash Kumar"/><br /><sub><b>Prakash Kumar</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Prakash1185" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -558,7 +538,7 @@ Thanks these wonderful people, welcome to join us:
|
||||
<!-- markdownlint-restore -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
## 💬 コミュニティ交流
|
||||
|
||||
@@ -572,8 +552,12 @@ WeChatグループ : `ahertzbeat` を検索.
|
||||
|
||||
WeChat公式アカウント : `usthecom`を検索.
|
||||
|
||||
[QQグループ](https://qm.qq.com/q/xxqecSC2cw) : グループ番号 `1035688434`
|
||||
|
||||
[Github Discussion](https://github.com/apache/hertzbeat/discussions)
|
||||
|
||||
[Reddit Community](https://www.reddit.com/r/hertzbeat/)
|
||||
|
||||
[Follow Us Twitter](https://x.com/hertzbeat1024)
|
||||
|
||||
[Subscribe YouTube](https://www.youtube.com/channel/UCri75zfWX0GHqJFPENEbLow)
|
||||
|
||||
+7
-28
@@ -1,35 +1,14 @@
|
||||
Please add the corresponding e2e (aka end-to-end) test cases if you add or update APIs.
|
||||
|
||||
## How to work
|
||||
* Start and watch the [Docker Compose](https://docs.docker.com/compose/) environment
|
||||
* The Compose file is `e2e/docker-compose.yml`
|
||||
* The `testing` container uses [api-testing](https://github.com/LinuxSuRen/api-testing)
|
||||
* The main test definition file is `e2e/testsuite.yaml`
|
||||
* Before starting the suite locally, build the `apache/hertzbeat:test` image from the repository root:
|
||||
|
||||
```bash
|
||||
mvn clean -B package -Prelease -Dmaven.test.skip=true --file pom.xml
|
||||
docker build -t apache/hertzbeat:test -f script/docker/server/Dockerfile ./dist
|
||||
```
|
||||
|
||||
* Run the E2E tests via [api-testing](https://github.com/LinuxSuRen/api-testing)
|
||||
* The test cases run from top to bottom
|
||||
* You can add the necessary assertions there
|
||||
* Test data files are under `e2e/data/`
|
||||
* The test report is generated at `e2e/report/report.md`
|
||||
* HertzBeat logs are written to `e2e/logs/`
|
||||
* Start and watch the [docker-compose](https://docs.docker.com/compose/) via [the script](script/start.sh)
|
||||
* Run the e2e testing via [api-testing](https://github.com/LinuxSuRen/api-testing)
|
||||
* It will run the test cases from top to bottom
|
||||
* You can add the necessary asserts to it
|
||||
|
||||
## Run locally
|
||||
Please follow these steps if you want to run the E2E tests locally.
|
||||
Please follow these steps if you want to run the e2e testing locally.
|
||||
|
||||
> Please make sure that Docker Compose v2 is installed
|
||||
> Please make sure you have installed docker-compose v2
|
||||
|
||||
* Build the local `apache/hertzbeat:test` image from the repository root
|
||||
* Change the directory to `e2e`, then run:
|
||||
|
||||
```bash
|
||||
docker compose up --exit-code-from testing --remove-orphans
|
||||
```
|
||||
|
||||
* If your environment uses the standalone Compose binary, use `docker-compose` instead of `docker compose`
|
||||
* After the run, check `e2e/report/report.md` and `e2e/logs/` for the test result and runtime logs
|
||||
* Change the directory to `e2e`, then execute `./script/start.sh`
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"intervals": 60,
|
||||
"tags": [],
|
||||
"app": "ftp",
|
||||
"instance": "127.0.0.1",
|
||||
"host": "127.0.0.1",
|
||||
"name": "{{.param.monitorFTP}}"
|
||||
},
|
||||
"params": [
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"intervals": 60,
|
||||
"tags": [],
|
||||
"app": "api",
|
||||
"instance": "127.0.0.1",
|
||||
"host": "127.0.0.1",
|
||||
"name": "{{.param.monitorHTTP}}"
|
||||
},
|
||||
"params": [
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"intervals": 60,
|
||||
"tags": [],
|
||||
"app": "ping",
|
||||
"instance": "127.0.0.1",
|
||||
"host": "127.0.0.1",
|
||||
"name": "{{.param.monitorPing}}"
|
||||
},
|
||||
"params": [
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"intervals": 60,
|
||||
"tags": [],
|
||||
"app": "port",
|
||||
"instance": "127.0.0.1",
|
||||
"host": "127.0.0.1",
|
||||
"name": "{{.param.monitorPort}}"
|
||||
},
|
||||
"params": [
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"intervals": 60,
|
||||
"tags": [],
|
||||
"app": "fullsite",
|
||||
"instance": "127.0.0.1",
|
||||
"host": "127.0.0.1",
|
||||
"name": "{{.param.monitorSiteMap}}"
|
||||
},
|
||||
"params": [
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"intervals": 60,
|
||||
"tags": [],
|
||||
"app": "ssl_cert",
|
||||
"instance": "127.0.0.1",
|
||||
"host": "127.0.0.1",
|
||||
"name": "{{.param.monitorSSL}}"
|
||||
},
|
||||
"params": [
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"intervals": 60,
|
||||
"tags": [],
|
||||
"app": "udp_port",
|
||||
"instance": "127.0.0.1",
|
||||
"host": "127.0.0.1",
|
||||
"name": "{{.param.monitorUDP}}"
|
||||
},
|
||||
"params": [
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"intervals": 60,
|
||||
"tags": [],
|
||||
"app": "website",
|
||||
"instance": "127.0.0.1",
|
||||
"host": "127.0.0.1",
|
||||
"name": "{{.param.monitorWebsite}}"
|
||||
},
|
||||
"params": [
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
version: '3.8'
|
||||
services:
|
||||
testing:
|
||||
image: ghcr.io/linuxsuren/api-testing:v0.0.19
|
||||
image: ghcr.io/linuxsuren/api-testing:v0.0.17
|
||||
environment:
|
||||
SERVER: http://hertzbeat:1157
|
||||
container_name: e2e-testing
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"intervals": 10,
|
||||
"tags": [],
|
||||
"app": "kubernetes",
|
||||
"instance": "172.29.0.11",
|
||||
"host": "172.29.0.11",
|
||||
"name": "Brave_Stingray_55yR"
|
||||
},
|
||||
"collector": "",
|
||||
|
||||
+1
-51
@@ -29,7 +29,6 @@ param:
|
||||
monitorUDP: "{{randAlpha 6}}"
|
||||
monitorWebsite: "{{randAlpha 6}}"
|
||||
monitorFTP: "{{randAlpha 6}}"
|
||||
apiTokenName: "e2e-token-{{randAlpha 6}}"
|
||||
labelName: "{{randAlpha 3}}"
|
||||
items:
|
||||
- name: login
|
||||
@@ -58,7 +57,7 @@ items:
|
||||
"credential": "hertzbeat"
|
||||
}
|
||||
expect:
|
||||
statusCode: 500
|
||||
statusCode: 409
|
||||
- name: monitorList
|
||||
request:
|
||||
api: /api/monitors?pageIndex=0&pageSize=8
|
||||
@@ -70,8 +69,6 @@ items:
|
||||
- name: missing-auth-header
|
||||
request:
|
||||
api: /api/monitors
|
||||
header:
|
||||
Accept: application/json
|
||||
expect:
|
||||
statusCode: 401
|
||||
- name: createSitemapMonitor
|
||||
@@ -388,53 +385,6 @@ items:
|
||||
header:
|
||||
Authorization: Bearer {{.login.data.token}}
|
||||
|
||||
## API Token Management
|
||||
- name: generateApiToken
|
||||
request:
|
||||
api: /api/account/token/generate?name={{.apiTokenName}}&expireSeconds=-1
|
||||
method: POST
|
||||
header:
|
||||
Authorization: Bearer {{.login.data.token}}
|
||||
expect:
|
||||
bodyFieldsExpect:
|
||||
code: "0"
|
||||
- name: listApiTokens
|
||||
request:
|
||||
api: /api/account/token
|
||||
header:
|
||||
Authorization: Bearer {{.login.data.token}}
|
||||
expect:
|
||||
bodyFieldsExpect:
|
||||
code: "0"
|
||||
data.0.name: "{{.apiTokenName}}"
|
||||
data.0.creator: "admin"
|
||||
- name: listApiTokensByGeneratedToken
|
||||
request:
|
||||
api: /api/account/token
|
||||
header:
|
||||
Authorization: Bearer {{.generateApiToken.data.token}}
|
||||
expect:
|
||||
bodyFieldsExpect:
|
||||
code: "0"
|
||||
data.0.name: "{{.apiTokenName}}"
|
||||
- name: useGeneratedTokenToCallApi
|
||||
request:
|
||||
api: /api/monitors?pageIndex=0&pageSize=8
|
||||
header:
|
||||
Authorization: Bearer {{.generateApiToken.data.token}}
|
||||
expect:
|
||||
bodyFieldsExpect:
|
||||
code: "0"
|
||||
- name: deleteApiToken
|
||||
request:
|
||||
api: /api/account/token/{{(index .listApiTokens.data 0).id | int64}}
|
||||
method: DELETE
|
||||
header:
|
||||
Authorization: Bearer {{.login.data.token}}
|
||||
expect:
|
||||
bodyFieldsExpect:
|
||||
code: "0"
|
||||
|
||||
## Config
|
||||
- name: getConfigEmail
|
||||
request:
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
~ contributor license agreements. See the NOTICE file distributed with
|
||||
~ this work for additional information regarding copyright ownership.
|
||||
~ The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
~ (the "License"); you may not use this file except in compliance with
|
||||
~ the License. You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.apache.hertzbeat</groupId>
|
||||
<artifactId>hertzbeat</artifactId>
|
||||
<version>2.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>hertzbeat-ai-agent</artifactId>
|
||||
<version>${hertzbeat.version}</version>
|
||||
<properties>
|
||||
<spring-ai.version>1.0.1</spring-ai.version>
|
||||
<java.version>17</java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.platform</groupId>
|
||||
<artifactId>junit-platform-launcher</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-model-openai</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.hertzbeat</groupId>
|
||||
<artifactId>hertzbeat-common</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.hertzbeat</groupId>
|
||||
<artifactId>hertzbeat-alerter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.usthe.sureness</groupId>
|
||||
<artifactId>spring-boot3-starter-sureness</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-bom</artifactId>
|
||||
<version>${spring-ai.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>true</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
+81
@@ -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);
|
||||
}
|
||||
+60
@@ -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();
|
||||
}
|
||||
+55
@@ -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);
|
||||
}
|
||||
+71
@@ -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);
|
||||
}
|
||||
|
||||
+257
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+141
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+136
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+214
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+246
@@ -0,0 +1,246 @@
|
||||
/*
|
||||
* 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 com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.usthe.sureness.mgt.SurenessSecurityManager;
|
||||
import com.usthe.sureness.subject.SubjectSum;
|
||||
import io.modelcontextprotocol.spec.McpError;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import io.modelcontextprotocol.spec.McpServerSession;
|
||||
import io.modelcontextprotocol.spec.McpServerTransport;
|
||||
import io.modelcontextprotocol.spec.McpServerTransportProvider;
|
||||
import io.modelcontextprotocol.util.Assert;
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.servlet.function.RouterFunction;
|
||||
import org.springframework.web.servlet.function.RouterFunctions;
|
||||
import org.springframework.web.servlet.function.ServerRequest;
|
||||
import org.springframework.web.servlet.function.ServerResponse;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Custom Server-Sent Events transport provider for Model Context Protocol.
|
||||
*/
|
||||
@Slf4j
|
||||
public class CustomSseServerTransport implements McpServerTransportProvider {
|
||||
private final ObjectMapper objectMapper;
|
||||
private final String messageEndpoint;
|
||||
private final String sseEndpoint;
|
||||
private final String baseUrl;
|
||||
@Getter
|
||||
private final RouterFunction<ServerResponse> routerFunction;
|
||||
@Setter
|
||||
private McpServerSession.Factory sessionFactory;
|
||||
private final Map<String, Object> sessionRequest = new HashMap<>();
|
||||
private final ConcurrentHashMap<String, McpServerSession> sessions;
|
||||
private volatile boolean isClosing;
|
||||
|
||||
public CustomSseServerTransport(ObjectMapper objectMapper, String messageEndpoint) {
|
||||
this(objectMapper, messageEndpoint, "/sse");
|
||||
}
|
||||
|
||||
|
||||
public CustomSseServerTransport(ObjectMapper objectMapper, String messageEndpoint, String sseEndpoint) {
|
||||
this(objectMapper, "", messageEndpoint, sseEndpoint);
|
||||
}
|
||||
|
||||
public CustomSseServerTransport(ObjectMapper objectMapper, String baseUrl, String messageEndpoint, String sseEndpoint) {
|
||||
this.sessions = new ConcurrentHashMap();
|
||||
this.isClosing = false;
|
||||
Assert.notNull(objectMapper, "ObjectMapper must not be null");
|
||||
Assert.notNull(baseUrl, "Message base URL must not be null");
|
||||
Assert.notNull(messageEndpoint, "Message endpoint must not be null");
|
||||
Assert.notNull(sseEndpoint, "SSE endpoint must not be null");
|
||||
this.objectMapper = objectMapper;
|
||||
this.baseUrl = baseUrl;
|
||||
this.messageEndpoint = messageEndpoint;
|
||||
this.sseEndpoint = sseEndpoint;
|
||||
this.routerFunction = RouterFunctions.route().GET(this.sseEndpoint, this::handleSseConnection).POST(this.messageEndpoint, this::handleMessage).build();
|
||||
}
|
||||
|
||||
public Mono<Void> notifyClients(String method, Object params) {
|
||||
if (this.sessions.isEmpty()) {
|
||||
log.debug("No active sessions to broadcast message to");
|
||||
return Mono.empty();
|
||||
} else {
|
||||
log.debug("Attempting to broadcast message to {} active sessions", this.sessions.size());
|
||||
return Flux.fromIterable(this.sessions.values())
|
||||
.flatMap((session) -> session.sendNotification(method, params)
|
||||
.doOnError((e) -> log.error("Failed to send message to session {}: {}", session.getId(), e.getMessage()))
|
||||
.onErrorComplete())
|
||||
.then();
|
||||
}
|
||||
}
|
||||
|
||||
public Mono<Void> closeGracefully() {
|
||||
return Flux.fromIterable(this.sessions.values()).doFirst(() -> {
|
||||
this.isClosing = true;
|
||||
log.debug("Initiating graceful shutdown with {} active sessions", this.sessions.size());
|
||||
}).flatMap(McpServerSession::closeGracefully).then().doOnSuccess((v) -> log.debug("Graceful shutdown completed"));
|
||||
}
|
||||
|
||||
private ServerResponse handleSseConnection(ServerRequest request) {
|
||||
log.debug("Handling SSE connection for request: {}", request);
|
||||
HttpServletRequest servletRequest = request.servletRequest();
|
||||
|
||||
try {
|
||||
|
||||
log.debug("Processing SSE connection for servlet request: {}", servletRequest);
|
||||
log.debug("Authorization header: {}", servletRequest.getHeader("Authorization"));
|
||||
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Authentication failed for SSE connection: {}", e.getMessage());
|
||||
return ServerResponse.status(HttpStatus.UNAUTHORIZED).body("Unauthorized: " + e.getMessage());
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (this.isClosing) {
|
||||
return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).body("Server is shutting down");
|
||||
} else {
|
||||
String sessionId = UUID.randomUUID().toString();
|
||||
log.debug("Generated session ID for SSE connection: {}", sessionId);
|
||||
log.debug("Creating new SSE connection for session: {}", sessionId);
|
||||
|
||||
|
||||
return ServerResponse.sse((sseBuilder) -> {
|
||||
sseBuilder.onComplete(() -> {
|
||||
log.debug("SSE connection completed for session: {}", sessionId);
|
||||
this.sessions.remove(sessionId);
|
||||
});
|
||||
sseBuilder.onTimeout(() -> {
|
||||
log.debug("SSE connection timed out for session: {}", sessionId);
|
||||
this.sessions.remove(sessionId);
|
||||
});
|
||||
CustomSseServerTransport.WebMvcMcpSessionTransport sessionTransport = new CustomSseServerTransport.WebMvcMcpSessionTransport(sessionId, sseBuilder);
|
||||
McpServerSession session = this.sessionFactory.create(sessionTransport);
|
||||
this.sessionRequest.put(sessionId, request.servletRequest());
|
||||
this.sessions.put(sessionId, session);
|
||||
|
||||
try {
|
||||
sseBuilder.id(sessionId).event("endpoint").data(this.baseUrl + this.messageEndpoint + "?sessionId=" + sessionId);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to send initial endpoint event: {}", e.getMessage());
|
||||
sseBuilder.error(e);
|
||||
}
|
||||
|
||||
}, Duration.ZERO);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private ServerResponse handleMessage(ServerRequest request) {
|
||||
if (this.isClosing) {
|
||||
return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).body("Server is shutting down");
|
||||
} else if (request.param("sessionId").isEmpty()) {
|
||||
return ServerResponse.badRequest().body(new McpError("Session ID missing in message endpoint"));
|
||||
} else {
|
||||
String sessionId = (String) request.param("sessionId").get();
|
||||
McpServerSession session = (McpServerSession) this.sessions.get(sessionId);
|
||||
log.debug("Authorization header for message request: {}", request.servletRequest().getHeader("Authorization"));
|
||||
SubjectSum subject = SurenessSecurityManager.getInstance().checkIn(sessionRequest.get(sessionId));
|
||||
McpContextHolder.setSubject(subject);
|
||||
|
||||
|
||||
if (session == null) {
|
||||
return ServerResponse.status(HttpStatus.NOT_FOUND).body(new McpError("Session not found: " + sessionId));
|
||||
} else {
|
||||
try {
|
||||
String body = request.body(String.class);
|
||||
McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(this.objectMapper, body);
|
||||
session.handle(message).block();
|
||||
return ServerResponse.ok().build();
|
||||
} catch (IOException | IllegalArgumentException e) {
|
||||
log.error("Failed to deserialize message: {}", ((Exception) e).getMessage());
|
||||
return ServerResponse.badRequest().body(new McpError("Invalid message format"));
|
||||
} catch (Exception e) {
|
||||
log.error("Error handling message: {}", e.getMessage());
|
||||
return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new McpError(e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class WebMvcMcpSessionTransport implements McpServerTransport {
|
||||
private final String sessionId;
|
||||
private final ServerResponse.SseBuilder sseBuilder;
|
||||
|
||||
WebMvcMcpSessionTransport(String sessionId, ServerResponse.SseBuilder sseBuilder) {
|
||||
this.sessionId = sessionId;
|
||||
this.sseBuilder = sseBuilder;
|
||||
log.debug("Session transport {} initialized with SSE builder", sessionId);
|
||||
}
|
||||
|
||||
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
|
||||
return Mono.fromRunnable(() -> {
|
||||
try {
|
||||
String jsonText = CustomSseServerTransport.this.objectMapper.writeValueAsString(message);
|
||||
this.sseBuilder.id(this.sessionId).event("message").data(jsonText);
|
||||
log.debug("Message sent to session {}", this.sessionId);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to send message to session {}: {}", this.sessionId, e.getMessage());
|
||||
this.sseBuilder.error(e);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
public <T> T unmarshalFrom(Object data, TypeReference<T> typeRef) {
|
||||
return (T) CustomSseServerTransport.this.objectMapper.convertValue(data, typeRef);
|
||||
}
|
||||
|
||||
public Mono<Void> closeGracefully() {
|
||||
return Mono.fromRunnable(() -> {
|
||||
log.debug("Closing session transport: {}", this.sessionId);
|
||||
|
||||
try {
|
||||
this.sseBuilder.complete();
|
||||
log.debug("Successfully completed SSE builder for session {}", this.sessionId);
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to complete SSE builder for session {}: {}", this.sessionId, e.getMessage());
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
public void close() {
|
||||
try {
|
||||
this.sseBuilder.complete();
|
||||
log.debug("Successfully completed SSE builder for session {}", this.sessionId);
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to complete SSE builder for session {}: {}", this.sessionId, e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
+57
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+2
-2
@@ -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;
|
||||
@@ -33,8 +33,8 @@ public final class McpContextHolder {
|
||||
* Attaches the user's context to the current thread.
|
||||
*/
|
||||
public static void setSubject(SubjectSum subject) {
|
||||
|
||||
subjectHolder.set(subject);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* OpenAI YAML Configuration - reads from spring.ai.openai.api-key
|
||||
*/
|
||||
@Data
|
||||
@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();
|
||||
}
|
||||
}
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* 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.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Provider for system prompts used in the AI agent
|
||||
*/
|
||||
|
||||
@Component
|
||||
public class PromptProvider {
|
||||
/**
|
||||
* Static version of the HertzBeat monitoring prompt
|
||||
*/
|
||||
public static final String HERTZBEAT_SYSTEM_PROMPT = """
|
||||
You are an AI Assistant specialized in monitoring infrastructure and applications with HertzBeat.
|
||||
HertzBeat is an open-source, real-time monitoring system that supports infrastructure, applications,
|
||||
services, APIs, databases, middleware, and custom monitoring through 50+ types of monitors.
|
||||
Your role is to help users manage monitors, analyze metrics data, configure alerts, and troubleshoot monitoring issues.
|
||||
*******
|
||||
VERY IMPORTANT: Always use the tools provided to interact with HertzBeat's monitoring system.
|
||||
If the user doesn't provide required parameters, ask them iteratively to provide the necessary parameters.
|
||||
********
|
||||
|
||||
## Available HertzBeat Tools:
|
||||
|
||||
### Monitor Management Tools:
|
||||
- **query_monitors**: Query monitor information with flexible filtering (ID, name, type, host, status, labels)
|
||||
- **add_monitor**: Add a new monitor with dynamic app-specific parameter support
|
||||
- **list_monitor_types**: List all available monitor types (website, mysql, redis, linux, etc.)
|
||||
- **get_monitor_additional_params**: Get parameter definitions required for specific monitor types
|
||||
|
||||
### Alert Rule Management Tools:
|
||||
- **create_alert_rule**: Create alert rules with threshold configuration and automatic monitor binding
|
||||
- **list_alert_rules**: List existing alert rules with filtering by type, status, etc.
|
||||
- **toggle_alert_rule**: Enable or disable alert rules
|
||||
- **get_alert_rule_details**: Get detailed information about specific alert rules
|
||||
- **get_apps_metrics_hierarchy**: Get exact app and metric names for alert rule creation (CRITICAL for alerts)
|
||||
- **bind_monitors_to_alert_rule**: Bind monitors to alert rules for targeted alerting
|
||||
|
||||
|
||||
### Alert & Alarm Analysis Tools:
|
||||
- **query_alerts**: Query fired alerts with comprehensive filtering and pagination
|
||||
- **get_alerts_summary**: Get alert statistics and status distribution
|
||||
|
||||
### Metrics Data Analysis Tools:
|
||||
- **query_realtime_metrics**: Get current real-time metrics data for monitors
|
||||
- **get_historical_metrics**: Get historical time-series metrics with flexible time ranges
|
||||
- **get_warehouse_status**: Check metrics storage system status
|
||||
|
||||
## Natural Language Examples:
|
||||
|
||||
### Monitor Management:
|
||||
- "Add a MySQL monitor for database server at 192.168.1.10 with user admin"
|
||||
- "Monitor website https://example.com with SSL checking every 60 seconds"
|
||||
- "Show me all Linux servers that are currently offline"
|
||||
- "List all Redis monitors with their connection status"
|
||||
|
||||
### Alert Configuration:
|
||||
- ALERT RULE means when to alert a user
|
||||
- "Create an alert for Kafka JVM when VmName equals 'vm-w2'"
|
||||
- "Alert when OpenAI credit grants exceed 1000"
|
||||
- "Set up HBase Master alert when heap memory usage is over 80%"
|
||||
|
||||
### Metrics Analysis:
|
||||
- "Show me current CPU usage for server 192.168.1.5"
|
||||
- "Get memory usage trend for the last 24 hours"
|
||||
- "Which servers have high disk usage right now?"
|
||||
- "Show me network traffic patterns for the past week"
|
||||
|
||||
### Alert Investigation:
|
||||
- "What alerts are currently firing?"
|
||||
- "Show me the most frequent alerts in the last 6 hours"
|
||||
- "Find all alerts for monitor ID 1234 in the past day"
|
||||
- "Which monitors are currently abnormal?"
|
||||
|
||||
|
||||
## Workflow Guidelines:
|
||||
|
||||
1. **Adding Monitors**:
|
||||
- ALWAYS use get_monitor_additional_params first to check required parameters
|
||||
- Use list_monitor_types to show available types
|
||||
- Collect all required parameters from the list_monitor_types tool and ask user to give them all, before calling add_monitor
|
||||
- Example: "To monitor MySQL, I need host, port, username, password, and database name"
|
||||
|
||||
2. **Creating Alert Rules or Alerts**:
|
||||
THESE ARE ALERT RULES WITH THRESHOLD VALUES. USERS CAN SPECIFY THE THRESHOLD VALUES FOR EXAMPLE,
|
||||
IF THE USER SAYS "ALERT ME WHEN MY COST EXCEEDS 700, THE EXPRESSION SHOULD BE 'cost > 700' NOT 'cost < 700'.
|
||||
APPLY THE SAME LOGIC FOR LESS THAN OPERATOR.
|
||||
It is important to first understand the hierarchy of apps, metrics, and field conditions
|
||||
Each app has its own metrics and each metric has its own field conditions.
|
||||
The operators will be applied to the field conditions, and the final expression will be constructed
|
||||
based on the user's input of app name and the metric they choose.
|
||||
Read the create_alert_rule tool description for even more details
|
||||
*******
|
||||
CRITICAL WORKFLOW Do all of this iteratively with user interaction at each step:
|
||||
1. ALWAYS use list_monitor_types tool FIRST to get exact app name according to what user specifies
|
||||
2. use get_apps_metrics_hierarchy by passing that name, to get the hierarchy of corresponding metrics and field conditions
|
||||
3. Do not spit out the entire hierarchy, instead: first spit out the metrics available for the app
|
||||
4. Ask the user to choose a metric from the available metrics
|
||||
5. Based on the metric chosen, present the available field conditions
|
||||
6. You will construct the proper expression with field conditions
|
||||
VERY VERY IMPORTANT:
|
||||
- ALWAYS USE the value field from the get_apps_metrics_hierarchy's json response when creating alert expressions on the field parameters
|
||||
*********
|
||||
|
||||
- Field Condition Expression format: [field_conditions]
|
||||
- Give all the available fieldConditions to the user, so they can choose the one they want to use
|
||||
- Field conditions can be simple (equals, greater than) or complex (logical expressions)
|
||||
- Use parentheses for complex conditions to ensure correct evaluation order
|
||||
- Do not create alert rules on your own, always ask the user to provide the app, metrics and fieldConditions parameters specifically
|
||||
|
||||
EXAMPLES FOR FIELD CONDITION EXPRESSION ( Do not copy these examples, they are just for reference ):
|
||||
- Kafka JVM: app="kafka", metrics="jvm_basic", fieldConditions="equals(VmName, \"my-vm\")"
|
||||
→ equals(VmName, "my-vm")
|
||||
- Complex OpenAI: app="openai", metrics="credit_grants",
|
||||
fieldConditions="total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)"
|
||||
→ total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)
|
||||
|
||||
- Priority levels: 0=critical, 1=warning, 2=info
|
||||
|
||||
3. **Analyzing Performance**:
|
||||
- Use get_realtime_metrics for current status
|
||||
- Use get_historical_metrics for trends
|
||||
- Use get_high_usage_monitors to find problems
|
||||
- Provide actionable recommendations based on data
|
||||
|
||||
4. **Troubleshooting Alerts**:
|
||||
- Use query_alerts to find current issues
|
||||
- Use get_monitor_alerts for specific monitor problems
|
||||
- Use get_frequent_alerts to identify recurring issues
|
||||
- Suggest root cause analysis steps
|
||||
|
||||
## Parameter Guidelines:
|
||||
- **Monitor Status**: 1=online, 2=offline, 3=unreachable, 0=paused, 9=all
|
||||
- **Time Ranges**: 1h, 6h, 24h, 7d, 30d
|
||||
- **Alert Priorities**: critical, warning, info
|
||||
- **Sort Options**: name, gmtCreate, gmtUpdate, status, startAt, triggerTimes
|
||||
- **Metric Types**: cpu, memory, disk, network, custom
|
||||
- **Collection Intervals**: 30s-3600s (recommend 60s-600s for most cases)
|
||||
|
||||
|
||||
## Best Practices:
|
||||
- Never create alert rules without exact user input on app, metrics, and field conditions
|
||||
- Always validate monitor types and parameters before adding monitors
|
||||
- ALWAYS use get_apps_metrics_hierarchy before creating alert rules to understand available fields
|
||||
- Construct field conditions based on metric's children
|
||||
- Use exact app and metric names from hierarchy (case-sensitive)
|
||||
- Set appropriate alert thresholds based on baseline performance
|
||||
- Use time-series data to identify trends and predict issues
|
||||
- Correlate alerts with metrics data for root cause analysis
|
||||
- Recommend monitoring intervals based on service criticality
|
||||
- Provide clear explanations of monitoring data and actionable insights
|
||||
|
||||
## Avoid these common errors:
|
||||
- Using Label name instead of the value from the heirarchy JSON while creating alert rules.
|
||||
- Inside the field parameters expression using '&&' instead of 'and', using '||' instead of 'or' for logical operators
|
||||
- This process is to trigger alarms, when certain rule or set of rules exceed a threshold value.
|
||||
So when a user says that the threshold should be less than 1000. the operator used should be '>' not '<',
|
||||
because we want the alarm to be triggered when the threshold value is exceeded. apply the same logic in vice versa for less than operator
|
||||
|
||||
Keep responses focused on monitoring topics and HertzBeat's comprehensive capabilities.
|
||||
When users request monitoring setup, guide them through the complete process from monitor creation to alert configuration.
|
||||
""";
|
||||
}
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* 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 com.usthe.sureness.subject.SubjectSum;
|
||||
import com.usthe.sureness.util.SurenessContextHolder;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.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;
|
||||
import org.springframework.http.codec.ServerSentEvent;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.List;
|
||||
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
|
||||
import static org.springframework.http.MediaType.TEXT_EVENT_STREAM_VALUE;
|
||||
|
||||
/**
|
||||
* Controller class for handling AI chat requests and conversation management.
|
||||
*/
|
||||
@Slf4j
|
||||
@Tag(name = "AI Chat API")
|
||||
@RestController
|
||||
@RequestMapping(path = "/api/chat", produces = {APPLICATION_JSON_VALUE})
|
||||
public class ChatController {
|
||||
|
||||
private final ConversationService conversationService;
|
||||
|
||||
@Autowired
|
||||
public ChatController(ConversationService conversationService) {
|
||||
this.conversationService = conversationService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @param context The chat request context containing message and optional conversationId
|
||||
* @return Flux of ServerSentEvent for streaming response
|
||||
*/
|
||||
@PostMapping(value = "/stream", produces = TEXT_EVENT_STREAM_VALUE)
|
||||
@Operation(summary = "Send a chat message with streaming response", description = "Send a message to AI and get a streaming response with conversation tracking")
|
||||
public Flux<ServerSentEvent<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()) {
|
||||
ChatResponseDto errorResponse = ChatResponseDto.builder()
|
||||
.conversationId(context.getConversationId())
|
||||
.response("Error: Message cannot be empty")
|
||||
.build();
|
||||
return Flux.just(ServerSentEvent.builder(errorResponse)
|
||||
.event("error")
|
||||
.build());
|
||||
}
|
||||
|
||||
log.info("Received streaming chat request for conversation: {}", context.getConversationId());
|
||||
return conversationService.streamChat(context.getMessage(), context.getConversationId());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error in stream chat endpoint: ", e);
|
||||
ChatResponseDto errorResponse = ChatResponseDto.builder()
|
||||
.conversationId(context.getConversationId())
|
||||
.response("An error occurred: " + e.getMessage())
|
||||
.build();
|
||||
return Flux.just(ServerSentEvent.builder(errorResponse)
|
||||
.event("error")
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all conversations
|
||||
*
|
||||
* @return List of all conversations
|
||||
*/
|
||||
@GetMapping(path = "/conversations")
|
||||
@Operation(summary = "List all conversations", description = "Get a list of all conversations")
|
||||
public ResponseEntity<Message<List<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"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get conversation history
|
||||
*
|
||||
* @param conversationId The conversation ID
|
||||
* @return Conversation details with message history
|
||||
*/
|
||||
@GetMapping(path = "/conversations/{conversationId}")
|
||||
@Operation(summary = "Get conversation history", description = "Get detailed information and message history for a specific conversation")
|
||||
public ResponseEntity<Message<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"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a conversation
|
||||
*
|
||||
* @param conversationId The conversation ID to delete
|
||||
* @return Success or error message
|
||||
*/
|
||||
@DeleteMapping(path = "/conversations/{conversationId}")
|
||||
@Operation(summary = "Delete conversation", description = "Delete a specific conversation and all its messages")
|
||||
public ResponseEntity<Message<Void>> deleteConversation(
|
||||
@Parameter(description = "Conversation ID", example = "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"));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.OpenAiConfigDto;
|
||||
import org.apache.hertzbeat.ai.agent.service.OpenAiConfigService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
|
||||
|
||||
/**
|
||||
* OpenAI Configuration API
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = "/api/ai-agent/config", produces = {APPLICATION_JSON_VALUE})
|
||||
@Tag(name = "OpenAI Configuration API")
|
||||
@Slf4j
|
||||
public class OpenAiConfigController {
|
||||
|
||||
private final OpenAiConfigService openAiConfigService;
|
||||
|
||||
public OpenAiConfigController(OpenAiConfigService openAiConfigService) {
|
||||
this.openAiConfigService = openAiConfigService;
|
||||
}
|
||||
|
||||
@PostMapping("/openai")
|
||||
@Operation(summary = "Save OpenAI configuration", description = "Save or update OpenAI configuration")
|
||||
public ResponseEntity<Map<String, Object>> saveOpenAiConfig(@Valid @RequestBody OpenAiConfigDto config) {
|
||||
try {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
|
||||
// Validate API key if enabled
|
||||
if (config.isEnable() && config.getApiKey() != null && !config.getApiKey().trim().isEmpty()) {
|
||||
OpenAiConfigService.ValidationResult validationResult = openAiConfigService.validateApiKey(config.getApiKey());
|
||||
|
||||
if (!validationResult.isValid()) {
|
||||
log.warn("API key validation failed during save: {}", validationResult.getMessage());
|
||||
response.put("code", 1);
|
||||
response.put("msg", "API key validation failed: " + validationResult.getMessage());
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
log.info("API key validation successful during save");
|
||||
}
|
||||
|
||||
// Save the configuration
|
||||
openAiConfigService.saveConfig(config);
|
||||
|
||||
response.put("code", 0);
|
||||
response.put("msg", "OpenAI configuration saved successfully");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to save OpenAI configuration", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("code", 1);
|
||||
response.put("msg", "Failed to save configuration: " + e.getMessage());
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/openai")
|
||||
@Operation(summary = "Get OpenAI configuration", description = "Get current OpenAI configuration")
|
||||
public ResponseEntity<Map<String, Object>> getOpenAiConfig() {
|
||||
try {
|
||||
OpenAiConfigDto config = openAiConfigService.getConfig();
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("code", 0);
|
||||
response.put("data", config);
|
||||
response.put("msg", "Success");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to get OpenAI configuration", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("code", 1);
|
||||
response.put("msg", "Failed to get configuration: " + e.getMessage());
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/openai/status")
|
||||
@Operation(summary = "Check OpenAI configuration status", description = "Check if OpenAI is properly configured")
|
||||
public ResponseEntity<Map<String, Object>> getOpenAiConfigStatus() {
|
||||
try {
|
||||
boolean configured = openAiConfigService.isConfigured();
|
||||
OpenAiConfigDto effectiveConfig = openAiConfigService.getEffectiveConfig();
|
||||
boolean hasDbConfig = openAiConfigService.getConfig() != null;
|
||||
boolean hasYamlConfig = effectiveConfig != null && !hasDbConfig;
|
||||
|
||||
// Validate the effective configuration
|
||||
boolean validationPassed = false;
|
||||
String validationMessage = "No configuration found";
|
||||
|
||||
if (effectiveConfig != null && effectiveConfig.isEnable() && effectiveConfig.getApiKey() != null && !effectiveConfig.getApiKey().trim().isEmpty()) {
|
||||
OpenAiConfigService.ValidationResult validationResult = openAiConfigService.validateApiKey(effectiveConfig.getApiKey());
|
||||
validationPassed = validationResult.isValid();
|
||||
validationMessage = validationResult.getMessage();
|
||||
|
||||
if (!validationPassed) {
|
||||
log.warn("OpenAI API key validation failed during status check: {}", validationMessage);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("code", 0);
|
||||
response.put("data", Map.of(
|
||||
"configured", configured && validationPassed,
|
||||
"hasDbConfig", hasDbConfig,
|
||||
"hasYamlConfig", hasYamlConfig,
|
||||
"validationPassed", validationPassed,
|
||||
"validationMessage", validationMessage
|
||||
));
|
||||
response.put("msg", "Success");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to get OpenAI configuration status", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("code", 1);
|
||||
response.put("msg", "Failed to get status: " + e.getMessage());
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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.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;
|
||||
|
||||
/**
|
||||
* OpenAI Agent Configuration Dao
|
||||
*/
|
||||
@Repository
|
||||
public interface OpenAiConfigDao extends JpaRepository<OpenAiConfig, String>, JpaSpecificationExecutor<OpenAiConfig> {
|
||||
|
||||
/**
|
||||
* Query by type
|
||||
* @param type type
|
||||
* @return Return the queried configuration information
|
||||
*/
|
||||
OpenAiConfig findByType(String type);
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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.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.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
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 java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* OpenAI Agent Config Entity
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "hzb_ai_agent_config")
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Schema(description = "OpenAI Agent config entity")
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public class OpenAiConfig {
|
||||
|
||||
@Id
|
||||
@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 = "Config content", description = "Config content,format json", accessMode = READ_WRITE)
|
||||
@Column(length = 8192)
|
||||
private String content;
|
||||
|
||||
@Schema(title = "The creator of this record", example = "tom", accessMode = READ_ONLY)
|
||||
@CreatedBy
|
||||
private String creator;
|
||||
|
||||
@Schema(title = "This record was last modified by", example = "tom", accessMode = READ_ONLY)
|
||||
@LastModifiedBy
|
||||
private String modifier;
|
||||
|
||||
@Schema(title = "This record creation time (millisecond timestamp)", accessMode = READ_ONLY)
|
||||
@CreatedDate
|
||||
private LocalDateTime gmtCreate;
|
||||
|
||||
@Schema(title = "Record the latest modification time (timestamp in milliseconds)", accessMode = READ_ONLY)
|
||||
@LastModifiedDate
|
||||
private LocalDateTime gmtUpdate;
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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.event;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
|
||||
/**
|
||||
* OpenAI configuration change event
|
||||
*/
|
||||
public class OpenAiConfigChangeEvent extends ApplicationEvent {
|
||||
|
||||
public OpenAiConfigChangeEvent(ApplicationContext source) {
|
||||
super(source);
|
||||
}
|
||||
}
|
||||
+4
-5
@@ -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;
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Chat response DTO for AI responses.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Chat response")
|
||||
public class ChatResponseDto {
|
||||
|
||||
@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 = "msg-user-123")
|
||||
private String userMessageId;
|
||||
|
||||
@Schema(description = "Assistant message ID", example = "msg-assistant-123")
|
||||
private String assistantMessageId;
|
||||
}
|
||||
+53
@@ -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;
|
||||
}
|
||||
+87
@@ -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;
|
||||
}
|
||||
+52
@@ -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;
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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 jakarta.validation.constraints.NotBlank;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* OpenAI Configuration DTO - simplified to handle only API key
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@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;
|
||||
}
|
||||
+3
-9
@@ -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();
|
||||
}
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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.pojo.dto.ChatResponseDto;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.ConversationDto;
|
||||
import org.springframework.http.codec.ServerSentEvent;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Service for managing chat conversations and interactions with LLM providers.
|
||||
*/
|
||||
public interface ConversationService {
|
||||
|
||||
/**
|
||||
* Create a new conversation
|
||||
*
|
||||
* @return Created conversation data
|
||||
*/
|
||||
ConversationDto createConversation();
|
||||
|
||||
/**
|
||||
* Send a message and receive a streaming response
|
||||
*
|
||||
* @param message The user's message
|
||||
* @param conversationId Optional conversation ID for continuing a chat
|
||||
* @return Flux of ServerSentEvent for streaming the response
|
||||
*/
|
||||
Flux<ServerSentEvent<ChatResponseDto>> streamChat(String message, String conversationId);
|
||||
|
||||
|
||||
/**
|
||||
* Get conversation history for a specific conversation
|
||||
*
|
||||
* @param conversationId Conversation ID
|
||||
* @return Conversation data including messages
|
||||
*/
|
||||
ConversationDto getConversation(String conversationId);
|
||||
|
||||
/**
|
||||
* Get all conversations for the current user
|
||||
*
|
||||
* @return List of conversations
|
||||
*/
|
||||
List<ConversationDto> getAllConversations();
|
||||
|
||||
/**
|
||||
* Delete a conversation
|
||||
*
|
||||
* @param conversationId Conversation ID to delete
|
||||
* @return true if deleted, false if conversation not found
|
||||
*/
|
||||
boolean deleteConversation(String conversationId);
|
||||
|
||||
/**
|
||||
* Check if a conversation exists
|
||||
*
|
||||
* @param conversationId Conversation ID to check
|
||||
* @return true if conversation exists, false otherwise
|
||||
*/
|
||||
boolean conversationExists(String conversationId);
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
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;
|
||||
import org.springframework.web.servlet.function.ServerResponse;
|
||||
|
||||
/**
|
||||
* Service interface for MCP server operations.
|
||||
*/
|
||||
public interface McpServerService {
|
||||
|
||||
/**
|
||||
* Provides the HertzBeat tools for the MCP server
|
||||
* @return ToolCallbackProvider with all HertzBeat monitoring tools
|
||||
*/
|
||||
ToolCallbackProvider hertzbeatTools();
|
||||
|
||||
/**
|
||||
* Provides a custom SSE server transport for the MCP server
|
||||
* @param objectMapper the ObjectMapper instance for JSON serialization
|
||||
* @param serverProperties the properties for the MCP server configuration
|
||||
* @return a CustomSseServerTransport instance configured with the provided properties
|
||||
*/
|
||||
CustomSseServerTransport webMvcSseServerTransportProvider(
|
||||
ObjectMapper objectMapper,
|
||||
McpServerProperties serverProperties
|
||||
);
|
||||
|
||||
/**
|
||||
* Provides the MCP server router function for web MVC
|
||||
* @param transport Custom SSE server transport
|
||||
* @return RouterFunction for handling MCP server requests
|
||||
*/
|
||||
RouterFunction<ServerResponse> mvcMcpRouterFunction(CustomSseServerTransport transport);
|
||||
}
|
||||
+100
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+100
@@ -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.impl;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.agent.config.PromptProvider;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.MessageDto;
|
||||
import org.apache.hertzbeat.ai.agent.service.ChatClientProviderService;
|
||||
import org.springframework.stereotype.Service;
|
||||
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;
|
||||
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 reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Implementation of the {@link ChatClientProviderService}.
|
||||
* Provides functionality to interact with the ChatClient for handling chat
|
||||
* messages.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class ChatClientProviderServiceImpl implements ChatClientProviderService {
|
||||
|
||||
private final ChatClient chatClient;
|
||||
|
||||
@Qualifier("hertzbeatTools")
|
||||
@Autowired
|
||||
private ToolCallbackProvider toolCallbackProvider;
|
||||
|
||||
@Autowired
|
||||
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 {
|
||||
List<Message> messages = new ArrayList<>();
|
||||
|
||||
// Add conversation history if available
|
||||
if (context.getConversationHistory() != null && !context.getConversationHistory().isEmpty()) {
|
||||
for (MessageDto historyMessage : context.getConversationHistory()) {
|
||||
if ("user".equals(historyMessage.getRole())) {
|
||||
messages.add(new UserMessage(historyMessage.getContent()));
|
||||
} else if ("assistant".equals(historyMessage.getRole())) {
|
||||
messages.add(new AssistantMessage(historyMessage.getContent()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
messages.add(new UserMessage(context.getMessage()));
|
||||
|
||||
log.info("Starting streaming chat for conversation: {}", context.getConversationId());
|
||||
|
||||
return this.chatClient.prompt()
|
||||
.messages(messages)
|
||||
.system(PromptProvider.HERTZBEAT_SYSTEM_PROMPT)
|
||||
.toolCallbacks(toolCallbackProvider)
|
||||
.stream()
|
||||
.content()
|
||||
.doOnComplete(() -> log.info("Streaming completed for conversation: {}", context.getConversationId()))
|
||||
.doOnError(error -> log.error("Error in streaming chat: {}", error.getMessage(), error));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error setting up streaming chat: {}", e.getMessage(), e);
|
||||
return Flux.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+276
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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 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;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.web.servlet.function.RouterFunction;
|
||||
import org.springframework.web.servlet.function.ServerResponse;
|
||||
|
||||
/**
|
||||
* Implementation of the McpServerService interface.
|
||||
* This service provides functionality for handling MCP server operations.
|
||||
*/
|
||||
@Service
|
||||
@Configuration
|
||||
public class McpServerServiceImpl implements McpServerService {
|
||||
@Autowired
|
||||
private MonitorTools monitorTools;
|
||||
@Autowired
|
||||
private AlertTools alertTools;
|
||||
@Autowired
|
||||
private MetricsTools metricsTools;
|
||||
@Autowired
|
||||
private AlertDefineTools alertDefineTools;
|
||||
|
||||
@Bean
|
||||
public ToolCallbackProvider hertzbeatTools() {
|
||||
return MethodToolCallbackProvider.builder().toolObjects(monitorTools, alertTools, alertDefineTools, metricsTools).build();
|
||||
}
|
||||
/**
|
||||
* Provides a custom SSE server transport for the MCP server.
|
||||
*
|
||||
* @param objectMapper the ObjectMapper instance for JSON serialization
|
||||
* @param serverProperties the properties for the MCP server configuration
|
||||
* @return a CustomSseServerTransport instance configured with the provided properties
|
||||
*/
|
||||
|
||||
@Bean
|
||||
public CustomSseServerTransport webMvcSseServerTransportProvider(
|
||||
ObjectMapper objectMapper,
|
||||
McpServerProperties serverProperties
|
||||
) {
|
||||
return new CustomSseServerTransport(
|
||||
objectMapper,
|
||||
serverProperties.getBaseUrl(),
|
||||
serverProperties.getSseMessageEndpoint(),
|
||||
serverProperties.getSseEndpoint()
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Provides the MCP server transport bean.
|
||||
*
|
||||
* @param transport the custom SSE server transport
|
||||
* @return the MCP server transport instance
|
||||
*/
|
||||
|
||||
@Primary
|
||||
@Bean
|
||||
public RouterFunction<ServerResponse> mvcMcpRouterFunction(CustomSseServerTransport transport) {
|
||||
return transport.getRouterFunction();
|
||||
}
|
||||
}
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.agent.config.OpenAiYamlConfig;
|
||||
import org.apache.hertzbeat.ai.agent.dao.OpenAiConfigDao;
|
||||
import org.apache.hertzbeat.ai.agent.entity.OpenAiConfig;
|
||||
import org.apache.hertzbeat.ai.agent.event.OpenAiConfigChangeEvent;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.OpenAiConfigDto;
|
||||
import org.apache.hertzbeat.ai.agent.service.OpenAiConfigService;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* OpenAI Configuration Service Implementation
|
||||
* Consolidated service for OpenAI configuration, validation, and client factory management
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class OpenAiConfigServiceImpl implements OpenAiConfigService {
|
||||
|
||||
private static final String CONFIG_TYPE = "openai";
|
||||
private static final String OPENAI_MODELS_ENDPOINT = "https://api.openai.com/v1/models";
|
||||
|
||||
private final OpenAiConfigDao openAiConfigDao;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final ApplicationContext applicationContext;
|
||||
private final OpenAiYamlConfig yamlConfig;
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
// Client factory cache
|
||||
private volatile OpenAiConfigDto currentConfig;
|
||||
|
||||
public OpenAiConfigServiceImpl(OpenAiConfigDao openAiConfigDao,
|
||||
ObjectMapper objectMapper,
|
||||
ApplicationContext applicationContext,
|
||||
OpenAiYamlConfig yamlConfig) {
|
||||
this.openAiConfigDao = openAiConfigDao;
|
||||
this.objectMapper = objectMapper;
|
||||
this.applicationContext = applicationContext;
|
||||
this.yamlConfig = yamlConfig;
|
||||
this.restTemplate = new RestTemplate();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void saveConfig(OpenAiConfigDto config) {
|
||||
try {
|
||||
String contentJson = objectMapper.writeValueAsString(config);
|
||||
|
||||
OpenAiConfig openAiConfig = OpenAiConfig.builder()
|
||||
.type(CONFIG_TYPE)
|
||||
.content(contentJson)
|
||||
.build();
|
||||
|
||||
openAiConfigDao.save(openAiConfig);
|
||||
log.info("OpenAI configuration saved successfully");
|
||||
|
||||
applicationContext.publishEvent(new OpenAiConfigChangeEvent(applicationContext));
|
||||
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalArgumentException("Failed to save OpenAI configuration: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public OpenAiConfigDto getConfig() {
|
||||
OpenAiConfig config = openAiConfigDao.findByType(CONFIG_TYPE);
|
||||
if (config == null || !StringUtils.hasText(config.getContent())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return objectMapper.readValue(config.getContent(), OpenAiConfigDto.class);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalArgumentException("Failed to parse OpenAI configuration: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConfigured() {
|
||||
OpenAiConfigDto effective = getEffectiveConfig();
|
||||
return effective != null && effective.isEnable() && StringUtils.hasText(effective.getApiKey());
|
||||
}
|
||||
|
||||
@Override
|
||||
public OpenAiConfigDto getEffectiveConfig() {
|
||||
OpenAiConfigDto dbConfig = getConfig();
|
||||
if (dbConfig != null && dbConfig.isEnable() && StringUtils.hasText(dbConfig.getApiKey())) {
|
||||
log.debug("Using database OpenAI configuration");
|
||||
return dbConfig;
|
||||
}
|
||||
|
||||
if (yamlConfig != null && yamlConfig.isEnable() && StringUtils.hasText(yamlConfig.getApiKey())) {
|
||||
log.debug("Using YAML OpenAI configuration from spring.ai.openai.api-key");
|
||||
OpenAiConfigDto yamlDto = new OpenAiConfigDto();
|
||||
yamlDto.setEnable(true);
|
||||
yamlDto.setApiKey(yamlConfig.getApiKey());
|
||||
return yamlDto;
|
||||
}
|
||||
|
||||
log.debug("No valid OpenAI configuration found");
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ValidationResult validateApiKey(String apiKey) {
|
||||
if (!StringUtils.hasText(apiKey)) {
|
||||
return ValidationResult.failure("API key cannot be empty");
|
||||
}
|
||||
|
||||
if (!apiKey.startsWith("sk-")) {
|
||||
return ValidationResult.failure("Invalid API key format. OpenAI API keys should start with 'sk-'");
|
||||
}
|
||||
|
||||
try {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("Authorization", "Bearer " + apiKey);
|
||||
headers.set("Content-Type", "application/json");
|
||||
|
||||
HttpEntity<String> entity = new HttpEntity<>(headers);
|
||||
|
||||
log.debug("Validating OpenAI API key by calling models endpoint");
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
OPENAI_MODELS_ENDPOINT,
|
||||
HttpMethod.GET,
|
||||
entity,
|
||||
String.class
|
||||
);
|
||||
|
||||
if (response.getStatusCode() == HttpStatus.OK) {
|
||||
log.info("OpenAI API key validation successful");
|
||||
return ValidationResult.success("API key is valid");
|
||||
} else {
|
||||
log.warn("OpenAI API key validation failed with status: {}", response.getStatusCode());
|
||||
return ValidationResult.failure("API key validation failed: " + response.getStatusCode());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error validating OpenAI API key", e);
|
||||
String errorMessage = e.getMessage();
|
||||
|
||||
// Parse common error messages
|
||||
if (errorMessage.contains("401")) {
|
||||
return ValidationResult.failure("Invalid API key - authentication failed");
|
||||
} else if (errorMessage.contains("403")) {
|
||||
return ValidationResult.failure("API key does not have permission to access models");
|
||||
} else if (errorMessage.contains("429")) {
|
||||
return ValidationResult.failure("Rate limit exceeded - please try again later");
|
||||
} else if (errorMessage.contains("timeout") || errorMessage.contains("connect")) {
|
||||
return ValidationResult.failure("Network error - unable to connect to OpenAI API");
|
||||
} else {
|
||||
return ValidationResult.failure("API key validation failed: " + errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reloadConfig() {
|
||||
synchronized (this) {
|
||||
currentConfig = null; // Force reload
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenAI configuration change event listener
|
||||
*/
|
||||
@EventListener(OpenAiConfigChangeEvent.class)
|
||||
public void onOpenAiConfigChange(OpenAiConfigChangeEvent event) {
|
||||
log.info("[OpenAiConfigService] OpenAI configuration change event received");
|
||||
reloadConfig();
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -15,7 +15,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.tools;
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.tools;
|
||||
|
||||
/**
|
||||
* Tools for alert operations and alarm data queries
|
||||
@@ -41,4 +42,5 @@ public interface AlertTools {
|
||||
*/
|
||||
String getAlertsSummary();
|
||||
|
||||
|
||||
}
|
||||
+53
@@ -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.tools;
|
||||
|
||||
/**
|
||||
* Tools for metrics data operations and queries
|
||||
*/
|
||||
public interface MetricsTools {
|
||||
|
||||
/**
|
||||
* Get real-time metrics data for a monitor
|
||||
* @param monitorId Monitor ID
|
||||
* @param metrics Metrics name (e.g., "system", "cpu", "memory")
|
||||
* @return Formatted real-time metrics data
|
||||
*/
|
||||
String getRealtimeMetrics(Long monitorId, String metrics);
|
||||
|
||||
/**
|
||||
* Get historical metrics data for a monitor
|
||||
* @param monitorId Monitor ID
|
||||
* @param app Monitor type (e.g., "linux", "mysql", "http")
|
||||
* @param metrics Metrics name (e.g., "system", "cpu", "memory")
|
||||
* @param metric Specific metric field (e.g., "usage", "used", "available")
|
||||
* @param label Label filter for specific instances
|
||||
* @param history Time range (e.g., "1h", "6h", "24h", "7d")
|
||||
* @param interval Whether to aggregate data with intervals
|
||||
* @return Historical metrics data formatted for display
|
||||
*/
|
||||
String getHistoricalMetrics(Long monitorId, String app, String metrics, String metric, String label, String history, Boolean interval);
|
||||
|
||||
/**
|
||||
* Check warehouse storage server status
|
||||
* @return Status of the metrics storage system
|
||||
*/
|
||||
String getWarehouseStatus();
|
||||
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 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.tools;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Interface for Monitoring Tools
|
||||
*/
|
||||
public interface MonitorTools {
|
||||
|
||||
/**
|
||||
* Add a new monitor with comprehensive configuration
|
||||
*
|
||||
* @param name Monitor name
|
||||
* @param app Monitor type/application (e.g., 'linux', 'mysql', 'http')
|
||||
* @param host Target host (IP address or domain name)
|
||||
* @param port Target port (optional, depends on monitor type)
|
||||
* @param intervals Collection interval in seconds (default: 600)
|
||||
* @param username Username for authentication (optional)
|
||||
* @param password Password for authentication (optional)
|
||||
* @param database Database name (for database monitors)
|
||||
* @param additionalParams Additional app-specific parameters as JSON string (optional)
|
||||
* @param description Monitor description (optional)
|
||||
* @return Result message with monitor ID if successful
|
||||
*/
|
||||
String addMonitor(
|
||||
String name,
|
||||
String app,
|
||||
String host,
|
||||
Integer port,
|
||||
Integer intervals,
|
||||
String username,
|
||||
String password,
|
||||
String database,
|
||||
String additionalParams,
|
||||
String description
|
||||
);
|
||||
|
||||
/**
|
||||
* List all available monitor types that can be added
|
||||
*
|
||||
* @param language Language code for localized names (e.g., 'en-US', 'zh-CN')
|
||||
* @return Formatted string list of available monitor types with descriptions
|
||||
*/
|
||||
String listMonitorTypes(String language);
|
||||
|
||||
/**
|
||||
* Comprehensive monitor querying with flexible filtering, pagination, and specialized views
|
||||
* @param ids Specific monitor IDs to retrieve (optional)
|
||||
* @param app Monitor type filter (linux, mysql, http, etc.)
|
||||
* @param status Monitor status (1=online, 2=offline, 3=unreachable, 0=paused, 9=all)
|
||||
* @param search Search in monitor names or hosts (partial matching)
|
||||
* @param labels Label filters, format: 'key1:value1,key2:value2'
|
||||
* @param sort Sort field (name, gmtCreate, gmtUpdate, status, app)
|
||||
* @param order Sort order (asc, desc)
|
||||
* @param pageIndex Page number starting from 0
|
||||
* @param pageSize Items per page (1-100 recommended)
|
||||
* @param includeStats Include status statistics summary
|
||||
* @return Comprehensive monitor information with optional statistics
|
||||
*/
|
||||
String queryMonitors(
|
||||
List<Long> ids,
|
||||
String app,
|
||||
Byte status,
|
||||
String search,
|
||||
String labels,
|
||||
String sort,
|
||||
String order,
|
||||
Integer pageIndex,
|
||||
Integer pageSize,
|
||||
Boolean includeStats);
|
||||
|
||||
/**
|
||||
* Get parameter definitions required for a specific monitor type
|
||||
*
|
||||
* @param app Monitor type/application name (e.g., 'linux', 'mysql', 'redis')
|
||||
* @return Formatted string with parameter definitions including field names, types, and requirements
|
||||
*/
|
||||
String getMonitorAdditionalParams(String app);
|
||||
}
|
||||
+61
-88
@@ -15,20 +15,19 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.tools.impl;
|
||||
package org.apache.hertzbeat.ai.agent.tools.impl;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.node.ArrayNode;
|
||||
import tools.jackson.databind.node.ObjectNode;
|
||||
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,13 +46,12 @@ import java.util.Map;
|
||||
@Service
|
||||
public class AlertDefineToolsImpl implements AlertDefineTools {
|
||||
@Autowired
|
||||
private AlertDefineService alertDefineService;
|
||||
@Autowired
|
||||
private AppService appService;
|
||||
private AlertDefineServiceAdapter alertDefineServiceAdapter;
|
||||
|
||||
|
||||
@Override
|
||||
@Tool(name = "create_alert_rule", description = """
|
||||
HertzBeat: ALERT RULE means when to alert a user
|
||||
ALERT RULE means when to alert a user
|
||||
THESE ARE ALERT RULES WITH THRESHOLD VALUES. USERS CAN SPECIFY THE THRESHOLD VALUES FOR EXAMPLE,
|
||||
IF THE USER SAYS "ALERT ME WHEN MY COST EXCEEDS 700, THE EXPRESSION SHOULD BE 'cost > 700' NOT 'cost < 700'.
|
||||
APPLY THE SAME LOGIC FOR LESS THAN OPERATOR.
|
||||
@@ -74,30 +72,30 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
|
||||
8. based on the user's output, conditionally call the bind_monitors_to_alert_rule tool to bind monitors to the alert rule
|
||||
VERY VERY IMPORTANT:
|
||||
- ALWAYS USE the value field from the get_apps_metrics_hierarchy's json response when creating alert expressions on the field parameters
|
||||
|
||||
|
||||
EXAMPLES FOR FIELD CONDITION EXPRESSION( Do not copy these examples, they are just for reference ):
|
||||
These are all just examples, you can take inspiration from them and create a rule based on hierarchy, always ask the user for all params, do not assume them, even for these examples:
|
||||
|
||||
|
||||
1. Kafka JVM Alert:
|
||||
- App: "kafka", Metric: "jvm_basic"
|
||||
- Field condition: equals(VmName, "myVM")
|
||||
- Field condition expression: equals(VmName, "myVM")
|
||||
|
||||
|
||||
2. LLM Credits Alert:
|
||||
- App: "openai", Metric: "credit_grants"
|
||||
- Field condition: total_granted > some_value
|
||||
- Field condition expression: total_granted > 1000
|
||||
|
||||
|
||||
3. HBase Master Alert:
|
||||
- App: "hbase_master", Metric: "server"
|
||||
- Field condition: heap_memory_used > 80 or some_factor<100
|
||||
- Field condition expression: heap_memory_used > 80 or some_factor<100
|
||||
|
||||
|
||||
4. Complex OpenAI Credits Alert:
|
||||
- App: "openai", Metric: "credit_grants"
|
||||
- Field condition: total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)
|
||||
- Field condition expression: total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)
|
||||
|
||||
|
||||
FIELD CONDITIONS GUIDANCE:
|
||||
- Field names come from metric's children in hierarchy (leaf nodes)
|
||||
- Use the "value" field from the metric's children, not the label when creating conditions
|
||||
@@ -109,7 +107,7 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
|
||||
- String values should be quoted: equals(VmName, "my-vm")
|
||||
- Simple conditions: heap_memory_used > 80, total_granted <= 1000
|
||||
- Complex conditions: total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)
|
||||
|
||||
|
||||
PRIORITY LEVELS:
|
||||
- 0: Critical (immediate action required)
|
||||
- 1: Warning (attention needed, default)
|
||||
@@ -196,7 +194,7 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
|
||||
return expressionValidation; // Return expression validation error message
|
||||
}
|
||||
|
||||
String expr = String.format("equals(__app__,\"%s\") && equals(__metrics__,\"%s\") && %s",
|
||||
String expr = String.format("equals(__app__,\"%s\") && equals(__metrics__,\"%s\") && %s",
|
||||
app.trim(), metrics.trim(), fieldConditions.trim());
|
||||
|
||||
|
||||
@@ -243,18 +241,16 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
|
||||
.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()));
|
||||
response.append(String.format("Successfully created %s alert rule '%s' with ID: %d\n",
|
||||
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));
|
||||
@@ -280,7 +276,7 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
|
||||
|
||||
@Override
|
||||
@Tool(name = "list_alert_rules", description = """
|
||||
HertzBeat: List existing alert rules with filtering options.
|
||||
List existing alert rules with filtering options.
|
||||
Shows configured thresholds and alert definitions.
|
||||
""")
|
||||
public String listAlertRules(
|
||||
@@ -300,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())
|
||||
@@ -342,7 +335,7 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
|
||||
|
||||
@Override
|
||||
@Tool(name = "toggle_alert_rule", description = """
|
||||
HertzBeat: Enable or disable an alert rule.
|
||||
Enable or disable an alert rule.
|
||||
Allows activating or deactivating threshold monitoring.
|
||||
""")
|
||||
public String toggleAlertRule(
|
||||
@@ -352,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",
|
||||
@@ -378,9 +357,10 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@Tool(name = "get_alert_rule_details", description = """
|
||||
HertzBeat: Get detailed information about a specific alert rule.
|
||||
Get detailed information about a specific alert rule.
|
||||
Shows complete threshold configuration and rule settings.
|
||||
""")
|
||||
public String getAlertRuleDetails(
|
||||
@@ -389,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";
|
||||
}
|
||||
@@ -439,10 +416,10 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
|
||||
|
||||
@Override
|
||||
@Tool(name = "get_apps_metrics_hierarchy", description = """
|
||||
HertzBeat: Get the hierarchical structure of all available apps and their metrics for alert rule creation.
|
||||
Get the hierarchical structure of all available apps and their metrics for alert rule creation.
|
||||
This tool provides the exact app name, metric name and corresponding param names according to each metric.
|
||||
Returns structured JSON data showing the complete hierarchy with field parameters for alert expressions.
|
||||
|
||||
|
||||
JSON Structure:
|
||||
- app: The application name
|
||||
- description: Tool description
|
||||
@@ -452,7 +429,7 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
|
||||
- Non-leaf nodes have: children array
|
||||
VERY IMPORTANT:
|
||||
- ALWAYS USE the value field from the field parameters when creating alert expressions.
|
||||
|
||||
|
||||
This structured data is needed to create proper alert expressions.
|
||||
""")
|
||||
public String getAppsMetricsHierarchy(
|
||||
@@ -464,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();
|
||||
@@ -482,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);
|
||||
@@ -492,7 +472,7 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
|
||||
|
||||
@Override
|
||||
@Tool(name = "bind_monitors_to_alert_rule", description = """
|
||||
HertzBeat: Bind monitors to an alert rule.
|
||||
Bind monitors to an alert rule.
|
||||
Call this tool if users want to bind specific monitors to their alert rule.
|
||||
Get the right monitor ids for a particular app using the query_monitors tool.
|
||||
Get the alert rule ID from the create_alert_rule tool output OR use the list_alert_rules tool with app_name search filter, if the output of create_alert_rule is not applicable.
|
||||
@@ -514,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);
|
||||
}
|
||||
@@ -524,7 +502,7 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
|
||||
// Parse monitor IDs from comma-separated string
|
||||
String[] monitorIdArray = monitorIds.split(",");
|
||||
List<String> validMonitorIds = new ArrayList<>();
|
||||
|
||||
|
||||
for (String monitorId : monitorIdArray) {
|
||||
String trimmedId = monitorId.trim();
|
||||
if (!trimmedId.isEmpty()) {
|
||||
@@ -536,7 +514,7 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (validMonitorIds.isEmpty()) {
|
||||
return "Error: No valid monitor IDs provided";
|
||||
}
|
||||
@@ -560,19 +538,19 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
|
||||
// Get the current expression and modify it
|
||||
String currentExpr = existingRule.getExpr();
|
||||
String newExpr;
|
||||
|
||||
|
||||
// Check if the expression already has __instance__ conditions
|
||||
if (currentExpr.contains("__instance__")) {
|
||||
// Extract existing monitor IDs and merge with new ones
|
||||
List<String> existingMonitorIds = UtilityClass.extractExistingMonitorIds(currentExpr);
|
||||
|
||||
|
||||
// Add new monitor IDs that aren't already present
|
||||
for (String newId : validMonitorIds) {
|
||||
if (!existingMonitorIds.contains(newId)) {
|
||||
existingMonitorIds.add(newId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
String updatedMonitorCondition;
|
||||
if (existingMonitorIds.size() == 1) {
|
||||
updatedMonitorCondition = String.format("equals(__instance__, \"%s\")", existingMonitorIds.get(0));
|
||||
@@ -587,31 +565,29 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
|
||||
conditionBuilder.append(")");
|
||||
updatedMonitorCondition = conditionBuilder.toString();
|
||||
}
|
||||
|
||||
|
||||
// Replace existing __instance__ conditions with updated ones
|
||||
newExpr = UtilityClass.replaceInstanceConditions(currentExpr, updatedMonitorCondition);
|
||||
|
||||
|
||||
// Update the alert rule
|
||||
existingRule.setExpr(newExpr);
|
||||
log.debug("Current security subject for modifyAlertDefine: {}", subjectSum);
|
||||
|
||||
alertDefineService.modifyAlertDefine(existingRule);
|
||||
|
||||
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",
|
||||
return String.format("Successfully added %d new monitor(s) to alert rule ID %d.\nTotal bound monitors: %s\nUpdated expression: %s",
|
||||
validMonitorIds.size(), ruleId, String.join(", ", existingMonitorIds), newExpr);
|
||||
}
|
||||
|
||||
|
||||
// Insert the monitor condition after the metrics condition
|
||||
// Pattern: equals(__app__,"app") && equals(__metrics__,"metric") && [existing_conditions]
|
||||
// Result: equals(__app__,"app") && equals(__metrics__,"metric") && [monitor_condition] && [existing_conditions]
|
||||
|
||||
|
||||
if (currentExpr.matches(".*equals\\(__app__,\"[^\"]+\"\\)\\s*&&\\s*equals\\(__metrics__,\"[^\"]+\"\\)\\s*&&\\s*.*")) {
|
||||
// Find the position after the metrics condition
|
||||
String metricsPattern = "equals\\(__metrics__,\"[^\"]+\"\\)";
|
||||
java.util.regex.Pattern regex = java.util.regex.Pattern.compile(metricsPattern);
|
||||
java.util.regex.Matcher matcher = regex.matcher(currentExpr);
|
||||
|
||||
|
||||
if (matcher.find()) {
|
||||
int metricsEnd = matcher.end();
|
||||
// Find the " && " after the metrics condition
|
||||
@@ -632,11 +608,10 @@ 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",
|
||||
return String.format("Successfully bound %d monitor(s) to alert rule ID %d.\nMonitor IDs: %s\nUpdated expression: %s",
|
||||
validMonitorIds.size(), ruleId, String.join(", ", validMonitorIds), newExpr);
|
||||
|
||||
} catch (Exception e) {
|
||||
@@ -645,6 +620,7 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Validates that the app, metric, and field conditions are valid according to hierarchy
|
||||
* @param app App name to validate
|
||||
@@ -657,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);
|
||||
@@ -689,4 +662,4 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+14
-11
@@ -15,15 +15,16 @@
|
||||
* 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.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;
|
||||
@@ -32,6 +33,7 @@ import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.ai.tool.annotation.ToolParam;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
|
||||
/**
|
||||
* Implementation of Alert Tools functionality for alarm data queries and management
|
||||
*/
|
||||
@@ -39,11 +41,11 @@ import org.springframework.stereotype.Service;
|
||||
@Service
|
||||
public class AlertToolsImpl implements AlertTools {
|
||||
@Autowired
|
||||
private AlertService alertService;
|
||||
private AlertServiceAdapter alertServiceAdapter;
|
||||
|
||||
@Override
|
||||
@Tool(name = "query_alerts", description = """
|
||||
HertzBeat: Query alerts with comprehensive filtering and pagination options.
|
||||
Query alerts with comprehensive filtering and pagination options.
|
||||
|
||||
ALERT TYPES:
|
||||
- Pass alertType='single' for individual alert instances
|
||||
@@ -114,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");
|
||||
@@ -152,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");
|
||||
@@ -191,9 +193,10 @@ public class AlertToolsImpl implements AlertTools {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@Tool(name = "get_alerts_summary", description = """
|
||||
HertzBeat: Get alerts summary statistics including total counts, status distribution, and recent trends.
|
||||
Get alerts summary statistics including total counts, status distribution, and recent trends.
|
||||
Returns comprehensive overview of the current alerting status across all monitors.
|
||||
""")
|
||||
public String getAlertsSummary() {
|
||||
@@ -202,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");
|
||||
+23
-20
@@ -15,13 +15,15 @@
|
||||
* 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.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;
|
||||
@@ -42,11 +44,13 @@ 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 = """
|
||||
HertzBeat: Get the supported monitor types/names from the list_monitor_types tool, make sure to use right name in the next call
|
||||
Get the supported monitor types/names from the list_monitor_types tool, make sure to use right name in the next call
|
||||
Use the query_monitors tool to find monitor IDs in case the user does not tell the id explicitly. You might have to use this multiple times based on the user's query
|
||||
Get real-time metrics data for a specific monitor.
|
||||
Returns current metrics values including CPU, memory, disk usage, etc.
|
||||
@@ -74,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);
|
||||
@@ -143,15 +147,15 @@ public class MetricsToolsImpl implements MetricsTools {
|
||||
|
||||
@Override
|
||||
@Tool(name = "get_historical_metrics", description = """
|
||||
HertzBeat: Get historical metrics data for analysis and trending.
|
||||
Get historical metrics data for analysis and trending.
|
||||
Returns time-series data for specified metrics over a time range.
|
||||
Use the query_monitors tool to find the correct monitor IDs/ name or type for the monitor(s) user asked the metrics for
|
||||
Pass that name into the get_apps_metrics_hierarchy tool to get the metrics hierarchy i.e metrics and the field parameter
|
||||
Pass that name into the get_apps_metrics_hierarchy tool to get the metrics hierarchy i.e metrics and the field paramater
|
||||
DO NOT USE THE LABEL FIELD ALWAYS USE THE VALUE FIELD FROM THE HIERARCHY JSON
|
||||
Ask user to provide the filters for labels, history and interval aggregation
|
||||
""")
|
||||
public String getHistoricalMetrics(
|
||||
@ToolParam(description = "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,
|
||||
@@ -160,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";
|
||||
@@ -169,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");
|
||||
|
||||
@@ -222,18 +226,17 @@ public class MetricsToolsImpl implements MetricsTools {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@Tool(name = "get_warehouse_status", description = """
|
||||
HertzBeat: Check the status of the metrics storage warehouse system.
|
||||
Check the status of the metrics storage warehouse system.
|
||||
Returns whether the metrics storage is operational and accessible.
|
||||
""")
|
||||
public String getWarehouseStatus() {
|
||||
try {
|
||||
log.info("Checking warehouse storage status");
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current security subject for getWarehouseStorageServerStatus: {}", subjectSum);
|
||||
|
||||
Boolean status = metricsDataService.getWarehouseStorageServerStatus();
|
||||
Boolean status = metricsServiceAdapter.getWarehouseStorageServerStatus();
|
||||
|
||||
StringBuilder response = new StringBuilder();
|
||||
response.append("METRICS WAREHOUSE STATUS\n");
|
||||
@@ -257,4 +260,4 @@ public class MetricsToolsImpl implements MetricsTools {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+491
@@ -0,0 +1,491 @@
|
||||
/*
|
||||
* 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.tools.impl;
|
||||
|
||||
import com.usthe.sureness.subject.SubjectSum;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.agent.adapters.MonitorServiceAdapter;
|
||||
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
|
||||
import org.apache.hertzbeat.ai.agent.utils.UtilityClass;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.apache.hertzbeat.ai.agent.tools.MonitorTools;
|
||||
import org.springframework.ai.tool.annotation.ToolParam;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.apache.hertzbeat.common.entity.manager.Monitor;
|
||||
import org.apache.hertzbeat.common.entity.manager.Param;
|
||||
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Implementation of Monitoring Tools functionality
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class MonitorToolsImpl implements MonitorTools {
|
||||
@Autowired
|
||||
private MonitorServiceAdapter monitorServiceAdapter;
|
||||
|
||||
|
||||
/**
|
||||
* Tool to query monitor information with flexible filtering and pagination.
|
||||
* Supports filtering by monitor IDs, type, status, host, labels, sorting, and
|
||||
* pagination.
|
||||
* Returns detailed monitor information including ID, name, type, host, and status.
|
||||
*/
|
||||
@Override
|
||||
@Tool(name = "query_monitors", description = """
|
||||
Query Existing/configured monitors in HertzBeat.
|
||||
This tool retrieves monitors based on various filters and parameters.
|
||||
Comprehensive monitor querying with flexible filtering, pagination, and specialized views.
|
||||
|
||||
MONITOR STATUSES:
|
||||
- status=1: Online/Active monitors (healthy, responding normally)
|
||||
- status=2: Offline monitors (not responding, connection failed)
|
||||
- status=3: Unreachable monitors (network/connectivity issues)
|
||||
- status=0: Paused monitors (manually disabled/suspended)
|
||||
- status=9 or null: All monitors regardless of status (default)
|
||||
|
||||
COMMON USE CASES & PARAMETER COMBINATIONS:
|
||||
|
||||
1. BASIC MONITOR LISTING:
|
||||
- Default: No parameters (shows all monitors, 8 per page)
|
||||
- By type: app='linux' (show only Linux monitors)
|
||||
- Search: search='web' (find monitors with 'web' in name/host)
|
||||
|
||||
2. STATUS-BASED QUERIES:
|
||||
- Healthy monitors: status=1, pageSize=50
|
||||
- Problem monitors: status=2 or status=3, pageSize=50
|
||||
- Offline monitors only: status=2
|
||||
- Unreachable monitors only: status=3
|
||||
- Paused monitors: status=0
|
||||
|
||||
3. MONITORING HEALTH OVERVIEW:
|
||||
- All statuses with statistics: status=9, includeStats=true, pageSize=100
|
||||
- Unhealthy monitors: Pass both status=2 AND status=3 (make 2 separate calls)
|
||||
|
||||
4. ADVANCED FILTERING:
|
||||
- Specific monitor types: app='mysql', status=1 (healthy MySQL monitors)
|
||||
- Label-based: labels='env:prod,critical:true'
|
||||
- Host search: search='192.168' (find by IP pattern)
|
||||
- Monitor IDs: ids=[1,2,3] (specific monitors by ID)
|
||||
|
||||
5. SORTING & PAGINATION:
|
||||
- Recently updated: sort='gmtUpdate', order='desc'
|
||||
- Alphabetical: sort='name', order='asc'
|
||||
- By creation: sort='gmtCreate', order='desc' (newest first)
|
||||
- Large datasets: pageSize=50-100 for bulk operations
|
||||
|
||||
RESPONSE FORMAT:
|
||||
- includeStats=true: Adds status distribution summary at top
|
||||
- Default: Simple list with ID, name, type, host, status
|
||||
- Shows total count and pagination info
|
||||
""")
|
||||
public String queryMonitors(
|
||||
@ToolParam(description = "Specific monitor IDs to retrieve (optional)", required = false) List<Long> ids,
|
||||
@ToolParam(description = "Monitor type filter: 'linux', 'mysql', 'http', 'redis', etc. (optional)", required = false) String app,
|
||||
@ToolParam(description = "Monitor status: 1=online, 2=offline, 3=unreachable, 0=paused, 9=all (default: 9)", required = false) Byte status,
|
||||
@ToolParam(description = "Search in monitor names or hosts (partial matching)", required = false) String search,
|
||||
@ToolParam(description = "Label filters, format: 'key1:value1,key2:value2'", required = false) String labels,
|
||||
@ToolParam(description = "Sort field: 'name', 'gmtCreate', 'gmtUpdate', 'status', 'app' (default: gmtCreate)", required = false) String sort,
|
||||
@ToolParam(description = "Sort order: 'asc' (ascending) or 'desc' (descending, default)", required = false) String order,
|
||||
@ToolParam(description = "Page number starting from 0 (default: 0)", required = false) Integer pageIndex,
|
||||
@ToolParam(description = "Items per page: 1-100 recommended (default: 20)", required = false) Integer pageSize,
|
||||
@ToolParam(description = "Include status statistics summary (default: false)", required = false) Boolean includeStats) {
|
||||
try {
|
||||
// Set defaults
|
||||
if (pageSize == null || pageSize <= 0) {
|
||||
pageSize = 20;
|
||||
}
|
||||
if (pageIndex == null) {
|
||||
pageIndex = 0;
|
||||
}
|
||||
if (includeStats == null) {
|
||||
includeStats = false;
|
||||
}
|
||||
|
||||
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");
|
||||
response.append("====================\n\n");
|
||||
|
||||
// Include statistics if requested
|
||||
if (includeStats) {
|
||||
// Get status distribution by calling with different status values
|
||||
long onlineCount = monitorServiceAdapter.getMonitors(null, app, search, (byte) 1, null, null, 0, 1000, labels).getTotalElements();
|
||||
long offlineCount = monitorServiceAdapter.getMonitors(null, app, search, (byte) 2, null, null, 0, 1000, labels).getTotalElements();
|
||||
long unreachableCount = monitorServiceAdapter.getMonitors(null, app, search, (byte) 3, null, null, 0, 1000, labels).getTotalElements();
|
||||
long pausedCount = monitorServiceAdapter.getMonitors(null, app, search, (byte) 0, null, null, 0, 1000, labels).getTotalElements();
|
||||
|
||||
response.append("STATUS OVERVIEW:\n");
|
||||
response.append("- Online: ").append(onlineCount).append("\n");
|
||||
response.append("- Offline: ").append(offlineCount).append("\n");
|
||||
response.append("- Unreachable: ").append(unreachableCount).append("\n");
|
||||
response.append("- Paused: ").append(pausedCount).append("\n");
|
||||
|
||||
long total = onlineCount + offlineCount + unreachableCount + pausedCount;
|
||||
if (total > 0) {
|
||||
double healthPercentage = (onlineCount * 100.0) / total;
|
||||
response.append("- Health Rate: ").append(String.format("%.1f", healthPercentage)).append("%\n");
|
||||
}
|
||||
response.append("\n");
|
||||
}
|
||||
|
||||
response.append("Query Results: ").append(result.getContent().size())
|
||||
.append(" monitors (Total: ").append(result.getTotalElements()).append(")\n");
|
||||
|
||||
if (result.getTotalPages() > 1) {
|
||||
response.append("Page ").append(pageIndex + 1).append(" of ").append(result.getTotalPages()).append("\n");
|
||||
}
|
||||
response.append("\n");
|
||||
|
||||
for (Monitor monitor : result.getContent()) {
|
||||
response.append("ID: ").append(monitor.getId())
|
||||
.append(" | Name: ").append(monitor.getName())
|
||||
.append(" | Type: ").append(monitor.getApp())
|
||||
.append(" | Host: ").append(monitor.getHost())
|
||||
.append(" | Status: ").append(UtilityClass.getStatusText(monitor.getStatus()));
|
||||
|
||||
// Add creation date for better context
|
||||
if (monitor.getGmtCreate() != null) {
|
||||
response.append(" | Created: ").append(monitor.getGmtCreate().toString(), 0, 10);
|
||||
}
|
||||
response.append("\n");
|
||||
}
|
||||
|
||||
if (result.getContent().isEmpty()) {
|
||||
response.append("No monitors found matching the specified criteria.\n");
|
||||
response.append("Try adjusting your filters or search terms.");
|
||||
}
|
||||
|
||||
return response.toString();
|
||||
} catch (Exception e) {
|
||||
return "Error retrieving monitors: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@Tool(name = "add_monitor", description = """
|
||||
Add a new monitoring target to HertzBeat with comprehensive configuration.
|
||||
This tool dynamically handles different parameter requirements for each monitor type.
|
||||
|
||||
This tool creates monitors with proper app-specific parameters.
|
||||
|
||||
*********
|
||||
VERY IMPORTANT:
|
||||
ALWAYS use get_monitor_additional_params to check the additional required parameters for the chosen type before adding a monitor or even mentioning it.
|
||||
Use list_monitor_types tool to see available monitor type names to use here in the app parameter.
|
||||
Use the information obtained from this to query user for parameters.
|
||||
If the User has not given any parameters, ask them to provide the necessary parameters, until all the necessary parameters are provided.
|
||||
**********
|
||||
|
||||
Examples of natural language requests this tool handles:
|
||||
- "Monitor website example.com with HTTPS on port 443"
|
||||
- "Add MySQL monitoring for database server at 192.168.1.10 with user admin"
|
||||
- "Monitor Linux server health on host server.company.com via SSH"
|
||||
- "Set up Redis monitoring on localhost port 6379 with password"
|
||||
|
||||
PARAMETER MAPPING: The tool intelligently maps common parameters:
|
||||
- host: Target server/domain
|
||||
- port: Service port (auto-detected if not specified)
|
||||
- username: Authentication username
|
||||
- password: Authentication password
|
||||
- database: Database name (for DB monitors)
|
||||
- additionalParams: JSON string for app-specific parameters (to be obtained from get_monitor_param_defines)
|
||||
|
||||
ADDITIONAL PARAMETERS EXAMPLES:
|
||||
- Website: {"uri":"/api/health", "ssl":"true", "method":"POST"}
|
||||
- Linux: {"privateKey":"ssh-key-content", "script":"custom-script"}
|
||||
- Database: {"url":"jdbc:mysql://custom", "timeout":"10000"}
|
||||
""")
|
||||
public String addMonitor(
|
||||
@ToolParam(description = "Monitor name (required)", required = true) String name,
|
||||
@ToolParam(description = "Monitor type: website, mysql, postgresql, redis, linux, windows, etc.", required = true) String app,
|
||||
@ToolParam(description = "Target host: IP address or domain name", required = true) String host,
|
||||
@ToolParam(description = "Target port (optional, auto-detected if not specified)", required = false) Integer port,
|
||||
@ToolParam(description = "Collection interval in seconds (default: 600)", required = false) Integer intervals,
|
||||
@ToolParam(description = "Username for authentication (optional)", required = false) String username,
|
||||
@ToolParam(description = "Password for authentication (optional)", required = false) String password,
|
||||
@ToolParam(description = "Database name (for database monitors)", required = false) String database,
|
||||
@ToolParam(description = "Additional app-specific parameters as JSON: {\"uri\":\"/api\", \"ssl\":\"true\", \"method\":\"POST\"}", required = false) String additionalParams,
|
||||
@ToolParam(description = "Monitor description (optional)", required = false) String description) {
|
||||
|
||||
try {
|
||||
log.info("Adding monitor: name={}, app={}, host={}", name, app, host);
|
||||
|
||||
// Validate required parameters
|
||||
if (name == null || name.trim().isEmpty()) {
|
||||
return "Error: Monitor name is required";
|
||||
}
|
||||
if (app == null || app.trim().isEmpty()) {
|
||||
return "Error: Monitor type/application is required";
|
||||
}
|
||||
if (host == null || host.trim().isEmpty()) {
|
||||
return "Error: Host is required";
|
||||
}
|
||||
|
||||
// Set defaults
|
||||
if (intervals == null || intervals < 10) {
|
||||
intervals = 600;
|
||||
}
|
||||
|
||||
// Create Monitor entity
|
||||
Monitor monitor = Monitor.builder()
|
||||
.name(name.trim())
|
||||
.app(app.toLowerCase().trim())
|
||||
.host(host.trim())
|
||||
.intervals(intervals)
|
||||
.status((byte) 1)
|
||||
.type((byte) 0)
|
||||
.description(description != null ? description.trim() : "")
|
||||
.build();
|
||||
|
||||
List<Param> params = createBasicParams(host, port, username, password, database, additionalParams);
|
||||
|
||||
// Validate that all required parameters for this monitor type are provided
|
||||
try {
|
||||
List<ParamDefine> requiredParams = monitorServiceAdapter.getMonitorParamDefines(app);
|
||||
log.info("Checking required parameters for monitor type '{}': {}", app, requiredParams);
|
||||
List<String> missingParams = new ArrayList<>();
|
||||
|
||||
for (ParamDefine paramDefine : requiredParams) {
|
||||
if (paramDefine.isRequired()) {
|
||||
String fieldName = paramDefine.getField();
|
||||
boolean hasParam = params.stream()
|
||||
.anyMatch(param -> fieldName.equals(param.getField()));
|
||||
if (!hasParam) {
|
||||
missingParams.add(fieldName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!missingParams.isEmpty()) {
|
||||
return String.format("Error: Missing required parameters for monitor type '%s': %s. "
|
||||
+ "Use get_monitor_additional_params tool to see all required parameters.",
|
||||
app, String.join(", ", missingParams));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Could not validate required parameters for monitor type '{}': {}", app, e.getMessage());
|
||||
}
|
||||
|
||||
// Call 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)",
|
||||
app.toUpperCase(), name, monitorId, host, intervals);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to add monitor '{}': {}", name, e.getMessage(), e);
|
||||
return "Error adding monitor '" + name + "': " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create basic parameter list from user inputs
|
||||
*/
|
||||
private List<Param> createBasicParams(String host, Integer port, String username,
|
||||
String password, String database, String additionalParams) {
|
||||
List<Param> params = new ArrayList<>();
|
||||
|
||||
// Add host (always required)
|
||||
params.add(Param.builder().field("host").paramValue(host.trim()).type((byte) 1).build());
|
||||
|
||||
// Add optional common parameters
|
||||
if (port != null) {
|
||||
params.add(Param.builder().field("port").paramValue(port.toString()).type((byte) 0).build());
|
||||
}
|
||||
if (username != null && !username.trim().isEmpty()) {
|
||||
params.add(Param.builder().field("username").paramValue(username.trim()).type((byte) 1).build());
|
||||
}
|
||||
if (password != null && !password.trim().isEmpty()) {
|
||||
params.add(Param.builder().field("password").paramValue(password.trim()).type((byte) 2).build());
|
||||
}
|
||||
if (database != null && !database.trim().isEmpty()) {
|
||||
params.add(Param.builder().field("database").paramValue(database.trim()).type((byte) 1).build());
|
||||
}
|
||||
|
||||
// Parse additional parameters if provided
|
||||
if (additionalParams != null && !additionalParams.trim().isEmpty()) {
|
||||
try {
|
||||
String cleaned = additionalParams.trim().replaceAll("[{}]", "");
|
||||
String[] pairs = cleaned.split(",");
|
||||
for (String pair : pairs) {
|
||||
String[] keyValue = pair.split(":");
|
||||
if (keyValue.length == 2) {
|
||||
String key = keyValue[0].trim().replaceAll("\"", "");
|
||||
String value = keyValue[1].trim().replaceAll("\"", "");
|
||||
params.add(Param.builder().field(key).paramValue(value).type((byte) 1).build());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to parse additionalParams: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Tool(name = "list_monitor_types", description = """
|
||||
List all available monitor types that can be added to HertzBeat.
|
||||
This tool shows all supported monitor types with their display names.
|
||||
Use this to see what types of monitors you can create with the add_monitor tool.
|
||||
""")
|
||||
public String listMonitorTypes(
|
||||
@ToolParam(description = "Language code for localized names (en-US, zh-CN, etc.). Default: en-US", required = false) String language) {
|
||||
|
||||
try {
|
||||
log.info("Listing available monitor types for language: {}", language);
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current subject in list_monitor_types tool: {}", subjectSum);
|
||||
|
||||
// Set default language if not provided
|
||||
if (language == null || language.trim().isEmpty()) {
|
||||
language = "en-US";
|
||||
}
|
||||
|
||||
// Get available monitor types from adapter
|
||||
Map<String, String> monitorTypes = monitorServiceAdapter.getAvailableMonitorTypes(language);
|
||||
|
||||
if (monitorTypes == null || monitorTypes.isEmpty()) {
|
||||
return "No monitor types are currently available.";
|
||||
}
|
||||
|
||||
// Format the response as a nice list
|
||||
StringBuilder response = new StringBuilder();
|
||||
response.append("Available Monitor Types (Total: ").append(monitorTypes.size()).append("):\n\n");
|
||||
|
||||
// Sort monitor types alphabetically by key
|
||||
List<Map.Entry<String, String>> sortedTypes = monitorTypes.entrySet().stream()
|
||||
.sorted(Map.Entry.comparingByKey())
|
||||
.toList();
|
||||
|
||||
for (Map.Entry<String, String> entry : sortedTypes) {
|
||||
String typeKey = entry.getKey();
|
||||
String displayName = entry.getValue();
|
||||
response.append("• ").append(typeKey)
|
||||
.append(" - ").append(displayName)
|
||||
.append("\n");
|
||||
}
|
||||
|
||||
response.append("\nTo add a monitor, use the add_monitor tool with one of these types as the 'app' parameter.");
|
||||
|
||||
log.info("Successfully listed {} monitor types", monitorTypes);
|
||||
return response.toString();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to list monitor types: {}", e.getMessage(), e);
|
||||
return "Error retrieving monitor types: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Tool(name = "get_monitor_additional_params", description = """
|
||||
Get the parameter definitions required for a specific monitor type.
|
||||
This tool shows what parameters are needed when adding a monitor of the specified type,
|
||||
ALWAYS use this before adding a monitor to understand what parameters the user needs to provide.
|
||||
Use the app parameter to specify the monitor type/application name (e.g., 'linux', 'mysql', 'redis') this can be obtained from the list_monitor_types tool.
|
||||
""")
|
||||
public String getMonitorAdditionalParams(
|
||||
@ToolParam(description = "Monitor type/application name (e.g., 'linux', 'mysql', 'redis')", required = true) String app) {
|
||||
|
||||
try {
|
||||
log.info("Getting parameter definitions for monitor type: {}", app);
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current subject in get_monitor_param_defines tool: {}", subjectSum);
|
||||
|
||||
// Validate required parameter
|
||||
if (app == null || app.trim().isEmpty()) {
|
||||
return "Error: Monitor type/application parameter is required";
|
||||
}
|
||||
|
||||
// Get parameter definitions from adapter
|
||||
List<ParamDefine> paramDefines = monitorServiceAdapter.getMonitorParamDefines(app);
|
||||
|
||||
if (paramDefines == null || paramDefines.isEmpty()) {
|
||||
return String.format("No parameter definitions found for monitor type '%s'. "
|
||||
+ "This monitor type may not exist or may not require additional parameters.", app);
|
||||
}
|
||||
|
||||
// Format the response
|
||||
StringBuilder response = new StringBuilder();
|
||||
response.append(String.format("Parameter Definitions for Monitor Type '%s' (Total: %d):\n\n",
|
||||
app, paramDefines.size()));
|
||||
|
||||
for (ParamDefine paramDefine : paramDefines) {
|
||||
response.append("• Field: ").append(paramDefine.getField()).append("\n");
|
||||
|
||||
// Add display name if available
|
||||
if (paramDefine.getName() != null && !paramDefine.getName().toString().trim().isEmpty()) {
|
||||
response.append(" Name: ").append(paramDefine.getName()).append("\n");
|
||||
}
|
||||
|
||||
// Add type
|
||||
if (paramDefine.getType() != null && !paramDefine.getType().trim().isEmpty()) {
|
||||
response.append(" Type: ").append(paramDefine.getType()).append("\n");
|
||||
}
|
||||
|
||||
// Add required status
|
||||
response.append(" Required: ").append(paramDefine.isRequired() ? "Yes" : "No").append("\n");
|
||||
|
||||
// Add default value if present
|
||||
if (paramDefine.getDefaultValue() != null && !paramDefine.getDefaultValue().trim().isEmpty()) {
|
||||
response.append(" Default: ").append(paramDefine.getDefaultValue()).append("\n");
|
||||
}
|
||||
|
||||
// Add validation range if present
|
||||
if (paramDefine.getRange() != null && !paramDefine.getRange().trim().isEmpty()) {
|
||||
response.append(" Range: ").append(paramDefine.getRange()).append("\n");
|
||||
}
|
||||
|
||||
// Add limit if present
|
||||
if (paramDefine.getLimit() != null) {
|
||||
response.append(" Limit: ").append(paramDefine.getLimit()).append("\n");
|
||||
}
|
||||
|
||||
// Add placeholder text if present
|
||||
if (paramDefine.getPlaceholder() != null && !paramDefine.getPlaceholder().trim().isEmpty()) {
|
||||
response.append(" Placeholder: ").append(paramDefine.getPlaceholder()).append("\n");
|
||||
}
|
||||
|
||||
response.append("\n");
|
||||
}
|
||||
|
||||
response.append("To add a monitor of this type, use the add_monitor tool with these parameters.\n");
|
||||
response.append(String.format("Example: add_monitor(name='my-monitor', app='%s', host='your-host', ...)", app));
|
||||
|
||||
log.info("Successfully retrieved {} parameter definitions for monitor type: {}", paramDefines.size(), app);
|
||||
return response.toString();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to get parameter definitions for monitor type '{}': {}", app, e.getMessage(), e);
|
||||
return "Error retrieving parameter definitions for monitor type '" + app + "': " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
+691
@@ -0,0 +1,691 @@
|
||||
/*
|
||||
* 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.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;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Utility class providing helper methods for field expression validation, parsing,
|
||||
* hierarchy management, timestamp formatting and metric/status conversions.
|
||||
* This class contains stateless utility methods used across the application for
|
||||
* common operations and validations.
|
||||
*/
|
||||
@Slf4j
|
||||
@lombok.experimental.UtilityClass
|
||||
public class UtilityClass {
|
||||
|
||||
/**
|
||||
* Validates the syntax of field conditions expression
|
||||
* @param fieldConditions Field conditions string to validate
|
||||
* @return "VALID" if syntax is correct, error message otherwise
|
||||
*/
|
||||
|
||||
public String validateExpressionSyntax(String fieldConditions) {
|
||||
try {
|
||||
log.debug("Validating expression syntax: {}", fieldConditions);
|
||||
|
||||
// Check for basic syntax requirements
|
||||
if (fieldConditions == null || fieldConditions.trim().isEmpty()) {
|
||||
return "Error: Field conditions cannot be empty";
|
||||
}
|
||||
|
||||
// Check for balanced parentheses
|
||||
if (!hasBalancedParentheses(fieldConditions)) {
|
||||
return "Error: Unbalanced parentheses in field conditions. Please check your expression syntax.";
|
||||
}
|
||||
|
||||
// Validate operators used in the expression
|
||||
String operatorValidation = validateOperators(fieldConditions);
|
||||
if (!operatorValidation.equals("VALID")) {
|
||||
return operatorValidation;
|
||||
}
|
||||
|
||||
// Validate logical connectors
|
||||
String logicalValidation = validateLogicalConnectors(fieldConditions);
|
||||
if (!logicalValidation.equals("VALID")) {
|
||||
return logicalValidation;
|
||||
}
|
||||
|
||||
// Validate function syntax (equals, contains, etc.)
|
||||
String functionValidation = validateFunctions(fieldConditions);
|
||||
if (!functionValidation.equals("VALID")) {
|
||||
return functionValidation;
|
||||
}
|
||||
|
||||
log.debug("Expression syntax validation passed for: {}", fieldConditions);
|
||||
return "VALID";
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error during expression syntax validation: {}", e.getMessage(), e);
|
||||
return String.format("Error: Unable to validate expression syntax: %s", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if parentheses are balanced in the expression
|
||||
*/
|
||||
public boolean hasBalancedParentheses(String expression) {
|
||||
int count = 0;
|
||||
for (char c : expression.toCharArray()) {
|
||||
if (c == '(') {
|
||||
count++;
|
||||
} else if (c == ')') {
|
||||
count--;
|
||||
if (count < 0) {
|
||||
return false; // More closing than opening
|
||||
}
|
||||
}
|
||||
}
|
||||
return count == 0; // Should be perfectly balanced
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that only supported operators are used
|
||||
*/
|
||||
public String validateOperators(String fieldConditions) {
|
||||
// Define supported operators for different field types
|
||||
String[] numericOperators = {">", "<", ">=", "<=", "==", "!=", "exists()", "!exists()"};
|
||||
String[] stringOperators = {"equals(", "contains(", "matches(", "exists()", "!equals(", "!contains(", "!matches(", "!exists()"};
|
||||
String[] logicalOperators = {" and ", " or "};
|
||||
|
||||
// Remove quotes and function calls temporarily for operator checking
|
||||
String tempExpression = fieldConditions
|
||||
.replaceAll("\"[^\"]*\"", "VALUE") // Remove quoted strings
|
||||
.replaceAll("'[^']*'", "VALUE") // Remove single quoted strings
|
||||
.replaceAll("\\w+\\([^)]*\\)", "FUNCTION"); // Remove function calls
|
||||
|
||||
// Check for invalid operators (common mistakes)
|
||||
String[] invalidOperators = {"&&", "||", "AND", "OR", "=", "!="};
|
||||
for (String invalidOp : invalidOperators) {
|
||||
if (tempExpression.contains(invalidOp)) {
|
||||
if (invalidOp.equals("&&") || invalidOp.equals("||")) {
|
||||
return String.format("Error: Use 'and'/'or' instead of '%s' for logical operations", invalidOp);
|
||||
}
|
||||
if (invalidOp.equals("AND") || invalidOp.equals("OR")) {
|
||||
return String.format("Error: Use lowercase '%s' for logical operations", invalidOp.toLowerCase());
|
||||
}
|
||||
if (invalidOp.equals("=")) {
|
||||
return "Error: Use '==' for equality comparison, not '='";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for unsupported special characters that might indicate syntax errors
|
||||
if (tempExpression.matches(".*[#$%^&*+\\[\\]{}|\\\\;:'\"`~].*")) {
|
||||
return "Error: Expression contains unsupported special characters. Use only supported operators and functions.";
|
||||
}
|
||||
|
||||
return "VALID";
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates logical connectors syntax
|
||||
*/
|
||||
public String validateLogicalConnectors(String fieldConditions) {
|
||||
// Check for proper spacing around logical operators
|
||||
if (fieldConditions.matches(".*(\\S(and|or)\\S).*")) {
|
||||
return "Error: Logical operators 'and'/'or' must be surrounded by spaces";
|
||||
}
|
||||
|
||||
// Check for consecutive logical operators
|
||||
if (fieldConditions.matches(".*(and\\s+and|or\\s+or|and\\s+or\\s+and|or\\s+and\\s+or).*")) {
|
||||
return "Error: Consecutive logical operators found. Use parentheses to group conditions properly.";
|
||||
}
|
||||
|
||||
// Check for logical operators at the beginning or end
|
||||
String trimmed = fieldConditions.trim();
|
||||
if (trimmed.startsWith("and ") || trimmed.startsWith("or ")
|
||||
|| trimmed.endsWith(" and") || trimmed.endsWith(" or")) {
|
||||
return "Error: Expression cannot start or end with logical operators 'and'/'or'";
|
||||
}
|
||||
|
||||
return "VALID";
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates function syntax (equals, contains, matches, etc.)
|
||||
*/
|
||||
public String validateFunctions(String fieldConditions) {
|
||||
// Check for properly formed function calls
|
||||
String[] supportedFunctions = {"equals", "contains", "matches", "exists", "!equals", "!contains", "!matches", "!exists"};
|
||||
|
||||
// Find all function-like patterns
|
||||
java.util.regex.Pattern functionPattern = java.util.regex.Pattern.compile("(!?\\w+)\\s*\\(([^)]*)\\)");
|
||||
java.util.regex.Matcher matcher = functionPattern.matcher(fieldConditions);
|
||||
|
||||
while (matcher.find()) {
|
||||
String functionName = matcher.group(1);
|
||||
String functionArgs = matcher.group(2);
|
||||
|
||||
// Check if function is supported
|
||||
boolean isSupported = false;
|
||||
for (String supportedFunc : supportedFunctions) {
|
||||
if (functionName.equals(supportedFunc)) {
|
||||
isSupported = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSupported) {
|
||||
return String.format("Error: Unsupported function '%s'. Supported functions: %s",
|
||||
functionName, String.join(", ", supportedFunctions));
|
||||
}
|
||||
|
||||
// Validate function arguments
|
||||
if (functionName.equals("exists") || functionName.equals("!exists")) {
|
||||
// exists() should have one parameter or no parameters
|
||||
String[] args = functionArgs.trim().isEmpty() ? new String[0] : functionArgs.split(",");
|
||||
if (args.length > 1) {
|
||||
return String.format("Error: Function '%s' should have at most one parameter", functionName);
|
||||
}
|
||||
} else {
|
||||
// Other functions should have exactly 2 parameters
|
||||
String[] args = functionArgs.split(",");
|
||||
if (args.length != 2) {
|
||||
return String.format("Error: Function '%s' requires exactly 2 parameters (field, value)", functionName);
|
||||
}
|
||||
|
||||
// Check that parameters are not empty
|
||||
for (String arg : args) {
|
||||
if (arg.trim().isEmpty()) {
|
||||
return String.format("Error: Function '%s' has empty parameter", functionName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "VALID";
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to 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, ..."
|
||||
*/
|
||||
public Map<String, String> parseKeyValuePairs(String input) {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
if (input == null || input.trim().isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
String[] pairs = input.split(",");
|
||||
for (String pair : pairs) {
|
||||
String[] keyValue = pair.split(":");
|
||||
if (keyValue.length == 2) {
|
||||
result.put(keyValue[0].trim(), keyValue[1].trim());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Recursively searches for a metric in the hierarchy
|
||||
*/
|
||||
public Hierarchy findMetricInHierarchy(List<Hierarchy> hierarchies, String metricName) {
|
||||
for (Hierarchy hierarchy : hierarchies) {
|
||||
// Check if this is the metric we're looking for
|
||||
if (metricName.equals(hierarchy.getValue())) {
|
||||
// Verify it has field children (leaf nodes)
|
||||
if (hierarchy.getChildren() != null && !hierarchy.getChildren().isEmpty()) {
|
||||
boolean hasLeafChildren = hierarchy.getChildren().stream()
|
||||
.anyMatch(child -> child.getIsLeaf() != null && child.getIsLeaf());
|
||||
if (hasLeafChildren) {
|
||||
return hierarchy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively search in children
|
||||
if (hierarchy.getChildren() != null) {
|
||||
Hierarchy found = findMetricInHierarchy(hierarchy.getChildren(), metricName);
|
||||
if (found != null) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a field is valid for the given metric
|
||||
*/
|
||||
public boolean isFieldValidForMetric(Hierarchy metricHierarchy, String fieldName) {
|
||||
if (metricHierarchy.getChildren() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (Hierarchy child : metricHierarchy.getChildren()) {
|
||||
if (child.getIsLeaf() != null && child.getIsLeaf() && fieldName.equals(child.getValue())) {
|
||||
return true;
|
||||
}
|
||||
// Also check nested children
|
||||
if (child.getChildren() != null && isFieldValidForMetric(child, fieldName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts field names from field conditions string
|
||||
* Handles simple cases like "field > 80", "equals(field, 'value')", complex expressions
|
||||
*/
|
||||
public List<String> extractFieldNamesFromConditions(String fieldConditions) {
|
||||
List<String> fieldNames = new ArrayList<>();
|
||||
|
||||
// Split by logical operators (and, or) and parentheses, but preserve the field names
|
||||
// This is a simple implementation - could be enhanced with a proper parser
|
||||
String[] parts = fieldConditions.split("\\s+(and|or|&&|\\|\\|)\\s+|[()]+");
|
||||
|
||||
for (String part : parts) {
|
||||
part = part.trim();
|
||||
if (part.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle equals() function: equals(fieldName, "value")
|
||||
if (part.contains("equals(")) {
|
||||
String fieldName = extractFieldFromEquals(part);
|
||||
if (fieldName != null && !fieldNames.contains(fieldName)) {
|
||||
fieldNames.add(fieldName);
|
||||
}
|
||||
} else {
|
||||
// Handle simple comparisons: fieldName > value, fieldName <= value
|
||||
String fieldName = extractFieldFromComparison(part);
|
||||
if (fieldName != null && !fieldNames.contains(fieldName)) {
|
||||
fieldNames.add(fieldName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fieldNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts field name from equals() function
|
||||
*/
|
||||
public String extractFieldFromEquals(String condition) {
|
||||
// Pattern: equals(fieldName, "value") or equals(fieldName, value)
|
||||
int startParen = condition.indexOf('(');
|
||||
int comma = condition.indexOf(',');
|
||||
|
||||
if (startParen != -1 && comma != -1 && comma > startParen) {
|
||||
String fieldName = condition.substring(startParen + 1, comma).trim();
|
||||
// Remove quotes if present
|
||||
if (fieldName.startsWith("\"") && fieldName.endsWith("\"")) {
|
||||
fieldName = fieldName.substring(1, fieldName.length() - 1);
|
||||
}
|
||||
return fieldName;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts field name from comparison operation
|
||||
*/
|
||||
public String extractFieldFromComparison(String condition) {
|
||||
// Pattern: fieldName operator value
|
||||
// Updated to include all supported operators
|
||||
String[] operators = {" >= ", " <= ", " > ", " < ", " == ", " != "};
|
||||
|
||||
for (String operator : operators) {
|
||||
if (condition.contains(operator)) {
|
||||
String fieldName = condition.split(operator)[0].trim();
|
||||
// Basic validation - field names shouldn't contain quotes or special chars
|
||||
if (fieldName.matches("[a-zA-Z_][a-zA-Z0-9_]*")) {
|
||||
return fieldName;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to format hierarchy structure as JSON recursively
|
||||
*/
|
||||
public ObjectNode formatHierarchyAsJson(ObjectMapper mapper, Hierarchy hierarchy) {
|
||||
ObjectNode node = mapper.createObjectNode();
|
||||
|
||||
node.put("value", hierarchy.getValue());
|
||||
node.put("label", hierarchy.getLabel());
|
||||
|
||||
if (hierarchy.getIsLeaf() != null && hierarchy.getIsLeaf()) {
|
||||
// Leaf node - actual metric field parameter
|
||||
node.put("type", "field_parameter");
|
||||
|
||||
if (hierarchy.getType() != null) {
|
||||
node.put("dataType", hierarchy.getType() == 0 ? "numeric" : "string");
|
||||
}
|
||||
if (hierarchy.getUnit() != null && !hierarchy.getUnit().trim().isEmpty()) {
|
||||
node.put("unit", hierarchy.getUnit());
|
||||
}
|
||||
node.put("description", "Available field parameter for alert conditions");
|
||||
} else {
|
||||
// Category, app, or metric node
|
||||
// Determine node type based on children
|
||||
boolean hasLeafChildren = hierarchy.getChildren().stream()
|
||||
.anyMatch(child -> child.getIsLeaf() != null && child.getIsLeaf());
|
||||
|
||||
if (hasLeafChildren) {
|
||||
node.put("type", "metric");
|
||||
node.put("description", "Metric with available field parameters");
|
||||
} else {
|
||||
node.put("type", "app");
|
||||
node.put("description", "Application with available metrics");
|
||||
}
|
||||
|
||||
if (hierarchy.getChildren() != null && !hierarchy.getChildren().isEmpty()) {
|
||||
ArrayNode childrenArray = mapper.createArrayNode();
|
||||
for (Hierarchy child : hierarchy.getChildren()) {
|
||||
childrenArray.add(formatHierarchyAsJson(mapper, child));
|
||||
}
|
||||
node.set("children", childrenArray);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format timestamp to readable format
|
||||
*/
|
||||
public String formatTimestamp(Long timestamp) {
|
||||
if (timestamp == null) {
|
||||
return "N/A";
|
||||
}
|
||||
LocalDateTime dateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault());
|
||||
return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @return Human-readable status text
|
||||
*/
|
||||
public String getStatusText(Byte status) {
|
||||
if (status == null) {
|
||||
return "Unknown";
|
||||
}
|
||||
return switch (status) {
|
||||
case 0 -> "Paused";
|
||||
case 1 -> "Online";
|
||||
case 2 -> "Offline";
|
||||
case 3 -> "Unreachable";
|
||||
default -> "Unknown (" + status + ")";
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param expression The alert expression containing __instance__ conditions
|
||||
* @return List of existing monitor IDs
|
||||
*/
|
||||
public List<String> extractExistingMonitorIds(String expression) {
|
||||
List<String> monitorIds = new ArrayList<>();
|
||||
String pattern = "equals\\(__instance__,\\s*\"([^\"]+)\"\\)";
|
||||
java.util.regex.Pattern regex = java.util.regex.Pattern.compile(pattern);
|
||||
java.util.regex.Matcher matcher = regex.matcher(expression);
|
||||
|
||||
while (matcher.find()) {
|
||||
monitorIds.add(matcher.group(1));
|
||||
}
|
||||
|
||||
return monitorIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace existing __instance__ conditions in the expression with new ones
|
||||
* @param expression The current expression
|
||||
* @param newMonitorCondition The new monitor condition to replace with
|
||||
* @return Updated expression
|
||||
*/
|
||||
public String replaceInstanceConditions(String expression, String newMonitorCondition) {
|
||||
// More precise pattern to match complete __instance__ condition blocks without cutting field parameters
|
||||
// Pattern matches either:
|
||||
// 1. Single: equals(__instance__, "id")
|
||||
// 2. Multiple: (equals(__instance__, "id1") or equals(__instance__, "id2") or ...)
|
||||
|
||||
// First try to match grouped conditions: (equals(__instance__, "id1") or equals(__instance__, "id2"))
|
||||
String groupedPattern = "\\(\\s*equals\\(__instance__,\\s*\"[^\"]+\"\\)(?:\\s+or\\s+equals\\(__instance__,\\s*\"[^\"]+\"\\))*\\s*\\)";
|
||||
if (expression.matches(".*" + groupedPattern + ".*")) {
|
||||
return expression.replaceFirst(groupedPattern, newMonitorCondition);
|
||||
}
|
||||
|
||||
// Then try single condition: equals(__instance__, "id")
|
||||
String singlePattern = "equals\\(__instance__,\\s*\"[^\"]+\"\\)";
|
||||
if (expression.matches(".*" + singlePattern + ".*")) {
|
||||
return expression.replaceFirst(singlePattern, newMonitorCondition);
|
||||
}
|
||||
|
||||
// If no match found, return original expression
|
||||
return expression;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
~ contributor license agreements. See the NOTICE file distributed with
|
||||
~ this work for additional information regarding copyright ownership.
|
||||
~ The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
~ (the "License"); you may not use this file except in compliance with
|
||||
~ the License. You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.apache.hertzbeat</groupId>
|
||||
<artifactId>hertzbeat</artifactId>
|
||||
<version>2.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>hertzbeat-ai</artifactId>
|
||||
<version>${hertzbeat.version}</version>
|
||||
<properties>
|
||||
<spring-ai.version>2.0.0</spring-ai.version>
|
||||
<java.version>25</java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.platform</groupId>
|
||||
<artifactId>junit-platform-launcher</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-openai</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-client-chat</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.hertzbeat</groupId>
|
||||
<artifactId>hertzbeat-common-spring</artifactId>
|
||||
</dependency>
|
||||
<!-- common -->
|
||||
<dependency>
|
||||
<groupId>org.apache.hertzbeat</groupId>
|
||||
<artifactId>hertzbeat-base</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.hertzbeat</groupId>
|
||||
<artifactId>hertzbeat-alerter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.hertzbeat</groupId>
|
||||
<artifactId>hertzbeat-manager</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.usthe.sureness</groupId>
|
||||
<artifactId>spring-boot3-starter-sureness</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-bom</artifactId>
|
||||
<version>${spring-ai.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>true</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -1,151 +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.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");
|
||||
}
|
||||
}
|
||||
|
||||
// Create Chat Options with baseUrl and apiKey
|
||||
OpenAiChatOptions openAiChatOptions = OpenAiChatOptions.builder()
|
||||
.baseUrl(modelProviderConfig.getBaseUrl())
|
||||
.apiKey(modelProviderConfig.getApiKey())
|
||||
.model(modelProviderConfig.getModel())
|
||||
.temperature(0.3)
|
||||
.build();
|
||||
|
||||
// Create Chat Model
|
||||
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
|
||||
.options(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,39 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.config;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.support.ResourceBundleMessageSource;
|
||||
|
||||
/**
|
||||
* Configuration for SOP internationalization.
|
||||
*/
|
||||
@Configuration
|
||||
public class SopI18nConfig {
|
||||
|
||||
@Bean("sopMessageSource")
|
||||
public MessageSource sopMessageSource() {
|
||||
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
|
||||
messageSource.setBasename("i18n/messages");
|
||||
messageSource.setDefaultEncoding("UTF-8");
|
||||
messageSource.setUseCodeAsDefaultMessage(true);
|
||||
return messageSource;
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.controller;
|
||||
|
||||
import com.usthe.sureness.subject.SubjectSum;
|
||||
import com.usthe.sureness.util.SurenessContextHolder;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.config.McpContextHolder;
|
||||
import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext;
|
||||
import org.apache.hertzbeat.ai.pojo.dto.ChatResponseChunk;
|
||||
import org.apache.hertzbeat.ai.pojo.dto.SecurityData;
|
||||
import org.apache.hertzbeat.ai.service.ConversationService;
|
||||
import org.apache.hertzbeat.common.entity.ai.ChatConversation;
|
||||
import org.apache.hertzbeat.common.entity.dto.Message;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.codec.ServerSentEvent;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.List;
|
||||
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
|
||||
import static org.springframework.http.MediaType.TEXT_EVENT_STREAM_VALUE;
|
||||
|
||||
/**
|
||||
* Controller class for handling AI chat requests and conversation management.
|
||||
*/
|
||||
@Slf4j
|
||||
@Tag(name = "AI Chat API")
|
||||
@RestController
|
||||
@RequestMapping(path = "/api/chat", produces = {APPLICATION_JSON_VALUE})
|
||||
public class ChatController {
|
||||
|
||||
private final ConversationService conversationService;
|
||||
|
||||
@Autowired
|
||||
public ChatController(ConversationService conversationService) {
|
||||
this.conversationService = conversationService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message and get a streaming response with conversation tracking
|
||||
*
|
||||
* @param context The chat request context containing message and optional conversationId
|
||||
* @return Flux of ServerSentEvent for streaming response
|
||||
*/
|
||||
@PostMapping(value = "/stream", produces = TEXT_EVENT_STREAM_VALUE)
|
||||
@Operation(summary = "Send a chat message with streaming response", description = "Send a message to AI and get a streaming response with conversation tracking")
|
||||
public Flux<ServerSentEvent<ChatResponseChunk>> streamChat(@Valid @RequestBody ChatRequestContext context) {
|
||||
try {
|
||||
// Validate message is not empty
|
||||
SubjectSum subject = SurenessContextHolder.getBindSubject();
|
||||
McpContextHolder.setSubject(subject);
|
||||
if (context.getMessage() == null || context.getMessage().trim().isEmpty()) {
|
||||
ChatResponseChunk errorResponse = ChatResponseChunk.builder()
|
||||
.conversationId(context.getConversationId())
|
||||
.response("Error: Message cannot be empty")
|
||||
.build();
|
||||
return Flux.just(ServerSentEvent.builder(errorResponse)
|
||||
.event("error")
|
||||
.build());
|
||||
}
|
||||
|
||||
log.info("Received streaming chat request for conversation: {}", context.getConversationId());
|
||||
return conversationService.streamChat(context.getMessage(), context.getConversationId());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error in stream chat endpoint: ", e);
|
||||
ChatResponseChunk errorResponse = ChatResponseChunk.builder()
|
||||
.conversationId(context.getConversationId())
|
||||
.response("An error occurred: " + e.getMessage())
|
||||
.build();
|
||||
return Flux.just(ServerSentEvent.builder(errorResponse)
|
||||
.event("error")
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new conversation
|
||||
*
|
||||
* @return Created conversation details
|
||||
*/
|
||||
@PostMapping(path = "/conversations")
|
||||
@Operation(summary = "Create a new conversation", description = "Create a new conversation")
|
||||
public ResponseEntity<Message<ChatConversation>> createConversation() {
|
||||
ChatConversation conversation = conversationService.createConversation();
|
||||
return ResponseEntity.ok(Message.success(conversation));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all conversations
|
||||
*
|
||||
* @return List of all conversations
|
||||
*/
|
||||
@GetMapping(path = "/conversations")
|
||||
@Operation(summary = "List all conversations", description = "Get a list of all conversations")
|
||||
public ResponseEntity<Message<List<ChatConversation>>> listConversations() {
|
||||
List<ChatConversation> conversations = conversationService.getAllConversations();
|
||||
return ResponseEntity.ok(Message.success(conversations));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get conversation history
|
||||
*
|
||||
* @param conversationId The conversation ID
|
||||
* @return Conversation details with message history
|
||||
*/
|
||||
@GetMapping(path = "/conversations/{conversationId}")
|
||||
@Operation(summary = "Get conversation history", description = "Get detailed information and message history for a specific conversation")
|
||||
public ResponseEntity<Message<ChatConversation>> getConversation(
|
||||
@Parameter(description = "Conversation ID", example = "12345678") @PathVariable(value = "conversationId") Long conversationId) {
|
||||
ChatConversation conversation = conversationService.getConversation(conversationId);
|
||||
return ResponseEntity.ok(Message.success(conversation));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a conversation
|
||||
*
|
||||
* @param conversationId The conversation ID to delete
|
||||
* @return Success or error message
|
||||
*/
|
||||
@DeleteMapping(path = "/conversations/{conversationId}")
|
||||
@Operation(summary = "Delete conversation", description = "Delete a specific conversation and all its messages")
|
||||
public ResponseEntity<Message<Void>> deleteConversation(
|
||||
@Parameter(description = "Conversation ID", example = "2345678") @PathVariable("conversationId") Long conversationId) {
|
||||
conversationService.deleteConversation(conversationId);
|
||||
return ResponseEntity.ok(Message.success());
|
||||
}
|
||||
|
||||
/**
|
||||
* Save data submitted by secure form
|
||||
* @param securityData security data
|
||||
* @return save result
|
||||
*/
|
||||
@PostMapping(path = "/security")
|
||||
@Operation(summary = "save security data", description = "Save security data")
|
||||
public ResponseEntity<Message<Boolean>> commitSecurityData(@Valid @RequestBody SecurityData securityData) {
|
||||
return ResponseEntity.ok(Message.success(conversationService.saveSecurityData(securityData)));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.sop.engine.SopEngine;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopDefinition;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopResult;
|
||||
import org.apache.hertzbeat.ai.sop.registry.SkillRegistry;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
/**
|
||||
* Controller for AI SOP (Standard Operating Procedure) execution.
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/ai/sop")
|
||||
@Tag(name = "AI SOP", description = "AI Standard Operating Procedure Management")
|
||||
public class SopController {
|
||||
|
||||
private final SkillRegistry skillRegistry;
|
||||
private final SopEngine sopEngine;
|
||||
|
||||
@Autowired
|
||||
public SopController(SkillRegistry skillRegistry, SopEngine sopEngine) {
|
||||
this.skillRegistry = skillRegistry;
|
||||
this.sopEngine = sopEngine;
|
||||
}
|
||||
|
||||
/**
|
||||
* List all available SOP skills.
|
||||
*/
|
||||
@GetMapping("/skills")
|
||||
@Operation(summary = "List available SOP skills",
|
||||
description = "Get all registered SOP skill definitions")
|
||||
public ResponseEntity<List<Map<String, String>>> listSkills() {
|
||||
List<Map<String, String>> skills = skillRegistry.getAllSkills().stream()
|
||||
.map(skill -> {
|
||||
Map<String, String> info = new HashMap<>();
|
||||
info.put("name", skill.getName());
|
||||
info.put("description", skill.getDescription());
|
||||
info.put("version", skill.getVersion());
|
||||
return info;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
return ResponseEntity.ok(skills);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get details of a specific SOP skill.
|
||||
*/
|
||||
@GetMapping("/skills/{skillName}")
|
||||
@Operation(summary = "Get SOP skill details",
|
||||
description = "Get detailed definition of a specific SOP skill")
|
||||
public ResponseEntity<SopDefinition> getSkillDetails(
|
||||
@Parameter(description = "Name of the SOP skill")
|
||||
@PathVariable String skillName) {
|
||||
SopDefinition skill = skillRegistry.getSkill(skillName);
|
||||
if (skill == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(skill);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a SOP skill with streaming output (SSE mode).
|
||||
* Use this for real-time progress updates in UI.
|
||||
*/
|
||||
@PostMapping(value = "/execute/{skillName}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
@Operation(summary = "Execute SOP skill (streaming)",
|
||||
description = "Execute a SOP skill with streaming output for real-time progress")
|
||||
public Flux<String> executeSopStream(
|
||||
@Parameter(description = "Name of the SOP skill to execute")
|
||||
@PathVariable String skillName,
|
||||
@Parameter(description = "Input parameters for the SOP")
|
||||
@RequestBody(required = false) Map<String, Object> params) {
|
||||
|
||||
log.info("Executing SOP skill (stream): {} with params: {}", skillName, params);
|
||||
|
||||
SopDefinition skill = skillRegistry.getSkill(skillName);
|
||||
if (skill == null) {
|
||||
return Flux.just("Error: SOP skill not found: " + skillName);
|
||||
}
|
||||
|
||||
Map<String, Object> inputParams = params != null ? params : new HashMap<>();
|
||||
|
||||
return sopEngine.execute(skill, inputParams)
|
||||
.doOnNext(msg -> log.debug("SOP output: {}", msg))
|
||||
.doOnError(e -> log.error("SOP execution error: {}", e.getMessage()))
|
||||
.doOnComplete(() -> log.info("SOP {} execution completed", skillName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a SOP skill synchronously and return unified result.
|
||||
* Use this for AI tool calls and programmatic access.
|
||||
*/
|
||||
@PostMapping(value = "/execute/{skillName}/sync", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@Operation(summary = "Execute SOP skill (sync)",
|
||||
description = "Execute a SOP skill synchronously and return unified result")
|
||||
public ResponseEntity<SopResult> executeSopSync(
|
||||
@Parameter(description = "Name of the SOP skill to execute")
|
||||
@PathVariable String skillName,
|
||||
@Parameter(description = "Input parameters for the SOP")
|
||||
@RequestBody(required = false) Map<String, Object> params) {
|
||||
|
||||
log.info("Executing SOP skill (sync): {} with params: {}", skillName, params);
|
||||
|
||||
SopDefinition skill = skillRegistry.getSkill(skillName);
|
||||
if (skill == null) {
|
||||
SopResult errorResult = SopResult.builder()
|
||||
.sopName(skillName)
|
||||
.status("FAILED")
|
||||
.error("SOP skill not found: " + skillName)
|
||||
.build();
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
Map<String, Object> inputParams = params != null ? params : new HashMap<>();
|
||||
|
||||
SopResult result = sopEngine.executeSync(skill, inputParams);
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a SOP skill and return AI-friendly text response.
|
||||
* Use this when SOP is called as a tool by AI.
|
||||
*/
|
||||
@PostMapping(value = "/execute/{skillName}/ai", produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
@Operation(summary = "Execute SOP skill (AI format)",
|
||||
description = "Execute a SOP and return AI-friendly text response")
|
||||
public ResponseEntity<String> executeSopForAi(
|
||||
@Parameter(description = "Name of the SOP skill to execute")
|
||||
@PathVariable String skillName,
|
||||
@Parameter(description = "Input parameters for the SOP")
|
||||
@RequestBody(required = false) Map<String, Object> params) {
|
||||
|
||||
log.info("Executing SOP skill (AI): {} with params: {}", skillName, params);
|
||||
|
||||
SopDefinition skill = skillRegistry.getSkill(skillName);
|
||||
if (skill == null) {
|
||||
return ResponseEntity.ok("Error: SOP skill not found: " + skillName);
|
||||
}
|
||||
|
||||
Map<String, Object> inputParams = params != null ? params : new HashMap<>();
|
||||
|
||||
SopResult result = sopEngine.executeSync(skill, inputParams);
|
||||
return ResponseEntity.ok(result.toAiResponse());
|
||||
}
|
||||
}
|
||||
-131
@@ -1,131 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import java.util.List;
|
||||
import org.apache.hertzbeat.ai.service.SopScheduleService;
|
||||
import org.apache.hertzbeat.common.entity.ai.SopSchedule;
|
||||
import org.apache.hertzbeat.common.entity.dto.Message;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* REST Controller for managing scheduled SOP executions.
|
||||
*/
|
||||
@Tag(name = "SOP Schedule API", description = "Manage scheduled SOP executions")
|
||||
@RestController
|
||||
@RequestMapping("/api/ai/schedule")
|
||||
public class SopScheduleController {
|
||||
|
||||
private final SopScheduleService sopScheduleService;
|
||||
private final org.apache.hertzbeat.ai.sop.registry.SkillRegistry skillRegistry;
|
||||
|
||||
@Autowired
|
||||
public SopScheduleController(SopScheduleService sopScheduleService,
|
||||
org.apache.hertzbeat.ai.sop.registry.SkillRegistry skillRegistry) {
|
||||
this.sopScheduleService = sopScheduleService;
|
||||
this.skillRegistry = skillRegistry;
|
||||
}
|
||||
|
||||
@Operation(summary = "Get all available SOP skills")
|
||||
@GetMapping("/skills")
|
||||
public ResponseEntity<Message<List<java.util.Map<String, String>>>> getAvailableSkills() {
|
||||
List<java.util.Map<String, String>> skills = new java.util.ArrayList<>();
|
||||
for (var skill : skillRegistry.getAllSkills()) {
|
||||
java.util.Map<String, String> skillInfo = new java.util.HashMap<>();
|
||||
skillInfo.put("name", skill.getName());
|
||||
skillInfo.put("description", skill.getDescription());
|
||||
skills.add(skillInfo);
|
||||
}
|
||||
return ResponseEntity.ok(Message.success(skills));
|
||||
}
|
||||
|
||||
@Operation(summary = "Get all schedules for a conversation")
|
||||
@GetMapping("/conversation/{conversationId}")
|
||||
public ResponseEntity<Message<List<SopSchedule>>> getSchedulesByConversation(
|
||||
@PathVariable Long conversationId) {
|
||||
List<SopSchedule> schedules = sopScheduleService.getSchedulesByConversation(conversationId);
|
||||
return ResponseEntity.ok(Message.success(schedules));
|
||||
}
|
||||
|
||||
@Operation(summary = "Get a schedule by ID")
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<Message<SopSchedule>> getSchedule(@PathVariable Long id) {
|
||||
SopSchedule schedule = sopScheduleService.getSchedule(id);
|
||||
if (schedule == null) {
|
||||
return ResponseEntity.ok(Message.fail((byte) 404, "Schedule not found"));
|
||||
}
|
||||
return ResponseEntity.ok(Message.success(schedule));
|
||||
}
|
||||
|
||||
@Operation(summary = "Create a new scheduled SOP task")
|
||||
@PostMapping
|
||||
public ResponseEntity<Message<SopSchedule>> createSchedule(@RequestBody SopSchedule schedule) {
|
||||
try {
|
||||
SopSchedule created = sopScheduleService.createSchedule(schedule);
|
||||
return ResponseEntity.ok(Message.success(created));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.ok(Message.fail((byte) 400, e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "Update an existing schedule")
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<Message<SopSchedule>> updateSchedule(
|
||||
@PathVariable Long id,
|
||||
@RequestBody SopSchedule schedule) {
|
||||
try {
|
||||
schedule.setId(id);
|
||||
SopSchedule updated = sopScheduleService.updateSchedule(schedule);
|
||||
return ResponseEntity.ok(Message.success(updated));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.ok(Message.fail((byte) 400, e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "Delete a schedule")
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Message<Void>> deleteSchedule(@PathVariable Long id) {
|
||||
sopScheduleService.deleteSchedule(id);
|
||||
return ResponseEntity.ok(Message.success());
|
||||
}
|
||||
|
||||
@Operation(summary = "Toggle schedule enabled status")
|
||||
@PutMapping("/{id}/toggle")
|
||||
public ResponseEntity<Message<SopSchedule>> toggleSchedule(
|
||||
@PathVariable Long id,
|
||||
@RequestParam boolean enabled) {
|
||||
try {
|
||||
SopSchedule updated = sopScheduleService.toggleSchedule(id, enabled);
|
||||
return ResponseEntity.ok(Message.success(updated));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.ok(Message.fail((byte) 400, e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.dao;
|
||||
|
||||
import org.apache.hertzbeat.common.entity.ai.ChatConversation;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/**
|
||||
* Repository interface for Conversation entities
|
||||
*/
|
||||
@Repository
|
||||
public interface ChatConversationDao extends JpaRepository<ChatConversation, Long> {
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.dao;
|
||||
|
||||
import org.apache.hertzbeat.common.entity.ai.ChatMessage;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Repository interface for Message entities
|
||||
*/
|
||||
@Repository
|
||||
public interface ChatMessageDao extends JpaRepository<ChatMessage, Long> {
|
||||
|
||||
/**
|
||||
* Find all messages by conversation id, ordered by create time
|
||||
* @param conversationId conversation id
|
||||
* @return list of messages
|
||||
*/
|
||||
List<ChatMessage> findByConversationIdOrderByGmtCreateAsc(Long conversationId);
|
||||
|
||||
/**
|
||||
* Find all messages by conversation ids, ordered by create time
|
||||
* @param conversationIds list of conversation ids
|
||||
* @return list of messages
|
||||
*/
|
||||
List<ChatMessage> findByConversationIdInOrderByGmtCreateAsc(List<Long> conversationIds);
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.dao;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import org.apache.hertzbeat.common.entity.ai.SopSchedule;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/**
|
||||
* Repository for SopSchedule entity operations.
|
||||
*/
|
||||
@Repository
|
||||
public interface SopScheduleDao extends JpaRepository<SopSchedule, Long>, JpaSpecificationExecutor<SopSchedule> {
|
||||
|
||||
/**
|
||||
* Find all schedules for a specific conversation.
|
||||
* @param conversationId The conversation ID
|
||||
* @return List of schedules
|
||||
*/
|
||||
List<SopSchedule> findByConversationId(Long conversationId);
|
||||
|
||||
/**
|
||||
* Find all enabled schedules that are due for execution.
|
||||
* @param currentTime The current time to compare against
|
||||
* @return List of due schedules
|
||||
*/
|
||||
@Query("SELECT s FROM SopSchedule s WHERE s.enabled = true AND s.nextRunTime <= :currentTime")
|
||||
List<SopSchedule> findDueSchedules(@Param("currentTime") LocalDateTime currentTime);
|
||||
|
||||
/**
|
||||
* Find all enabled schedules.
|
||||
* @return List of enabled schedules
|
||||
*/
|
||||
List<SopSchedule> findByEnabledTrue();
|
||||
|
||||
/**
|
||||
* Delete all schedules for a conversation.
|
||||
* @param conversationId The conversation ID
|
||||
*/
|
||||
void deleteByConversationId(Long conversationId);
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.pojo.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Chat response DTO for AI responses.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Chat response")
|
||||
public class ChatResponseChunk {
|
||||
|
||||
@Schema(description = "Conversation ID", example = "123")
|
||||
private Long conversationId;
|
||||
|
||||
@Schema(description = "AI response message", example = "Here are your monitors...")
|
||||
private String response;
|
||||
|
||||
@Schema(description = "User message ID", example = "123")
|
||||
private Long userMessageId;
|
||||
|
||||
@Schema(description = "Assistant message ID", example = "123")
|
||||
private Long assistantMessageId;
|
||||
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.pojo.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* security data
|
||||
*/
|
||||
@Data
|
||||
public class SecurityData {
|
||||
|
||||
@NotNull
|
||||
@Schema(description = "Conversation ID", example = "123")
|
||||
private Long conversationId;
|
||||
|
||||
@NotNull
|
||||
@Schema(description = "security data", example = "{\"password\":\"xxxxx\"}")
|
||||
private String securityData;
|
||||
|
||||
}
|
||||
@@ -1,175 +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.schedule;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.dao.ChatMessageDao;
|
||||
import org.apache.hertzbeat.ai.service.SopScheduleService;
|
||||
import org.apache.hertzbeat.ai.sop.engine.SopEngine;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopResult;
|
||||
import org.apache.hertzbeat.ai.sop.registry.SkillRegistry;
|
||||
import org.apache.hertzbeat.ai.utils.SopMessageUtil;
|
||||
import org.apache.hertzbeat.common.entity.ai.ChatMessage;
|
||||
import org.apache.hertzbeat.common.entity.ai.SopSchedule;
|
||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Scheduled executor that checks for due SOP schedules and executes them.
|
||||
* Runs every minute to check if any schedules are due for execution.
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@EnableScheduling
|
||||
public class SopScheduleExecutor {
|
||||
|
||||
/**
|
||||
* Role identifier for system push messages in conversations.
|
||||
*/
|
||||
public static final String ROLE_SYSTEM_PUSH = "system_push";
|
||||
|
||||
private final SopScheduleService sopScheduleService;
|
||||
private final SopEngine sopEngine;
|
||||
private final SkillRegistry skillRegistry;
|
||||
private final ChatMessageDao chatMessageDao;
|
||||
|
||||
@Autowired
|
||||
public SopScheduleExecutor(SopScheduleService sopScheduleService,
|
||||
@Lazy SopEngine sopEngine,
|
||||
@Lazy SkillRegistry skillRegistry,
|
||||
ChatMessageDao chatMessageDao) {
|
||||
this.sopScheduleService = sopScheduleService;
|
||||
this.sopEngine = sopEngine;
|
||||
this.skillRegistry = skillRegistry;
|
||||
this.chatMessageDao = chatMessageDao;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for due schedules every minute and execute them.
|
||||
*/
|
||||
@Scheduled(fixedRate = 60000) // Every minute
|
||||
public void checkAndExecuteDueSchedules() {
|
||||
try {
|
||||
List<SopSchedule> dueSchedules = sopScheduleService.getDueSchedules();
|
||||
|
||||
if (dueSchedules.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("Found {} due schedules to execute", dueSchedules.size());
|
||||
|
||||
for (SopSchedule schedule : dueSchedules) {
|
||||
executeSchedule(schedule);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error checking due schedules", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a single scheduled SOP and push result to conversation.
|
||||
*/
|
||||
private void executeSchedule(SopSchedule schedule) {
|
||||
log.info("Executing scheduled SOP {} for conversation {}",
|
||||
schedule.getSopName(), schedule.getConversationId());
|
||||
|
||||
try {
|
||||
// Check if skill exists
|
||||
var definition = skillRegistry.getSkill(schedule.getSopName());
|
||||
if (definition == null) {
|
||||
log.warn("Skill {} not found, skipping schedule {}",
|
||||
schedule.getSopName(), schedule.getId());
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse parameters
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
if (schedule.getSopParams() != null && !schedule.getSopParams().isEmpty()) {
|
||||
try {
|
||||
params = JsonUtil.fromJson(schedule.getSopParams(), Map.class);
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to parse SOP params: {}", schedule.getSopParams());
|
||||
}
|
||||
}
|
||||
|
||||
// Execute SOP
|
||||
SopResult result = sopEngine.executeSync(definition, params);
|
||||
|
||||
// Create push message
|
||||
String messageContent = formatPushMessage(schedule, result);
|
||||
|
||||
// Save message to conversation
|
||||
ChatMessage pushMessage = ChatMessage.builder()
|
||||
.conversationId(schedule.getConversationId())
|
||||
.role(ROLE_SYSTEM_PUSH)
|
||||
.content(messageContent)
|
||||
.build();
|
||||
|
||||
chatMessageDao.save(pushMessage);
|
||||
|
||||
log.info("Successfully pushed SOP result to conversation {}",
|
||||
schedule.getConversationId());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to execute scheduled SOP {} for conversation {}",
|
||||
schedule.getSopName(), schedule.getConversationId(), e);
|
||||
|
||||
// Still save an error message
|
||||
String errorContent = SopMessageUtil.getMessage("schedule.push.error.prefix") + " " + schedule.getSopName()
|
||||
+ "\n\n" + SopMessageUtil.getMessage("schedule.push.error.label") + " " + e.getMessage();
|
||||
ChatMessage errorMessage = ChatMessage.builder()
|
||||
.conversationId(schedule.getConversationId())
|
||||
.role(ROLE_SYSTEM_PUSH)
|
||||
.content(errorContent)
|
||||
.build();
|
||||
chatMessageDao.save(errorMessage);
|
||||
|
||||
} finally {
|
||||
// Update schedule times
|
||||
sopScheduleService.updateAfterExecution(schedule.getId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the push message content with SOP result.
|
||||
*/
|
||||
private String formatPushMessage(SopSchedule schedule, SopResult result) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(SopMessageUtil.getMessage("schedule.push.report.title")).append("\n\n");
|
||||
sb.append(SopMessageUtil.getMessage("schedule.push.report.skill")).append(": ").append(schedule.getSopName()).append("\n");
|
||||
sb.append(SopMessageUtil.getMessage("schedule.push.report.time")).append(": ").append(LocalDateTime.now()).append("\n\n");
|
||||
sb.append("---\n\n");
|
||||
|
||||
if ("SUCCESS".equals(result.getStatus())) {
|
||||
sb.append(result.getContent());
|
||||
} else {
|
||||
sb.append(SopMessageUtil.getMessage("schedule.push.report.failed")).append("\n\n");
|
||||
sb.append(result.getError());
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package org.apache.hertzbeat.ai.service;
|
||||
|
||||
import org.apache.hertzbeat.ai.pojo.dto.ChatResponseChunk;
|
||||
import org.apache.hertzbeat.ai.pojo.dto.SecurityData;
|
||||
import org.apache.hertzbeat.common.entity.ai.ChatConversation;
|
||||
import org.springframework.http.codec.ServerSentEvent;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Service for managing chat conversations and interactions with LLM providers.
|
||||
*/
|
||||
public interface ConversationService {
|
||||
|
||||
/**
|
||||
* Send a message and receive a streaming response
|
||||
*
|
||||
* @param message The user's message
|
||||
* @param conversationId Optional conversation ID for continuing a chat
|
||||
* @return Flux of ServerSentEvent for streaming the response
|
||||
*/
|
||||
Flux<ServerSentEvent<ChatResponseChunk>> streamChat(String message, Long conversationId);
|
||||
|
||||
/**
|
||||
* Create a new conversation
|
||||
*
|
||||
* @return Created conversation data
|
||||
*/
|
||||
ChatConversation createConversation();
|
||||
|
||||
/**
|
||||
* Get conversation history for a specific conversation
|
||||
*
|
||||
* @param conversationId Conversation ID
|
||||
* @return Conversation data including messages
|
||||
*/
|
||||
ChatConversation getConversation(Long conversationId);
|
||||
|
||||
/**
|
||||
* Get all conversations for the current user
|
||||
*
|
||||
* @return List of conversations
|
||||
*/
|
||||
List<ChatConversation> getAllConversations();
|
||||
|
||||
/**
|
||||
* Delete a conversation
|
||||
*
|
||||
* @param conversationId Conversation ID to delete
|
||||
*/
|
||||
void deleteConversation(Long conversationId);
|
||||
|
||||
/**
|
||||
* save security data for a conversation
|
||||
*
|
||||
* @param securityData securityData
|
||||
* @return save result
|
||||
*/
|
||||
Boolean saveSecurityData(SecurityData securityData);
|
||||
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package org.apache.hertzbeat.ai.service;
|
||||
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
|
||||
/**
|
||||
* Service interface for MCP server operations.
|
||||
*/
|
||||
public interface McpServerService {
|
||||
|
||||
/**
|
||||
* Provides the HertzBeat tools for the MCP server
|
||||
* @return ToolCallbackProvider with all HertzBeat monitoring tools
|
||||
*/
|
||||
ToolCallbackProvider hertzbeatTools();
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.service;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.hertzbeat.common.entity.ai.SopSchedule;
|
||||
|
||||
/**
|
||||
* Service interface for managing scheduled SOP executions.
|
||||
*/
|
||||
public interface SopScheduleService {
|
||||
|
||||
/**
|
||||
* Create a new scheduled SOP task.
|
||||
* @param schedule The schedule configuration
|
||||
* @return The created schedule with generated ID
|
||||
*/
|
||||
SopSchedule createSchedule(SopSchedule schedule);
|
||||
|
||||
/**
|
||||
* Update an existing schedule.
|
||||
* @param schedule The updated schedule
|
||||
* @return The updated schedule
|
||||
*/
|
||||
SopSchedule updateSchedule(SopSchedule schedule);
|
||||
|
||||
/**
|
||||
* Delete a schedule by ID.
|
||||
* @param id The schedule ID
|
||||
*/
|
||||
void deleteSchedule(Long id);
|
||||
|
||||
/**
|
||||
* Get a schedule by ID.
|
||||
* @param id The schedule ID
|
||||
* @return The schedule or null if not found
|
||||
*/
|
||||
SopSchedule getSchedule(Long id);
|
||||
|
||||
/**
|
||||
* Get all schedules for a conversation.
|
||||
* @param conversationId The conversation ID
|
||||
* @return List of schedules
|
||||
*/
|
||||
List<SopSchedule> getSchedulesByConversation(Long conversationId);
|
||||
|
||||
/**
|
||||
* Toggle the enabled status of a schedule.
|
||||
* @param id The schedule ID
|
||||
* @param enabled The new enabled status
|
||||
* @return The updated schedule
|
||||
*/
|
||||
SopSchedule toggleSchedule(Long id, boolean enabled);
|
||||
|
||||
/**
|
||||
* Get all schedules that are due for execution.
|
||||
* @return List of due schedules
|
||||
*/
|
||||
List<SopSchedule> getDueSchedules();
|
||||
|
||||
/**
|
||||
* Update the execution times after a schedule runs.
|
||||
* @param id The schedule ID
|
||||
*/
|
||||
void updateAfterExecution(Long id);
|
||||
}
|
||||
-220
@@ -1,220 +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 java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopDefinition;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopParameter;
|
||||
import org.apache.hertzbeat.ai.sop.registry.SkillRegistry;
|
||||
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.support.event.AiProviderConfigChangeEvent;
|
||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.messages.AssistantMessage;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Implementation of the {@link ChatClientProviderService}. Provides functionality to interact with the ChatClient for
|
||||
* handling chat messages.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class ChatClientProviderServiceImpl implements ChatClientProviderService {
|
||||
|
||||
private static final String SKILLS_PLACEHOLDER = "{dynamically_injected_skills_list}";
|
||||
private static final String CONVERSATION_ID_PLACEHOLDER = "{current_conversation_id}";
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
private final GeneralConfigDao generalConfigDao;
|
||||
|
||||
private ModelProviderConfig modelProviderConfig;
|
||||
|
||||
|
||||
private final SkillRegistry skillRegistry;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("hertzbeatTools")
|
||||
private ToolCallbackProvider toolCallbackProvider;
|
||||
|
||||
private boolean isConfigured = false;
|
||||
|
||||
@Value("classpath:/prompt/system-message.st")
|
||||
private Resource systemResource;
|
||||
|
||||
@Value("classpath:/prompt/extra-message-protected.st")
|
||||
private Resource extraResourceProtected;
|
||||
|
||||
|
||||
@Autowired
|
||||
public ChatClientProviderServiceImpl(ApplicationContext applicationContext,
|
||||
GeneralConfigDao generalConfigDao,
|
||||
@Lazy SkillRegistry skillRegistry) {
|
||||
this.applicationContext = applicationContext;
|
||||
this.generalConfigDao = generalConfigDao;
|
||||
this.skillRegistry = skillRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<String> streamChat(ChatRequestContext context) {
|
||||
try {
|
||||
// Get the current (potentially refreshed) ChatClient instance
|
||||
ChatClient chatClient = applicationContext.getBean("openAiChatClient", ChatClient.class);
|
||||
|
||||
List<Message> messages = new ArrayList<>();
|
||||
|
||||
// Add conversation history if available
|
||||
if (context.getConversationHistory() != null && !context.getConversationHistory().isEmpty()) {
|
||||
for (ChatMessage historyMessage : context.getConversationHistory()) {
|
||||
if ("user".equals(historyMessage.getRole())) {
|
||||
messages.add(new UserMessage(historyMessage.getContent()));
|
||||
} else if ("assistant".equals(historyMessage.getRole())) {
|
||||
messages.add(new AssistantMessage(historyMessage.getContent()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
messages.add(new UserMessage(context.getMessage()));
|
||||
|
||||
log.info("Starting streaming chat for conversation: {}", context.getConversationId());
|
||||
|
||||
// Build system prompt with dynamic skills list and conversation ID
|
||||
// The conversationId is injected into the prompt so AI can pass it to schedule tools
|
||||
String systemPrompt = buildSystemPrompt(context.getConversationId());
|
||||
|
||||
return chatClient.prompt()
|
||||
.messages(messages)
|
||||
.system(systemPrompt)
|
||||
.tools(toolCallbackProvider)
|
||||
.stream()
|
||||
.content()
|
||||
.doOnComplete(() -> log.info("Streaming completed for conversation: {}", context.getConversationId()))
|
||||
.doOnError(error -> log.error("Error in streaming chat: {}", error.getMessage(), error));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error setting up streaming chat: {}", e.getMessage(), e);
|
||||
return Flux.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the system prompt with dynamically injected skills list and conversation ID.
|
||||
*/
|
||||
private String buildSystemPrompt(Long conversationId) {
|
||||
try {
|
||||
String template = systemResource.getContentAsString(StandardCharsets.UTF_8);
|
||||
String skillsList = generateSkillsList();
|
||||
template = template
|
||||
.replace(SKILLS_PLACEHOLDER, skillsList)
|
||||
.replace(CONVERSATION_ID_PLACEHOLDER, String.valueOf(conversationId));
|
||||
|
||||
// add extra prompt for protected model to guide it to use protected tools
|
||||
if (Objects.equals(modelProviderConfig.getParticipationModel(), "PROTECTED")) {
|
||||
Map<String, Object> metadata = new HashMap<>();
|
||||
metadata.put("conversationId", conversationId);
|
||||
return template + SystemPromptTemplate.builder().resource(extraResourceProtected).build()
|
||||
.create(metadata)
|
||||
.getContents();
|
||||
} else {
|
||||
return template;
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to read system prompt template: {}", e.getMessage());
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate a formatted list of available skills for the system prompt.
|
||||
*/
|
||||
private String generateSkillsList() {
|
||||
List<SopDefinition> skills = skillRegistry.getAllSkills();
|
||||
|
||||
if (skills.isEmpty()) {
|
||||
return "No skills currently available. Use listSkills tool to refresh.";
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (SopDefinition skill : skills) {
|
||||
sb.append("- **").append(skill.getName()).append("**: ");
|
||||
sb.append(skill.getDescription());
|
||||
|
||||
// Add parameter hints
|
||||
if (skill.getParameters() != null && !skill.getParameters().isEmpty()) {
|
||||
sb.append(" (requires: ");
|
||||
List<String> paramNames = new ArrayList<>();
|
||||
for (SopParameter param : skill.getParameters()) {
|
||||
if (param.isRequired()) {
|
||||
paramNames.add(param.getName());
|
||||
}
|
||||
}
|
||||
sb.append(String.join(", ", paramNames));
|
||||
sb.append(")");
|
||||
}
|
||||
sb.append("\n");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@EventListener(AiProviderConfigChangeEvent.class)
|
||||
public void onAiProviderConfigChange(AiProviderConfigChangeEvent event) {
|
||||
GeneralConfig providerConfig = generalConfigDao.findByType("provider");
|
||||
this.modelProviderConfig = JsonUtil.fromJson(providerConfig.getContent(), ModelProviderConfig.class);
|
||||
}
|
||||
|
||||
@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;
|
||||
this.modelProviderConfig = modelProviderConfig;
|
||||
}
|
||||
return isConfigured;
|
||||
}
|
||||
}
|
||||
-215
@@ -1,215 +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 java.util.Optional;
|
||||
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.pojo.dto.SecurityData;
|
||||
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.apache.hertzbeat.common.util.AesUtil;
|
||||
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.transaction.annotation.Transactional;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
ChatConversation conversation = conversationDao.findById(conversationId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Conversation not found: " + conversationId));
|
||||
|
||||
// Manually load messages for conversation history
|
||||
List<ChatMessage> messages = messageDao.findByConversationIdOrderByGmtCreateAsc(conversationId);
|
||||
conversation.setMessages(messages);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// Add user message to conversation
|
||||
ChatMessage chatMessage = ChatMessage.builder()
|
||||
.conversationId(conversationId)
|
||||
.content(message)
|
||||
.role("user")
|
||||
.build();
|
||||
chatMessage = messageDao.save(chatMessage);
|
||||
|
||||
ChatRequestContext context = ChatRequestContext.builder()
|
||||
.message(message)
|
||||
.conversationId(conversationId)
|
||||
.conversationHistory(messages)
|
||||
.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;
|
||||
}
|
||||
ChatConversation conversation = conversationDao.findById(conversationId).orElse(null);
|
||||
if (conversation != null) {
|
||||
List<ChatMessage> messages = messageDao.findByConversationIdOrderByGmtCreateAsc(conversationId);
|
||||
conversation.setMessages(messages);
|
||||
}
|
||||
return conversation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ChatConversation> getAllConversations() {
|
||||
List<ChatConversation> conversations = conversationDao.findAll(Sort.by(Sort.Direction.DESC, "id"));
|
||||
if (conversations.isEmpty()) {
|
||||
return conversations;
|
||||
}
|
||||
List<Long> conversationIds = conversations.stream()
|
||||
.map(ChatConversation::getId)
|
||||
.toList();
|
||||
List<ChatMessage> allMessages = messageDao.findByConversationIdInOrderByGmtCreateAsc(conversationIds);
|
||||
Map<Long, List<ChatMessage>> messagesByConversationId = allMessages.stream()
|
||||
.collect(Collectors.groupingBy(ChatMessage::getConversationId));
|
||||
for (ChatConversation conversation : conversations) {
|
||||
List<ChatMessage> messages = messagesByConversationId.getOrDefault(conversation.getId(),
|
||||
Collections.emptyList());
|
||||
conversation.setMessages(messages);
|
||||
}
|
||||
return conversations;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteConversation(Long conversationId) {
|
||||
List<ChatMessage> messages = messageDao.findByConversationIdOrderByGmtCreateAsc(conversationId);
|
||||
if (!messages.isEmpty()) {
|
||||
messageDao.deleteAll(messages);
|
||||
}
|
||||
conversationDao.deleteById(conversationId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean saveSecurityData(SecurityData securityData) {
|
||||
Optional<ChatConversation> chatConversation = conversationDao.findById(securityData.getConversationId());
|
||||
if (chatConversation.isPresent()) {
|
||||
ChatConversation conversation = chatConversation.get();
|
||||
conversation.setSecurityData(AesUtil.aesEncode(securityData.getSecurityData()));
|
||||
conversationDao.save(conversation);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
-66
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package org.apache.hertzbeat.ai.service.impl;
|
||||
|
||||
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.tools.ScheduleTools;
|
||||
import org.apache.hertzbeat.ai.tools.SkillTools;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Implementation of the McpServerService interface.
|
||||
* This service provides functionality for handling MCP server operations.
|
||||
*/
|
||||
@Service
|
||||
@Configuration
|
||||
public class McpServerServiceImpl implements McpServerService {
|
||||
@Autowired
|
||||
private MonitorTools monitorTools;
|
||||
@Autowired
|
||||
private AlertTools alertTools;
|
||||
@Autowired
|
||||
private MetricsTools metricsTools;
|
||||
@Autowired
|
||||
private AlertDefineTools alertDefineTools;
|
||||
@Autowired
|
||||
private SkillTools skillTools;
|
||||
@Autowired
|
||||
private ScheduleTools scheduleTools;
|
||||
|
||||
@Bean
|
||||
public ToolCallbackProvider hertzbeatTools() {
|
||||
// Note: DatabaseTools is NOT exposed to AI directly.
|
||||
// AI should use Skills (via skillTools) to perform database diagnostics.
|
||||
return MethodToolCallbackProvider.builder()
|
||||
.toolObjects(monitorTools, alertTools, alertDefineTools, metricsTools,
|
||||
skillTools, scheduleTools)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user