mirror of
https://github.com/dromara/hertzbeat.git
synced 2026-09-17 18:19:02 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad4028e284 | ||
|
|
5a17f10c36 | ||
|
|
35a7154259 | ||
|
|
485bca1a1f |
+1
-251
@@ -2545,8 +2545,7 @@
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/176664901?v=4",
|
||||
"profile": "https://github.com/Saramanda9988",
|
||||
"contributions": [
|
||||
"doc",
|
||||
"code"
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -2668,255 +2667,6 @@
|
||||
"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"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "orangeCatDeveloper",
|
||||
"name": "NekoPunch",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/95899648?v=4",
|
||||
"profile": "https://github.com/orangeCatDeveloper",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "wy471x",
|
||||
"name": "wy471x",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/52033069?v=4",
|
||||
"profile": "https://wy471x.github.io/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "hengyuss",
|
||||
"name": "hengyuss",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/81064732?v=4",
|
||||
"profile": "https://github.com/hengyuss",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "moduvoice",
|
||||
"name": "moduvoice",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/291867022?v=4",
|
||||
"profile": "https://moduvoice.com/",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "hutiefang76",
|
||||
"name": "hutiefang76",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/137664623?v=4",
|
||||
"profile": "https://github.com/hutiefang76",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ZhouYinLong-lab",
|
||||
"name": "柳含知 Liu Hanzhi",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/250777154?v=4",
|
||||
"profile": "https://zylatent.com/",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "wilmerdooley",
|
||||
"name": "wilmerdooley",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/259930736?v=4",
|
||||
"profile": "https://github.com/wilmerdooley",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Zmjjeff7",
|
||||
"name": "Zhenhong Guo",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/175370943?v=4",
|
||||
"profile": "https://github.com/Zmjjeff7",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "abhyudayareddy",
|
||||
"name": "abhyudayareddy",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/54602866?v=4",
|
||||
"profile": "https://github.com/abhyudayareddy",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "neon-hippo",
|
||||
"name": "neon-hippo",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/165560498?v=4",
|
||||
"profile": "https://github.com/neon-hippo",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "P-Peaceful",
|
||||
"name": "P_Peaceful",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/52856161?v=4",
|
||||
"profile": "https://github.com/P-Peaceful",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "zhusaidong",
|
||||
"name": "zhusaidong",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/3039961?v=4",
|
||||
"profile": "https://github.com/zhusaidong",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "zhehenlu",
|
||||
"name": "zhlu",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/31504542?v=4",
|
||||
"profile": "https://github.com/zhehenlu",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "brettgervasoni",
|
||||
"name": "brettgervasoni",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/34056000?v=4",
|
||||
"profile": "https://github.com/brettgervasoni",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Darshan-paul",
|
||||
"name": "Darshan-paul",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/211450705?v=4",
|
||||
"profile": "https://github.com/Darshan-paul",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "04cb",
|
||||
"name": "layla",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/111667698?v=4",
|
||||
"profile": "https://github.com/04cb",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "miantalha45",
|
||||
"name": "Talha Amjad",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/155809113?v=4",
|
||||
"profile": "https://github.com/miantalha45",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "turanalmammadov",
|
||||
"name": "Turan Almammadov",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/16321061?v=4",
|
||||
"profile": "https://turanalmammadov.com/",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "zhaoyangplus",
|
||||
"name": "zhaoyangplus",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/245090302?v=4",
|
||||
"profile": "https://github.com/zhaoyangplus",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "yexuanyang",
|
||||
"name": "Yang Yexuan",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/73885401?v=4",
|
||||
"profile": "https://github.com/yexuanyang",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "markguo123",
|
||||
"name": "markguo123",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/155072651?v=4",
|
||||
"profile": "https://github.com/markguo123",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "leo-934",
|
||||
"name": "leo",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/55838224?v=4",
|
||||
"profile": "https://github.com/leo-934",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc",
|
||||
"blog"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "HertzBeat DevContainer",
|
||||
"name": "Hertzbeat DevContainer",
|
||||
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/java:1": {
|
||||
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"portsAttributes": {
|
||||
"4200": {
|
||||
"label": "HertzBeat Web",
|
||||
"label": "Hertzbeat Web",
|
||||
"onAutoForward": "notify"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
/home export-ignore
|
||||
/hip export-ignore
|
||||
/template-marketplace export-ignore
|
||||
/.github export-ignore
|
||||
/.idea export-ignore
|
||||
/.devcontainer export-ignore
|
||||
|
||||
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: '22'
|
||||
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@992badcdf24e3b8eb7e87ff9287fe931bcb00c6e
|
||||
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 pnpm (must run before setup-node so the pnpm cache can be configured)
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
# Setup Node.js environment
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: pnpm
|
||||
cache-dependency-path: home/pnpm-lock.yaml
|
||||
|
||||
# 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,30 +30,17 @@ 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
|
||||
timeout-minutes: 30
|
||||
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
|
||||
working-directory: web-app
|
||||
run: pnpm install
|
||||
- name: EsLint Test
|
||||
working-directory: web-app
|
||||
run: pnpm lint:ts
|
||||
- name: Unit Test
|
||||
working-directory: web-app
|
||||
run: pnpm test
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: ${{ env.RUST_VERSION }}
|
||||
components: rustfmt, clippy
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -23,11 +23,6 @@ on:
|
||||
- cron: '0 0 * * *'
|
||||
push:
|
||||
branches: [ action* ]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -39,41 +34,39 @@ jobs:
|
||||
- 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
|
||||
|
||||
|
||||
- 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
|
||||
|
||||
@@ -56,7 +56,3 @@ application-mysql.yml
|
||||
application-pg.yml
|
||||
/package.json
|
||||
/yarn.lock
|
||||
|
||||
# Local Codex workspace files
|
||||
.codex/
|
||||
AGENTS.md
|
||||
|
||||
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"
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ vscode:
|
||||
|
||||
ports:
|
||||
- port: 4200
|
||||
name: HertzBeat
|
||||
name: Hertzbeat
|
||||
onOpen: open-browser
|
||||
|
||||
- port: 1157
|
||||
|
||||
@@ -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,
|
||||
@@ -44,8 +38,7 @@
|
||||
"MD045": true,
|
||||
"MD046": true,
|
||||
"MD047": true,
|
||||
"MD052": true,
|
||||
"MD060": false
|
||||
"MD052": true
|
||||
},
|
||||
"ignores": [
|
||||
"**/node_modules/**",
|
||||
|
||||
+70
-70
@@ -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,19 +20,19 @@
|
||||
|
||||
- 💬**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/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 [startup](hertzbeat-startup) 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`
|
||||
|
||||
@@ -57,51 +57,51 @@ Even small corrections to typos are very welcome :)
|
||||
6. 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 +112,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.
|
||||
|
||||
@@ -133,19 +133,19 @@ Add WeChat account `ahertzbeat` to pull you into the WeChat group.
|
||||
|
||||

|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
------
|
||||
|
||||
<br>
|
||||
|
||||
## 贡献者指南
|
||||
## 贡献者指南
|
||||
|
||||
> 非常欢迎参与项目贡献,我们致力于维护一个互相帮助的快乐社区。
|
||||
> 非常欢迎参与项目贡献,我们致力于维护一个互相帮助的快乐社区。
|
||||
|
||||
### 贡献方式
|
||||
### 贡献方式
|
||||
|
||||
> 在 HertzBeat 社区,贡献方式有很多:
|
||||
> 在 HertzBeat 社区,贡献方式有很多:
|
||||
|
||||
- 💻**代码**:可以帮助社区完成一些任务、编写新的feature或者是修复一些bug;
|
||||
|
||||
@@ -159,20 +159,20 @@ 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/emoji-key)
|
||||
|
||||
即便是小到错别字的修正我们也都非常欢迎 :)
|
||||
即便是小到错别字的修正我们也都非常欢迎 :)
|
||||
|
||||
### 让 HertzBeat 运行起来
|
||||
### 让 HertzBeat 运行起来
|
||||
|
||||
> 让 HertzBeat 的代码在您的开发工具上运行起来,并且能够断点调试。
|
||||
> 让 HertzBeat 的代码在您的开发工具上运行起来,并且能够断点调试。
|
||||
> 此为前后端分离项目,本地代码启动需将后端[hertzbeat-startup](hertzbeat-startup)和前端[web-app](web-app)分别启动生效。
|
||||
|
||||
#### 后端启动
|
||||
|
||||
1. 需要 `maven3+`, `java25` 和 `lombok` 环境
|
||||
1. 需要 `maven3+`, `java17` 和 `lombok` 环境
|
||||
|
||||
2. (可选)修改配置文件配置信息-`hertzbeat-startup/src/main/resources/application.yml`
|
||||
|
||||
@@ -197,71 +197,71 @@ Add WeChat account `ahertzbeat` to pull you into the WeChat group.
|
||||
|
||||
6. 浏览器访问 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 +271,4 @@ Add WeChat account `ahertzbeat` to pull you into the WeChat group.
|
||||
|
||||
### 模块
|
||||
|
||||

|
||||

|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Apache HertzBeat™
|
||||
Copyright 2024-2026 The Apache Software Foundation
|
||||
Copyright 2024-2025 The Apache Software Foundation
|
||||
|
||||
This product includes software developed at
|
||||
The Apache Software Foundation (http://www.apache.org/).
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
</p>
|
||||
|
||||
[](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)
|
||||
@@ -20,13 +21,13 @@
|
||||
[](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 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.
|
||||
|
||||
### Features
|
||||
|
||||
@@ -39,25 +40,71 @@
|
||||
- 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.
|
||||
|
||||
> 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 unified platform, AI intelligence, powerful customization, multi-type support, high performance, and easy expansion, aims to help users quickly and conveniently achieve observability requirements.
|
||||
|
||||
----
|
||||
|
||||
----
|
||||
|
||||
## 🥐 Architecture
|
||||
## 🥐 Architecture
|
||||
|
||||

|
||||

|
||||
|
||||
## 🐕 Quick Start
|
||||
## ⛄ 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.
|
||||
|
||||
|
||||
- [Website](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-website.yml), [Port Telnet](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-port.yml),
|
||||
[Http Api](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-api.yml), [Ping Connect](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ping.yml),
|
||||
[Jvm](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-jvm.yml), [SiteMap](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-fullsite.yml),
|
||||
[Ssl Certificate](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ssl_cert.yml), [SpringBoot2](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-springboot2.yml),
|
||||
[FTP Server](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ftp.yml), [SpringBoot3](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-springboot3.yml),
|
||||
[Udp Port](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-udp_port.yml), [Dns](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-dns.yml),
|
||||
[Pop3](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-pop3.yml), [Ntp](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ntp.yml),
|
||||
[Api Code](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-api_code.yml), [Smtp](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-smtp.yml),
|
||||
[Nginx](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-nginx.yml)
|
||||
- [Mysql](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-mysql.yml), [PostgreSQL](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-postgresql.yml),
|
||||
[MariaDB](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-mariadb.yml), [Redis](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-redis.yml),
|
||||
[ElasticSearch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-elasticsearch.yml), [SqlServer](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-sqlserver.yml),
|
||||
[Oracle](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-oracle.yml), [MongoDB](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-mongodb.yml),
|
||||
[DM](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-dm.yml), [OpenGauss](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-opengauss.yml),
|
||||
[ClickHouse](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-clickhouse.yml), [IoTDB](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-iotdb.yml),
|
||||
[Redis Cluster](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-redis_cluster.yml), [Redis Sentinel](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-redis_sentinel.yml)
|
||||
[Doris BE](https://github.com/apache/hertzbeat/blob/master/hertzbeat-manager/src/main/resources/define/app-doris_be.yml), [Doris FE](https://github.com/apache/hertzbeat/blob/master/hertzbeat-manager/src/main/resources/define/app-doris_fe.yml),
|
||||
[Memcached](https://github.com/apache/hertzbeat/blob/master/hertzbeat-manager/src/main/resources/define/app-memcached.yml), [NebulaGraph](https://github.com/apache/hertzbeat/blob/master/hertzbeat-manager/src/main/resources/define/app-nebula_graph.yml)
|
||||
- [Linux](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-linux.yml), [Ubuntu](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ubuntu.yml),
|
||||
[CentOS](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-centos.yml), [Windows](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-windows.yml),
|
||||
[EulerOS](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-euleros.yml), [Fedora CoreOS](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-coreos.yml),
|
||||
[OpenSUSE](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-opensuse.yml), [Rocky Linux](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-rockylinux.yml),
|
||||
[Red Hat](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-redhat.yml), [FreeBSD](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-freebsd.yml),
|
||||
[AlmaLinux](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-almalinux.yml), [Debian Linux](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-debian.yml)
|
||||
- [Tomcat](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-tomcat.yml), [Nacos](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-nacos.yml),
|
||||
[Zookeeper](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-zookeeper.yml), [RabbitMQ](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-rabbitmq.yml),
|
||||
[Flink](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-flink.yml), [Kafka](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-kafka.yml),
|
||||
[ShenYu](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-shenyu.yml), [DynamicTp](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-dynamic_tp.yml),
|
||||
[Jetty](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-jetty.yml), [ActiveMQ](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-activemq.yml),
|
||||
[Spring Gateway](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-spring_gateway.yml), [EMQX MQTT](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-emqx.yml),
|
||||
[AirFlow](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-airflow.yml), [Hive](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-hive.yml),
|
||||
[Spark](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-spark.yml), [Hadoop](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-hadoop.yml)
|
||||
- [Kubernetes](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-kubernetes.yml), [Docker](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-docker.yml)
|
||||
- [CiscoSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-cisco_switch.yml), [HpeSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-hpe_switch.yml),
|
||||
[HuaweiSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-huawei_switch.yml), [TpLinkSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-tplink_switch.yml),
|
||||
[H3cSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-h3c_switch.yml)
|
||||
- And More Your Custom Template.
|
||||
- Notified Support `Discord` `Slack` `Telegram` `Email` `Dingtalk` `WeChat` `FeiShu` `Webhook` `SMS` `ServerChan`.
|
||||
|
||||
|
||||
## 🐕 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
|
||||
|
||||
@@ -65,7 +112,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)
|
||||
|
||||
@@ -79,16 +126,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:
|
||||
@@ -101,27 +148,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 `hertzbeat-startup` 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 `hertzbeat-startup` 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
|
||||
|
||||
@@ -133,8 +178,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 -->
|
||||
@@ -480,7 +525,7 @@ Thanks to these wonderful people, welcome to join us:
|
||||
<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/Saramanda9988"><img src="https://avatars.githubusercontent.com/u/176664901?v=4?s=100" width="100px;" alt="LunaRain_079"/><br /><sub><b>LunaRain_079</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/delei"><img src="https://avatars.githubusercontent.com/u/17263766?v=4?s=100" width="100px;" alt="DeleiGuo"/><br /><sub><b>DeleiGuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Adelei" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -498,41 +543,6 @@ Thanks to these wonderful people, welcome to join us:
|
||||
<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>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/orangeCatDeveloper"><img src="https://avatars.githubusercontent.com/u/95899648?v=4?s=100" width="100px;" alt="NekoPunch"/><br /><sub><b>NekoPunch</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=orangeCatDeveloper" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=orangeCatDeveloper" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://wy471x.github.io/"><img src="https://avatars.githubusercontent.com/u/52033069?v=4?s=100" width="100px;" alt="wy471x"/><br /><sub><b>wy471x</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wy471x" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hengyuss"><img src="https://avatars.githubusercontent.com/u/81064732?v=4?s=100" width="100px;" alt="hengyuss"/><br /><sub><b>hengyuss</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hengyuss" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://moduvoice.com/"><img src="https://avatars.githubusercontent.com/u/291867022?v=4?s=100" width="100px;" alt="moduvoice"/><br /><sub><b>moduvoice</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=moduvoice" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hutiefang76"><img src="https://avatars.githubusercontent.com/u/137664623?v=4?s=100" width="100px;" alt="hutiefang76"/><br /><sub><b>hutiefang76</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hutiefang76" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://zylatent.com/"><img src="https://avatars.githubusercontent.com/u/250777154?v=4?s=100" width="100px;" alt="柳含知 Liu Hanzhi"/><br /><sub><b>柳含知 Liu Hanzhi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ZhouYinLong-lab" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wilmerdooley"><img src="https://avatars.githubusercontent.com/u/259930736?v=4?s=100" width="100px;" alt="wilmerdooley"/><br /><sub><b>wilmerdooley</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wilmerdooley" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Zmjjeff7"><img src="https://avatars.githubusercontent.com/u/175370943?v=4?s=100" width="100px;" alt="Zhenhong Guo"/><br /><sub><b>Zhenhong Guo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Zmjjeff7" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/abhyudayareddy"><img src="https://avatars.githubusercontent.com/u/54602866?v=4?s=100" width="100px;" alt="abhyudayareddy"/><br /><sub><b>abhyudayareddy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=abhyudayareddy" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/neon-hippo"><img src="https://avatars.githubusercontent.com/u/165560498?v=4?s=100" width="100px;" alt="neon-hippo"/><br /><sub><b>neon-hippo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=neon-hippo" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/P-Peaceful"><img src="https://avatars.githubusercontent.com/u/52856161?v=4?s=100" width="100px;" alt="P_Peaceful"/><br /><sub><b>P_Peaceful</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=P-Peaceful" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=P-Peaceful" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zhusaidong"><img src="https://avatars.githubusercontent.com/u/3039961?v=4?s=100" width="100px;" alt="zhusaidong"/><br /><sub><b>zhusaidong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhusaidong" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zhehenlu"><img src="https://avatars.githubusercontent.com/u/31504542?v=4?s=100" width="100px;" alt="zhlu"/><br /><sub><b>zhlu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhehenlu" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=zhehenlu" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/brettgervasoni"><img src="https://avatars.githubusercontent.com/u/34056000?v=4?s=100" width="100px;" alt="brettgervasoni"/><br /><sub><b>brettgervasoni</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=brettgervasoni" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Darshan-paul"><img src="https://avatars.githubusercontent.com/u/211450705?v=4?s=100" width="100px;" alt="Darshan-paul"/><br /><sub><b>Darshan-paul</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Darshan-paul" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/04cb"><img src="https://avatars.githubusercontent.com/u/111667698?v=4?s=100" width="100px;" alt="layla"/><br /><sub><b>layla</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=04cb" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/miantalha45"><img src="https://avatars.githubusercontent.com/u/155809113?v=4?s=100" width="100px;" alt="Talha Amjad"/><br /><sub><b>Talha Amjad</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=miantalha45" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://turanalmammadov.com/"><img src="https://avatars.githubusercontent.com/u/16321061?v=4?s=100" width="100px;" alt="Turan Almammadov"/><br /><sub><b>Turan Almammadov</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=turanalmammadov" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=turanalmammadov" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zhaoyangplus"><img src="https://avatars.githubusercontent.com/u/245090302?v=4?s=100" width="100px;" alt="zhaoyangplus"/><br /><sub><b>zhaoyangplus</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhaoyangplus" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yexuanyang"><img src="https://avatars.githubusercontent.com/u/73885401?v=4?s=100" width="100px;" alt="Yang Yexuan"/><br /><sub><b>Yang Yexuan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yexuanyang" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/markguo123"><img src="https://avatars.githubusercontent.com/u/155072651?v=4?s=100" width="100px;" alt="markguo123"/><br /><sub><b>markguo123</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=markguo123" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/leo-934"><img src="https://avatars.githubusercontent.com/u/55838224?v=4?s=100" width="100px;" alt="leo"/><br /><sub><b>leo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=leo-934" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=leo-934" title="Documentation">📖</a> <a href="#blog-leo-934" title="Blogposts">📝</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -540,7 +550,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
|
||||
|
||||
@@ -552,10 +562,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)
|
||||
|
||||
+69
-57
@@ -10,6 +10,7 @@
|
||||
</p>
|
||||
|
||||
[](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,13 +20,13 @@
|
||||
[](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 驱动的下一代开源实时观测系统。指标日志统一收集,告警一站分发,智能管控分析。无需 Agent,高性能集群,提供强大的自定义监控和状态页构建能力。
|
||||
[Apache HertzBeat™](https://github.com/apache/hertzbeat) 是 AI 驱动的下一代开源实时观测系统。指标日志统一收集,告警一站分发,智能管控分析。无需 Agent,高性能集群,提供强大的自定义监控和状态页构建能力。
|
||||
|
||||
### 特点
|
||||
|
||||
@@ -39,26 +40,70 @@
|
||||
- 提供强大的状态页构建能力,轻松向用户传达您产品服务的实时状态。
|
||||
|
||||
|
||||
> `HertzBeat`的统一平台,AI智能,强大自定义,多类型支持,高性能,易扩展,希望能帮助用户快速方便实现观测需求。
|
||||
> `HertzBeat`的统一平台,AI智能,强大自定义,多类型支持,高性能,易扩展,希望能帮助用户快速方便实现观测需求。
|
||||
|
||||
----
|
||||
|
||||
----
|
||||
|
||||
## 🥐 模块
|
||||
## 🥐 模块
|
||||
|
||||

|
||||

|
||||
|
||||
## 🐕 快速开始
|
||||
## ⛄ 已支持
|
||||
|
||||
- 如果您是想将 HertzBeat 部署到内网环境搭建监控系统,请参考下面的部署文档进行操作。
|
||||
> 我们将监控采集类型(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),
|
||||
[Jvm](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-jvm.yml), [SiteMap](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-fullsite.yml),
|
||||
[Ssl Certificate](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ssl_cert.yml), [SpringBoot2](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-springboot2.yml),
|
||||
[FTP Server](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ftp.yml), [SpringBoot3](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-springboot3.yml),
|
||||
[Udp Port](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-udp_port.yml), [Dns](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-dns.yml),
|
||||
[Pop3](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-pop3.yml), [Ntp](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ntp.yml),
|
||||
[Api Code](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-api_code.yml), [Smtp](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-smtp.yml),
|
||||
[Nginx](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-nginx.yml)
|
||||
- [Mysql](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-mysql.yml), [PostgreSQL](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-postgresql.yml),
|
||||
[MariaDB](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-mariadb.yml), [Redis](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-redis.yml),
|
||||
[ElasticSearch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-elasticsearch.yml), [SqlServer](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-sqlserver.yml),
|
||||
[Oracle](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-oracle.yml), [MongoDB](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-mongodb.yml),
|
||||
[DM](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-dm.yml), [OpenGauss](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-opengauss.yml),
|
||||
[ClickHouse](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-clickhouse.yml), [IoTDB](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-iotdb.yml),
|
||||
[Redis Cluster](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-redis_cluster.yml), [Redis Sentinel](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-redis_sentinel.yml)
|
||||
[Doris BE](https://github.com/apache/hertzbeat/blob/master/hertzbeat-manager/src/main/resources/define/app-doris_be.yml), [Doris FE](https://github.com/apache/hertzbeat/blob/master/hertzbeat-manager/src/main/resources/define/app-doris_fe.yml),
|
||||
[Memcached](https://github.com/apache/hertzbeat/blob/master/hertzbeat-manager/src/main/resources/define/app-memcached.yml), [NebulaGraph](https://github.com/apache/hertzbeat/blob/master/hertzbeat-manager/src/main/resources/define/app-nebula_graph.yml)
|
||||
- [Linux](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-linux.yml), [Ubuntu](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ubuntu.yml),
|
||||
[CentOS](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-centos.yml), [Windows](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-windows.yml),
|
||||
[EulerOS](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-euleros.yml), [Fedora CoreOS](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-coreos.yml),
|
||||
[OpenSUSE](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-opensuse.yml), [Rocky Linux](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-rockylinux.yml),
|
||||
[Red Hat](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-redhat.yml), [FreeBSD](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-freebsd.yml),
|
||||
[AlmaLinux](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-almalinux.yml), [Debian Linux](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-debian.yml)
|
||||
- [Tomcat](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-tomcat.yml), [Nacos](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-nacos.yml),
|
||||
[Zookeeper](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-zookeeper.yml), [RabbitMQ](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-rabbitmq.yml),
|
||||
[Flink](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-flink.yml), [Kafka](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-kafka.yml),
|
||||
[ShenYu](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-shenyu.yml), [DynamicTp](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-dynamic_tp.yml),
|
||||
[Jetty](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-jetty.yml), [ActiveMQ](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-activemq.yml),
|
||||
[Spring Gateway](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-spring_gateway.yml), [EMQX MQTT](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-emqx.yml),
|
||||
[AirFlow](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-airflow.yml), [Hive](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-hive.yml),
|
||||
[Spark](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-spark.yml), [Hadoop](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-hadoop.yml)
|
||||
- [Kubernetes](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-kubernetes.yml), [Docker](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-docker.yml)
|
||||
- [CiscoSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-cisco_switch.yml), [HpeSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-hpe_switch.yml),
|
||||
[HuaweiSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-huawei_switch.yml), [TpLinkSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-tplink_switch.yml),
|
||||
[H3cSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-h3c_switch.yml)
|
||||
- 和更多自定义指标模板。
|
||||
- 通知支持 `Discord` `Slack` `Telegram` `邮件` `钉钉` `微信` `飞书` `短信` `Webhook` `Server酱`。
|
||||
|
||||
## 🐕 快速开始
|
||||
|
||||
- 如果您是想将 HertzBeat 部署到内网环境搭建监控系统,请参考下面的部署文档进行操作。
|
||||
|
||||
### 🍞 HertzBeat 安装
|
||||
> HertzBeat 支持通过源码安装启动,Docker 容器运行和安装包方式安装部署,CPU 架构支持 x86/arm64。
|
||||
|
||||
##### 方式一:Docker 方式快速安装
|
||||
##### 方式一:Docker 方式快速安装
|
||||
|
||||
1. `docker` 环境仅需一条命令即可开始
|
||||
1. `docker` 环境仅需一条命令即可开始
|
||||
|
||||
```shell
|
||||
docker run -d -p 1157:1157 -p 1158:1158 --name hertzbeat apache/hertzbeat
|
||||
@@ -82,12 +127,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:
|
||||
@@ -100,9 +145,7 @@
|
||||
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)
|
||||
@@ -110,8 +153,8 @@
|
||||
##### 方式三:本地代码启动
|
||||
|
||||
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`
|
||||
2. 后端:需要 `maven3+`, `java17` 和 `lombok` 环境,修改 `YML` 配置信息,添加JVM参数`--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED`后启动 `hertzbeat-startup` 服务即可。
|
||||
3. 前端:需要 `nodejs npm angular-cli`环境,待本地后端启动后,在 `web-app` 目录下启动 `ng serve --open`
|
||||
4. 浏览器访问 `http://localhost:4200` 即可开始,默认账号密码 `admin/hertzbeat`
|
||||
|
||||
详细步骤参考 [参与贡献之本地代码启动](CONTRIBUTING.md)
|
||||
@@ -120,7 +163,7 @@
|
||||
|
||||
通过 [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)
|
||||
|
||||
##### 方式五:Kubernetes Helm Charts 部署 hertzbeat+collector+postgresql+tsdb
|
||||
|
||||
@@ -132,8 +175,8 @@
|
||||
|
||||
## ✨ 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 -->
|
||||
@@ -479,7 +522,7 @@ Thanks these wonderful people, welcome to join us:
|
||||
<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/Saramanda9988"><img src="https://avatars.githubusercontent.com/u/176664901?v=4?s=100" width="100px;" alt="LunaRain_079"/><br /><sub><b>LunaRain_079</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/delei"><img src="https://avatars.githubusercontent.com/u/17263766?v=4?s=100" width="100px;" alt="DeleiGuo"/><br /><sub><b>DeleiGuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Adelei" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -497,41 +540,6 @@ Thanks these wonderful people, welcome to join us:
|
||||
<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>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/orangeCatDeveloper"><img src="https://avatars.githubusercontent.com/u/95899648?v=4?s=100" width="100px;" alt="NekoPunch"/><br /><sub><b>NekoPunch</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=orangeCatDeveloper" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=orangeCatDeveloper" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://wy471x.github.io/"><img src="https://avatars.githubusercontent.com/u/52033069?v=4?s=100" width="100px;" alt="wy471x"/><br /><sub><b>wy471x</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wy471x" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hengyuss"><img src="https://avatars.githubusercontent.com/u/81064732?v=4?s=100" width="100px;" alt="hengyuss"/><br /><sub><b>hengyuss</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hengyuss" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://moduvoice.com/"><img src="https://avatars.githubusercontent.com/u/291867022?v=4?s=100" width="100px;" alt="moduvoice"/><br /><sub><b>moduvoice</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=moduvoice" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hutiefang76"><img src="https://avatars.githubusercontent.com/u/137664623?v=4?s=100" width="100px;" alt="hutiefang76"/><br /><sub><b>hutiefang76</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hutiefang76" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://zylatent.com/"><img src="https://avatars.githubusercontent.com/u/250777154?v=4?s=100" width="100px;" alt="柳含知 Liu Hanzhi"/><br /><sub><b>柳含知 Liu Hanzhi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ZhouYinLong-lab" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wilmerdooley"><img src="https://avatars.githubusercontent.com/u/259930736?v=4?s=100" width="100px;" alt="wilmerdooley"/><br /><sub><b>wilmerdooley</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wilmerdooley" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Zmjjeff7"><img src="https://avatars.githubusercontent.com/u/175370943?v=4?s=100" width="100px;" alt="Zhenhong Guo"/><br /><sub><b>Zhenhong Guo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Zmjjeff7" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/abhyudayareddy"><img src="https://avatars.githubusercontent.com/u/54602866?v=4?s=100" width="100px;" alt="abhyudayareddy"/><br /><sub><b>abhyudayareddy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=abhyudayareddy" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/neon-hippo"><img src="https://avatars.githubusercontent.com/u/165560498?v=4?s=100" width="100px;" alt="neon-hippo"/><br /><sub><b>neon-hippo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=neon-hippo" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/P-Peaceful"><img src="https://avatars.githubusercontent.com/u/52856161?v=4?s=100" width="100px;" alt="P_Peaceful"/><br /><sub><b>P_Peaceful</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=P-Peaceful" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=P-Peaceful" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zhusaidong"><img src="https://avatars.githubusercontent.com/u/3039961?v=4?s=100" width="100px;" alt="zhusaidong"/><br /><sub><b>zhusaidong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhusaidong" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zhehenlu"><img src="https://avatars.githubusercontent.com/u/31504542?v=4?s=100" width="100px;" alt="zhlu"/><br /><sub><b>zhlu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhehenlu" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=zhehenlu" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/brettgervasoni"><img src="https://avatars.githubusercontent.com/u/34056000?v=4?s=100" width="100px;" alt="brettgervasoni"/><br /><sub><b>brettgervasoni</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=brettgervasoni" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Darshan-paul"><img src="https://avatars.githubusercontent.com/u/211450705?v=4?s=100" width="100px;" alt="Darshan-paul"/><br /><sub><b>Darshan-paul</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Darshan-paul" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/04cb"><img src="https://avatars.githubusercontent.com/u/111667698?v=4?s=100" width="100px;" alt="layla"/><br /><sub><b>layla</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=04cb" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/miantalha45"><img src="https://avatars.githubusercontent.com/u/155809113?v=4?s=100" width="100px;" alt="Talha Amjad"/><br /><sub><b>Talha Amjad</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=miantalha45" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://turanalmammadov.com/"><img src="https://avatars.githubusercontent.com/u/16321061?v=4?s=100" width="100px;" alt="Turan Almammadov"/><br /><sub><b>Turan Almammadov</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=turanalmammadov" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=turanalmammadov" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zhaoyangplus"><img src="https://avatars.githubusercontent.com/u/245090302?v=4?s=100" width="100px;" alt="zhaoyangplus"/><br /><sub><b>zhaoyangplus</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhaoyangplus" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yexuanyang"><img src="https://avatars.githubusercontent.com/u/73885401?v=4?s=100" width="100px;" alt="Yang Yexuan"/><br /><sub><b>Yang Yexuan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yexuanyang" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/markguo123"><img src="https://avatars.githubusercontent.com/u/155072651?v=4?s=100" width="100px;" alt="markguo123"/><br /><sub><b>markguo123</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=markguo123" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/leo-934"><img src="https://avatars.githubusercontent.com/u/55838224?v=4?s=100" width="100px;" alt="leo"/><br /><sub><b>leo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=leo-934" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=leo-934" title="Documentation">📖</a> <a href="#blog-leo-934" title="Blogposts">📝</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -539,11 +547,11 @@ Thanks these wonderful people, welcome to join us:
|
||||
<!-- markdownlint-restore -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
## 💬 社区交流
|
||||
|
||||
##### Channel
|
||||
##### Channel
|
||||
|
||||
[订阅邮件列表](https://lists.apache.org/list.html?dev@hertzbeat.apache.org) : 发送邮件至 `dev-subscribe@hertzbeat.apache.org` 来订阅邮件列表.
|
||||
|
||||
@@ -553,8 +561,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)
|
||||
|
||||
+76
-64
@@ -7,9 +7,10 @@
|
||||
<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>
|
||||
|
||||
[](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,13 +20,13 @@
|
||||
[](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) は AI 駆動の次世代オープンソースリアルタイム観測システムです。メトリクスとログの統一収集、アラートの一元配信、インテリジェント管理分析。エージェント不要、高性能クラスタ、強力なカスタム監視とステータスページ構築機能を提供します。
|
||||
|
||||
### 特性
|
||||
|
||||
@@ -38,17 +39,61 @@
|
||||
- 高性能で、コレクタークラスタの水平拡張、マルチ分離ネットワーク監視、クラウドエッジ協調をサポート。
|
||||
- 強力なステータスページ構築機能を提供し、サービスのリアルタイムステータスをユーザーに簡単に伝達。
|
||||
|
||||
> `HertzBeat`の統一プラットフォーム、AI インテリジェンス、強力なカスタマイズ、多種類サポート、高性能、容易な拡張性は、ユーザーが迅速かつ便利に観測要件を実現することを支援することを目的としています。
|
||||
> `HertzBeat`の統一プラットフォーム、AI インテリジェンス、強力なカスタマイズ、多種類サポート、高性能、容易な拡張性は、ユーザーが迅速かつ便利に観測要件を実現することを支援することを目的としています。
|
||||
|
||||
----
|
||||
|
||||
----
|
||||
|
||||
## 🥐 モジュール
|
||||
## 🥐 モジュール
|
||||
|
||||

|
||||

|
||||
|
||||
## 🐕 クイックスタート
|
||||
## ⛄ 利用可能
|
||||
|
||||
> `mysql`、`Java仮想マシン`、`k8s` などのすべての監視コレクション タイプを `YML` 監視テンプレートとして定義し、ユーザーがそれらをインポートして対応するタイプの監視をサポートできるようにしています。
|
||||
> カスタマイズされた一般的なモニターのテンプレートを提供することを歓迎します。
|
||||
|
||||
- [Website](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-website.yml), [Port Telnet](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-port.yml),
|
||||
[Http Api](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-api.yml), [Ping Connect](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ping.yml),
|
||||
[Jvm](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-jvm.yml), [SiteMap](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-fullsite.yml),
|
||||
[Ssl Certificate](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ssl_cert.yml), [SpringBoot2](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-springboot2.yml),
|
||||
[FTP Server](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ftp.yml), [SpringBoot3](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-springboot3.yml),
|
||||
[Udp Port](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-udp_port.yml), [Dns](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-dns.yml),
|
||||
[Pop3](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-pop3.yml), [Ntp](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ntp.yml),
|
||||
[Api Code](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-api_code.yml), [Smtp](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-smtp.yml),
|
||||
[Nginx](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-nginx.yml)
|
||||
- [Mysql](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-mysql.yml), [PostgreSQL](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-postgresql.yml),
|
||||
[MariaDB](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-mariadb.yml), [Redis](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-redis.yml),
|
||||
[ElasticSearch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-elasticsearch.yml), [SqlServer](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-sqlserver.yml),
|
||||
[Oracle](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-oracle.yml), [MongoDB](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-mongodb.yml),
|
||||
[DM](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-dm.yml), [OpenGauss](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-opengauss.yml),
|
||||
[ClickHouse](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-clickhouse.yml), [IoTDB](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-iotdb.yml),
|
||||
[Redis Cluster](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-redis_cluster.yml), [Redis Sentinel](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-redis_sentinel.yml)
|
||||
[Doris BE](https://github.com/apache/hertzbeat/blob/master/hertzbeat-manager/src/main/resources/define/app-doris_be.yml), [Doris FE](https://github.com/apache/hertzbeat/blob/master/hertzbeat-manager/src/main/resources/define/app-doris_fe.yml),
|
||||
[Memcached](https://github.com/apache/hertzbeat/blob/master/hertzbeat-manager/src/main/resources/define/app-memcached.yml), [NebulaGraph](https://github.com/apache/hertzbeat/blob/master/hertzbeat-manager/src/main/resources/define/app-nebula_graph.yml)
|
||||
- [Linux](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-linux.yml), [Ubuntu](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ubuntu.yml),
|
||||
[CentOS](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-centos.yml), [Windows](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-windows.yml),
|
||||
[EulerOS](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-euleros.yml), [Fedora CoreOS](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-coreos.yml),
|
||||
[OpenSUSE](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-opensuse.yml), [Rocky Linux](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-rockylinux.yml),
|
||||
[Red Hat](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-redhat.yml), [FreeBSD](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-freebsd.yml),
|
||||
[AlmaLinux](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-almalinux.yml), [Debian Linux](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-debian.yml)
|
||||
- [Tomcat](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-tomcat.yml), [Nacos](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-nacos.yml),
|
||||
[Zookeeper](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-zookeeper.yml), [RabbitMQ](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-rabbitmq.yml),
|
||||
[Flink](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-flink.yml), [Kafka](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-kafka.yml),
|
||||
[ShenYu](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-shenyu.yml), [DynamicTp](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-dynamic_tp.yml),
|
||||
[Jetty](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-jetty.yml), [ActiveMQ](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-activemq.yml),
|
||||
[Spring Gateway](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-spring_gateway.yml), [EMQX MQTT](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-emqx.yml),
|
||||
[AirFlow](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-airflow.yml), [Hive](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-hive.yml),
|
||||
[Spark](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-spark.yml), [Hadoop](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-hadoop.yml)
|
||||
- [Kubernetes](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-kubernetes.yml), [Docker](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-docker.yml)
|
||||
- [CiscoSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-cisco_switch.yml), [HpeSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-hpe_switch.yml),
|
||||
[HuaweiSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-huawei_switch.yml), [TpLinkSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-tplink_switch.yml),
|
||||
[H3cSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-h3c_switch.yml)
|
||||
- その他、カスタマイズされたモニターテンプレート。
|
||||
- 通知利用可能 `Discord` `Slack` `Telegram` `Email` `Dingtalk` `WeChat` `FeiShu` `Webhook` `SMS` `ServerChan`。
|
||||
|
||||
## 🐕 クイックスタート
|
||||
|
||||
- HertzBeat をローカルに展開する場合は、以下のデプロイメントドキュメントを参照してください。
|
||||
|
||||
@@ -57,13 +102,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. コレクタークラスタのデプロイメント(オプション)
|
||||
|
||||
@@ -77,17 +122,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:
|
||||
@@ -103,27 +148,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` を実行します。
|
||||
2. バックエンド:`maven3+`、`java17`と`lombok`の環境は必要です。`YML` 設定を修正し、Java仮想マシンパラメータに`--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED` を追加し、`hertzbeat-startup` を起動します。
|
||||
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
|
||||
|
||||
@@ -135,8 +178,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 -->
|
||||
@@ -482,7 +525,7 @@ Thanks these wonderful people, welcome to join us:
|
||||
<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/Saramanda9988"><img src="https://avatars.githubusercontent.com/u/176664901?v=4?s=100" width="100px;" alt="LunaRain_079"/><br /><sub><b>LunaRain_079</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Saramanda9988" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/delei"><img src="https://avatars.githubusercontent.com/u/17263766?v=4?s=100" width="100px;" alt="DeleiGuo"/><br /><sub><b>DeleiGuo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Documentation">📖</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=delei" title="Tests">⚠️</a> <a href="https://github.com/apache/hertzbeat/issues?q=author%3Adelei" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -500,41 +543,6 @@ Thanks these wonderful people, welcome to join us:
|
||||
<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>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/orangeCatDeveloper"><img src="https://avatars.githubusercontent.com/u/95899648?v=4?s=100" width="100px;" alt="NekoPunch"/><br /><sub><b>NekoPunch</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=orangeCatDeveloper" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=orangeCatDeveloper" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://wy471x.github.io/"><img src="https://avatars.githubusercontent.com/u/52033069?v=4?s=100" width="100px;" alt="wy471x"/><br /><sub><b>wy471x</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wy471x" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hengyuss"><img src="https://avatars.githubusercontent.com/u/81064732?v=4?s=100" width="100px;" alt="hengyuss"/><br /><sub><b>hengyuss</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hengyuss" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://moduvoice.com/"><img src="https://avatars.githubusercontent.com/u/291867022?v=4?s=100" width="100px;" alt="moduvoice"/><br /><sub><b>moduvoice</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=moduvoice" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hutiefang76"><img src="https://avatars.githubusercontent.com/u/137664623?v=4?s=100" width="100px;" alt="hutiefang76"/><br /><sub><b>hutiefang76</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=hutiefang76" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://zylatent.com/"><img src="https://avatars.githubusercontent.com/u/250777154?v=4?s=100" width="100px;" alt="柳含知 Liu Hanzhi"/><br /><sub><b>柳含知 Liu Hanzhi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ZhouYinLong-lab" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wilmerdooley"><img src="https://avatars.githubusercontent.com/u/259930736?v=4?s=100" width="100px;" alt="wilmerdooley"/><br /><sub><b>wilmerdooley</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wilmerdooley" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Zmjjeff7"><img src="https://avatars.githubusercontent.com/u/175370943?v=4?s=100" width="100px;" alt="Zhenhong Guo"/><br /><sub><b>Zhenhong Guo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Zmjjeff7" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/abhyudayareddy"><img src="https://avatars.githubusercontent.com/u/54602866?v=4?s=100" width="100px;" alt="abhyudayareddy"/><br /><sub><b>abhyudayareddy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=abhyudayareddy" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/neon-hippo"><img src="https://avatars.githubusercontent.com/u/165560498?v=4?s=100" width="100px;" alt="neon-hippo"/><br /><sub><b>neon-hippo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=neon-hippo" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/P-Peaceful"><img src="https://avatars.githubusercontent.com/u/52856161?v=4?s=100" width="100px;" alt="P_Peaceful"/><br /><sub><b>P_Peaceful</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=P-Peaceful" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=P-Peaceful" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zhusaidong"><img src="https://avatars.githubusercontent.com/u/3039961?v=4?s=100" width="100px;" alt="zhusaidong"/><br /><sub><b>zhusaidong</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhusaidong" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zhehenlu"><img src="https://avatars.githubusercontent.com/u/31504542?v=4?s=100" width="100px;" alt="zhlu"/><br /><sub><b>zhlu</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhehenlu" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=zhehenlu" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/brettgervasoni"><img src="https://avatars.githubusercontent.com/u/34056000?v=4?s=100" width="100px;" alt="brettgervasoni"/><br /><sub><b>brettgervasoni</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=brettgervasoni" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Darshan-paul"><img src="https://avatars.githubusercontent.com/u/211450705?v=4?s=100" width="100px;" alt="Darshan-paul"/><br /><sub><b>Darshan-paul</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Darshan-paul" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/04cb"><img src="https://avatars.githubusercontent.com/u/111667698?v=4?s=100" width="100px;" alt="layla"/><br /><sub><b>layla</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=04cb" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/miantalha45"><img src="https://avatars.githubusercontent.com/u/155809113?v=4?s=100" width="100px;" alt="Talha Amjad"/><br /><sub><b>Talha Amjad</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=miantalha45" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://turanalmammadov.com/"><img src="https://avatars.githubusercontent.com/u/16321061?v=4?s=100" width="100px;" alt="Turan Almammadov"/><br /><sub><b>Turan Almammadov</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=turanalmammadov" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=turanalmammadov" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zhaoyangplus"><img src="https://avatars.githubusercontent.com/u/245090302?v=4?s=100" width="100px;" alt="zhaoyangplus"/><br /><sub><b>zhaoyangplus</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=zhaoyangplus" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yexuanyang"><img src="https://avatars.githubusercontent.com/u/73885401?v=4?s=100" width="100px;" alt="Yang Yexuan"/><br /><sub><b>Yang Yexuan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yexuanyang" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/markguo123"><img src="https://avatars.githubusercontent.com/u/155072651?v=4?s=100" width="100px;" alt="markguo123"/><br /><sub><b>markguo123</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=markguo123" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/leo-934"><img src="https://avatars.githubusercontent.com/u/55838224?v=4?s=100" width="100px;" alt="leo"/><br /><sub><b>leo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=leo-934" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=leo-934" title="Documentation">📖</a> <a href="#blog-leo-934" title="Blogposts">📝</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -542,7 +550,7 @@ Thanks these wonderful people, welcome to join us:
|
||||
<!-- markdownlint-restore -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
## 💬 コミュニティ交流
|
||||
|
||||
@@ -556,8 +564,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`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
<artifactId>hertzbeat-ai</artifactId>
|
||||
<version>${hertzbeat.version}</version>
|
||||
<properties>
|
||||
<spring-ai.version>2.0.0</spring-ai.version>
|
||||
<java.version>25</java.version>
|
||||
<spring-ai.version>1.1.1</spring-ai.version>
|
||||
<java.version>17</java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -59,7 +59,7 @@
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.hertzbeat</groupId>
|
||||
<artifactId>hertzbeat-common-spring</artifactId>
|
||||
<artifactId>hertzbeat-common</artifactId>
|
||||
</dependency>
|
||||
<!-- common -->
|
||||
<dependency>
|
||||
@@ -78,7 +78,6 @@
|
||||
<groupId>com.usthe.sureness</groupId>
|
||||
<artifactId>spring-boot3-starter-sureness</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.apache.hertzbeat.common.util.JsonUtil;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.openai.OpenAiChatModel;
|
||||
import org.springframework.ai.openai.OpenAiChatOptions;
|
||||
import org.springframework.ai.openai.api.OpenAiApi;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
@@ -100,17 +101,21 @@ public class LlmConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// Create Chat Options with baseUrl and apiKey
|
||||
OpenAiApi.Builder builder = new OpenAiApi.Builder();
|
||||
builder.baseUrl(modelProviderConfig.getBaseUrl());
|
||||
builder.apiKey(modelProviderConfig.getApiKey());
|
||||
builder.completionsPath("/chat/completions");
|
||||
|
||||
// Create Chat Options
|
||||
OpenAiChatOptions openAiChatOptions = OpenAiChatOptions.builder()
|
||||
.baseUrl(modelProviderConfig.getBaseUrl())
|
||||
.apiKey(modelProviderConfig.getApiKey())
|
||||
.model(modelProviderConfig.getModel())
|
||||
.temperature(0.3)
|
||||
.build();
|
||||
|
||||
|
||||
// Create Chat Model
|
||||
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
|
||||
.options(openAiChatOptions)
|
||||
.openAiApi(builder.build())
|
||||
.defaultOptions(openAiChatOptions)
|
||||
.build();
|
||||
|
||||
// Create and return ChatClient
|
||||
|
||||
@@ -18,21 +18,14 @@
|
||||
package org.apache.hertzbeat.ai.config;
|
||||
|
||||
import com.usthe.sureness.subject.SubjectSum;
|
||||
import com.usthe.sureness.util.SurenessContextHolder;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
import org.springframework.ai.chat.model.ToolContext;
|
||||
import org.springframework.core.NamedThreadLocal;
|
||||
import org.springframework.core.NamedInheritableThreadLocal;
|
||||
|
||||
/**
|
||||
* Context holder for AI agent security context.
|
||||
*/
|
||||
public final class McpContextHolder {
|
||||
|
||||
static final String SUBJECT_CONTEXT_KEY = McpContextHolder.class.getName() + ".subject";
|
||||
|
||||
private static final ThreadLocal<SubjectSum> subjectHolder =
|
||||
new NamedThreadLocal<>("MCP Security and User Identification Context");
|
||||
new NamedInheritableThreadLocal<>("MCP Security and User Identification Context");
|
||||
|
||||
private McpContextHolder() {}
|
||||
|
||||
@@ -51,73 +44,10 @@ public final class McpContextHolder {
|
||||
return subjectHolder.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a security context that is propagated only through the Spring AI tool invocation chain.
|
||||
*
|
||||
* @param subject current authenticated subject, may be {@code null}
|
||||
* @return tool context without null values
|
||||
*/
|
||||
public static Map<String, Object> createToolContext(SubjectSum subject) {
|
||||
return subject == null ? Map.of() : Map.of(SUBJECT_CONTEXT_KEY, subject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves and validates the authenticated subject from the Spring AI tool context.
|
||||
*
|
||||
* @param toolContext Spring AI tool context
|
||||
* @return authenticated subject, or {@code null} when absent
|
||||
*/
|
||||
public static SubjectSum getSubject(ToolContext toolContext) {
|
||||
if (toolContext == null) {
|
||||
return null;
|
||||
}
|
||||
Object subject = toolContext.getContext().get(SUBJECT_CONTEXT_KEY);
|
||||
return subject instanceof SubjectSum subjectSum ? subjectSum : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a synchronous tool invocation within the given subject scope and restores the original thread
|
||||
* contexts afterward.
|
||||
*
|
||||
* <p>Spring AI tool callbacks may run on pooled threads, so they cannot rely on a {@link ThreadLocal} left on
|
||||
* the Servlet request thread. This method binds both MCP and Sureness contexts so the tool and downstream
|
||||
* services observe the same user identity.</p>
|
||||
*
|
||||
* @param subject current request subject, may be {@code null}
|
||||
* @param operation synchronous tool invocation
|
||||
* @param <T> tool result type
|
||||
* @return tool invocation result
|
||||
*/
|
||||
public static <T> T callWithSubject(SubjectSum subject, Supplier<T> operation) {
|
||||
SubjectSum previousMcpSubject = getSubject();
|
||||
SubjectSum previousSurenessSubject = SurenessContextHolder.getBindSubject();
|
||||
replaceSubjects(subject);
|
||||
try {
|
||||
return operation.get();
|
||||
} finally {
|
||||
replaceSubjects(previousMcpSubject, previousSurenessSubject);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the context from the thread to prevent memory leaks.
|
||||
*/
|
||||
public static void clear() {
|
||||
subjectHolder.remove();
|
||||
}
|
||||
|
||||
private static void replaceSubjects(SubjectSum subject) {
|
||||
replaceSubjects(subject, subject);
|
||||
}
|
||||
|
||||
private static void replaceSubjects(SubjectSum mcpSubject, SubjectSum surenessSubject) {
|
||||
clear();
|
||||
SurenessContextHolder.unbindSubject();
|
||||
if (mcpSubject != null) {
|
||||
setSubject(mcpSubject);
|
||||
}
|
||||
if (surenessSubject != null) {
|
||||
SurenessContextHolder.bindSubject(surenessSubject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-60
@@ -1,60 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.config;
|
||||
|
||||
import org.springframework.ai.chat.model.ToolContext;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.definition.ToolDefinition;
|
||||
import org.springframework.ai.tool.metadata.ToolMetadata;
|
||||
|
||||
/**
|
||||
* Establishes a request-scoped security context for Spring AI tool callbacks.
|
||||
*
|
||||
* <p>The wrapper keeps the tool definition and metadata unchanged. It binds the identity only for the delegated
|
||||
* call and restores the worker thread's original state after both successful and failed invocations.</p>
|
||||
*/
|
||||
public final class SecurityContextToolCallback implements ToolCallback {
|
||||
|
||||
private final ToolCallback delegate;
|
||||
|
||||
public SecurityContextToolCallback(ToolCallback delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ToolDefinition getToolDefinition() {
|
||||
return delegate.getToolDefinition();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ToolMetadata getToolMetadata() {
|
||||
return delegate.getToolMetadata();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String call(String toolInput) {
|
||||
return call(toolInput, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String call(String toolInput, ToolContext toolContext) {
|
||||
return McpContextHolder.callWithSubject(
|
||||
McpContextHolder.getSubject(toolContext),
|
||||
() -> delegate.call(toolInput, toolContext));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -17,13 +17,15 @@
|
||||
|
||||
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;
|
||||
@@ -39,7 +41,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import javax.validation.Valid;
|
||||
import java.util.List;
|
||||
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
|
||||
@@ -72,14 +74,16 @@ public class ChatController {
|
||||
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();
|
||||
.conversationId(context.getConversationId())
|
||||
.response("Error: Message cannot be empty")
|
||||
.build();
|
||||
return Flux.just(ServerSentEvent.builder(errorResponse)
|
||||
.event("error")
|
||||
.build());
|
||||
.event("error")
|
||||
.build());
|
||||
}
|
||||
|
||||
log.info("Received streaming chat request for conversation: {}", context.getConversationId());
|
||||
@@ -88,12 +92,12 @@ public class ChatController {
|
||||
} 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();
|
||||
.conversationId(context.getConversationId())
|
||||
.response("An error occurred: " + e.getMessage())
|
||||
.build();
|
||||
return Flux.just(ServerSentEvent.builder(errorResponse)
|
||||
.event("error")
|
||||
.build());
|
||||
.event("error")
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +134,7 @@ public class ChatController {
|
||||
@GetMapping(path = "/conversations/{conversationId}")
|
||||
@Operation(summary = "Get conversation history", description = "Get detailed information and message history for a specific conversation")
|
||||
public ResponseEntity<Message<ChatConversation>> getConversation(
|
||||
@Parameter(description = "Conversation ID", example = "12345678") @PathVariable(value = "conversationId") Long conversationId) {
|
||||
@Parameter(description = "Conversation ID", example = "12345678") @PathVariable(value = "conversationId") Long conversationId) {
|
||||
ChatConversation conversation = conversationService.getConversation(conversationId);
|
||||
return ResponseEntity.ok(Message.success(conversation));
|
||||
}
|
||||
@@ -144,21 +148,8 @@ public class ChatController {
|
||||
@DeleteMapping(path = "/conversations/{conversationId}")
|
||||
@Operation(summary = "Delete conversation", description = "Delete a specific conversation and all its messages")
|
||||
public ResponseEntity<Message<Void>> deleteConversation(
|
||||
@Parameter(description = "Conversation ID", example = "2345678") @PathVariable("conversationId") Long conversationId) {
|
||||
@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,181 +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.HttpStatus;
|
||||
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.status(HttpStatus.NOT_FOUND).body(errorResult);
|
||||
}
|
||||
|
||||
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,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);
|
||||
}
|
||||
+2
-10
@@ -18,13 +18,12 @@
|
||||
|
||||
package org.apache.hertzbeat.ai.pojo.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.usthe.sureness.subject.SubjectSum;
|
||||
import java.util.List;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.hertzbeat.common.entity.ai.ChatMessage;
|
||||
|
||||
/**
|
||||
@@ -48,11 +47,4 @@ public class ChatRequestContext {
|
||||
* Conversation history messages for context
|
||||
*/
|
||||
private List<ChatMessage> conversationHistory;
|
||||
|
||||
/**
|
||||
* Authenticated subject captured by the server. It cannot be supplied through client JSON and is never included
|
||||
* in model messages.
|
||||
*/
|
||||
@JsonIgnore
|
||||
private SubjectSum subject;
|
||||
}
|
||||
|
||||
@@ -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.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;
|
||||
import tools.jackson.core.type.TypeReference;
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
try {
|
||||
executeSchedule(schedule);
|
||||
} catch (Exception e) {
|
||||
log.error("Unexpected error processing scheduled SOP {}", schedule.getId(), e);
|
||||
}
|
||||
}
|
||||
} 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) {
|
||||
// Do not silently skip an invalid schedule because its execution time will still be advanced.
|
||||
throw new IllegalStateException("SOP skill not found: " + schedule.getSopName());
|
||||
}
|
||||
|
||||
// Parse parameters
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
if (schedule.getSopParams() != null && !schedule.getSopParams().isEmpty()) {
|
||||
Map<String, Object> parsedParams = JsonUtil.fromJson(
|
||||
schedule.getSopParams(), new TypeReference<>() {});
|
||||
if (parsedParams == null) {
|
||||
throw new IllegalArgumentException("SOP schedule parameters must be a valid JSON object");
|
||||
}
|
||||
params = parsedParams;
|
||||
}
|
||||
|
||||
// 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
-11
@@ -19,7 +19,6 @@
|
||||
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;
|
||||
@@ -34,7 +33,7 @@ public interface ConversationService {
|
||||
/**
|
||||
* Send a message and receive a streaming response
|
||||
*
|
||||
* @param message The user's message
|
||||
* @param message The user's message
|
||||
* @param conversationId Optional conversation ID for continuing a chat
|
||||
* @return Flux of ServerSentEvent for streaming the response
|
||||
*/
|
||||
@@ -68,13 +67,4 @@ public interface ConversationService {
|
||||
* @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,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);
|
||||
}
|
||||
+20
-146
@@ -18,28 +18,15 @@
|
||||
|
||||
package org.apache.hertzbeat.ai.service.impl;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.config.McpContextHolder;
|
||||
import org.apache.hertzbeat.ai.config.SecurityContextToolCallback;
|
||||
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;
|
||||
@@ -48,59 +35,38 @@ 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.ai.tool.ToolCallback;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.util.StringUtils;
|
||||
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.
|
||||
* 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 volatile ModelProviderConfig modelProviderConfig;
|
||||
|
||||
|
||||
private final SkillRegistry skillRegistry;
|
||||
|
||||
|
||||
@Autowired
|
||||
@Qualifier("hertzbeatTools")
|
||||
private ToolCallbackProvider toolCallbackProvider;
|
||||
|
||||
private volatile boolean configured;
|
||||
|
||||
private volatile boolean configurationLoaded;
|
||||
|
||||
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) {
|
||||
public ChatClientProviderServiceImpl(ApplicationContext applicationContext, GeneralConfigDao generalConfigDao) {
|
||||
this.applicationContext = applicationContext;
|
||||
this.generalConfigDao = generalConfigDao;
|
||||
this.skillRegistry = skillRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -108,7 +74,7 @@ public class ChatClientProviderServiceImpl implements ChatClientProviderService
|
||||
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
|
||||
@@ -126,22 +92,14 @@ public class ChatClientProviderServiceImpl implements ChatClientProviderService
|
||||
|
||||
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());
|
||||
ToolCallback[] toolCallbacks = Arrays.stream(toolCallbackProvider.getToolCallbacks())
|
||||
.map(SecurityContextToolCallback::new)
|
||||
.toArray(ToolCallback[]::new);
|
||||
|
||||
return chatClient.prompt()
|
||||
.messages(messages)
|
||||
.system(systemPrompt)
|
||||
.tools((Object[]) toolCallbacks)
|
||||
.toolContext(McpContextHolder.createToolContext(context.getSubject()))
|
||||
.stream()
|
||||
.content()
|
||||
.doOnComplete(() -> log.info("Streaming completed for conversation: {}", context.getConversationId()))
|
||||
.doOnError(error -> log.error("Error in streaming chat: {}", error.getMessage(), error));
|
||||
.messages(messages)
|
||||
.system(SystemPromptTemplate.builder().resource(systemResource).build().getTemplate())
|
||||
.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);
|
||||
@@ -149,97 +107,13 @@ public class ChatClientProviderServiceImpl implements ChatClientProviderService
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
ModelProviderConfig currentConfig = modelProviderConfig;
|
||||
if (currentConfig != null && Objects.equals(currentConfig.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) {
|
||||
refreshProviderConfiguration();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConfigured() {
|
||||
if (!configurationLoaded) {
|
||||
synchronized (this) {
|
||||
if (!configurationLoaded) {
|
||||
refreshProviderConfiguration();
|
||||
}
|
||||
}
|
||||
if (!isConfigured) {
|
||||
GeneralConfig providerConfig = generalConfigDao.findByType("provider");
|
||||
ModelProviderConfig modelProviderConfig = JsonUtil.fromJson(providerConfig.getContent(), ModelProviderConfig.class);
|
||||
isConfigured = modelProviderConfig != null && modelProviderConfig.getApiKey() != null;
|
||||
}
|
||||
return configured;
|
||||
}
|
||||
|
||||
/**
|
||||
* Atomically refreshes the configuration snapshot after enabling, disabling, or switching providers.
|
||||
*/
|
||||
private synchronized void refreshProviderConfiguration() {
|
||||
GeneralConfig providerConfig = generalConfigDao.findByType("provider");
|
||||
ModelProviderConfig refreshedConfig = null;
|
||||
if (providerConfig != null && StringUtils.hasText(providerConfig.getContent())) {
|
||||
refreshedConfig = JsonUtil.fromJson(providerConfig.getContent(), ModelProviderConfig.class);
|
||||
}
|
||||
modelProviderConfig = refreshedConfig;
|
||||
configured = refreshedConfig != null && StringUtils.hasText(refreshedConfig.getApiKey());
|
||||
configurationLoaded = true;
|
||||
return isConfigured;
|
||||
}
|
||||
}
|
||||
|
||||
+61
-99
@@ -17,26 +17,21 @@
|
||||
|
||||
package org.apache.hertzbeat.ai.service.impl;
|
||||
|
||||
import com.usthe.sureness.subject.SubjectSum;
|
||||
import com.usthe.sureness.util.SurenessContextHolder;
|
||||
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.dao.SopScheduleDao;
|
||||
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 org.springframework.util.CollectionUtils;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.Collections;
|
||||
@@ -58,9 +53,6 @@ public class ConversationServiceImpl implements ConversationService {
|
||||
@Autowired
|
||||
private ChatMessageDao messageDao;
|
||||
|
||||
@Autowired
|
||||
private SopScheduleDao sopScheduleDao;
|
||||
|
||||
@Autowired
|
||||
private ChatClientProviderService chatClientProviderService;
|
||||
|
||||
@@ -70,100 +62,90 @@ public class ConversationServiceImpl implements ConversationService {
|
||||
// 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();
|
||||
.conversationId(conversationId)
|
||||
.response("Provider is not configured. Please configure your AI Provider.")
|
||||
.build();
|
||||
return Flux.just(ServerSentEvent.builder(errorResponse)
|
||||
.event("error")
|
||||
.build());
|
||||
.event("error")
|
||||
.build());
|
||||
}
|
||||
|
||||
ChatConversation conversation;
|
||||
if (conversationId == null) {
|
||||
// The API contract makes conversationId optional, so create a conversation for the first message.
|
||||
conversation = new ChatConversation();
|
||||
conversation.setTitle(buildConversationTitle(message));
|
||||
conversation = conversationDao.save(conversation);
|
||||
} else {
|
||||
conversation = conversationDao.findById(conversationId)
|
||||
log.info("Starting streaming conversation: {}", conversationId);
|
||||
ChatConversation conversation = conversationDao.findById(conversationId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Conversation not found: " + conversationId));
|
||||
}
|
||||
Long currentConversationId = conversation.getId();
|
||||
log.info("Starting streaming conversation: {}", currentConversationId);
|
||||
|
||||
// Manually load messages for conversation history
|
||||
List<ChatMessage> messages = messageDao.findByConversationIdOrderByGmtCreateAsc(currentConversationId);
|
||||
List<ChatMessage> messages = messageDao.findByConversationIdOrderByGmtCreateAsc(conversationId);
|
||||
conversation.setMessages(messages);
|
||||
|
||||
if (conversation.getTitle().startsWith("conversation")) {
|
||||
// Auto-generate title from first user message
|
||||
conversation.setTitle(buildConversationTitle(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(currentConversationId)
|
||||
.conversationId(conversationId)
|
||||
.content(message)
|
||||
.role("user")
|
||||
.build();
|
||||
chatMessage = messageDao.save(chatMessage);
|
||||
|
||||
ChatRequestContext context = ChatRequestContext.builder()
|
||||
.message(message)
|
||||
.conversationId(currentConversationId)
|
||||
.conversationHistory(messages)
|
||||
.build();
|
||||
.message(message)
|
||||
.conversationId(conversationId)
|
||||
.conversationHistory(CollectionUtils.isEmpty(conversation.getMessages()) ? null
|
||||
: conversation.getMessages().subList(0, conversation.getMessages().size() - 1))
|
||||
.build();
|
||||
|
||||
// Stream response from AI service
|
||||
StringBuilder fullResponse = new StringBuilder();
|
||||
ChatMessage finalChatMessage = chatMessage;
|
||||
SubjectSum subject = SurenessContextHolder.getBindSubject();
|
||||
context.setSubject(subject);
|
||||
return chatClientProviderService.streamChat(context)
|
||||
.map(chunk -> {
|
||||
fullResponse.append(chunk);
|
||||
ChatResponseChunk responseChunk = ChatResponseChunk.builder()
|
||||
.conversationId(currentConversationId)
|
||||
.userMessageId(finalChatMessage.getId())
|
||||
.response(chunk)
|
||||
.build();
|
||||
.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(currentConversationId)
|
||||
.content(fullResponse.toString())
|
||||
.role("assistant")
|
||||
.build();
|
||||
assistantMessage = messageDao.save(assistantMessage);
|
||||
ChatResponseChunk finalResponse = ChatResponseChunk.builder()
|
||||
.conversationId(currentConversationId)
|
||||
.response("")
|
||||
.assistantMessageId(assistantMessage.getId())
|
||||
.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: {}", currentConversationId))
|
||||
.doOnError(error -> log.error("Error in streaming chat for conversation {}: {}", currentConversationId,
|
||||
error.getMessage(), error))
|
||||
.onErrorResume(error -> {
|
||||
ChatResponseChunk errorResponse = ChatResponseChunk.builder()
|
||||
.conversationId(currentConversationId)
|
||||
.response("An error occurred: " + error.getMessage())
|
||||
.userMessageId(finalChatMessage.getId())
|
||||
.build();
|
||||
return Flux.just(ServerSentEvent.builder(errorResponse)
|
||||
.event("error")
|
||||
.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
|
||||
@@ -173,10 +155,6 @@ public class ConversationServiceImpl implements ConversationService {
|
||||
return conversationDao.save(conversation);
|
||||
}
|
||||
|
||||
private String buildConversationTitle(String message) {
|
||||
return message.length() > 30 ? message.substring(0, 27) + "..." : message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatConversation getConversation(Long conversationId) {
|
||||
if (conversationId == null) {
|
||||
@@ -197,14 +175,13 @@ public class ConversationServiceImpl implements ConversationService {
|
||||
return conversations;
|
||||
}
|
||||
List<Long> conversationIds = conversations.stream()
|
||||
.map(ChatConversation::getId)
|
||||
.toList();
|
||||
.map(ChatConversation::getId)
|
||||
.toList();
|
||||
List<ChatMessage> allMessages = messageDao.findByConversationIdInOrderByGmtCreateAsc(conversationIds);
|
||||
Map<Long, List<ChatMessage>> messagesByConversationId = allMessages.stream()
|
||||
.collect(Collectors.groupingBy(ChatMessage::getConversationId));
|
||||
.collect(Collectors.groupingBy(ChatMessage::getConversationId));
|
||||
for (ChatConversation conversation : conversations) {
|
||||
List<ChatMessage> messages = messagesByConversationId.getOrDefault(conversation.getId(),
|
||||
Collections.emptyList());
|
||||
List<ChatMessage> messages = messagesByConversationId.getOrDefault(conversation.getId(), Collections.emptyList());
|
||||
conversation.setMessages(messages);
|
||||
}
|
||||
return conversations;
|
||||
@@ -213,25 +190,10 @@ public class ConversationServiceImpl implements ConversationService {
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteConversation(Long conversationId) {
|
||||
// Delete associated schedules first to prevent tasks from writing orphaned messages.
|
||||
sopScheduleDao.deleteByConversationId(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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-14
@@ -23,8 +23,6 @@ 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;
|
||||
@@ -47,20 +45,9 @@ public class McpServerServiceImpl implements McpServerService {
|
||||
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();
|
||||
return MethodToolCallbackProvider.builder().toolObjects(monitorTools, alertTools, alertDefineTools, metricsTools).build();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
-164
@@ -1,164 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.service.impl;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.dao.SopScheduleDao;
|
||||
import org.apache.hertzbeat.ai.service.SopScheduleService;
|
||||
import org.apache.hertzbeat.common.entity.ai.SopSchedule;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.scheduling.support.CronExpression;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* Implementation of SopScheduleService for managing scheduled SOP executions.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class SopScheduleServiceImpl implements SopScheduleService {
|
||||
|
||||
private final SopScheduleDao sopScheduleDao;
|
||||
|
||||
@Autowired
|
||||
public SopScheduleServiceImpl(SopScheduleDao sopScheduleDao) {
|
||||
this.sopScheduleDao = sopScheduleDao;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public SopSchedule createSchedule(SopSchedule schedule) {
|
||||
// Validate cron expression
|
||||
validateCronExpression(schedule.getCronExpression());
|
||||
|
||||
// Calculate next run time
|
||||
schedule.setNextRunTime(calculateNextRunTime(schedule.getCronExpression()));
|
||||
schedule.setEnabled(schedule.getEnabled() != null ? schedule.getEnabled() : true);
|
||||
|
||||
SopSchedule saved = sopScheduleDao.save(schedule);
|
||||
log.info("Created schedule {} for conversation {} with SOP {}",
|
||||
saved.getId(), saved.getConversationId(), saved.getSopName());
|
||||
return saved;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public SopSchedule updateSchedule(SopSchedule schedule) {
|
||||
SopSchedule existing = sopScheduleDao.findById(schedule.getId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Schedule not found: " + schedule.getId()));
|
||||
|
||||
// Update fields
|
||||
existing.setSopName(schedule.getSopName());
|
||||
existing.setSopParams(schedule.getSopParams());
|
||||
|
||||
// If cron expression changed, recalculate next run time
|
||||
if (!existing.getCronExpression().equals(schedule.getCronExpression())) {
|
||||
validateCronExpression(schedule.getCronExpression());
|
||||
existing.setCronExpression(schedule.getCronExpression());
|
||||
existing.setNextRunTime(calculateNextRunTime(schedule.getCronExpression()));
|
||||
}
|
||||
|
||||
if (schedule.getEnabled() != null) {
|
||||
existing.setEnabled(schedule.getEnabled());
|
||||
}
|
||||
|
||||
return sopScheduleDao.save(existing);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void deleteSchedule(Long id) {
|
||||
log.info("Deleting schedule {}", id);
|
||||
sopScheduleDao.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SopSchedule getSchedule(Long id) {
|
||||
return sopScheduleDao.findById(id).orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SopSchedule> getSchedulesByConversation(Long conversationId) {
|
||||
return sopScheduleDao.findByConversationId(conversationId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public SopSchedule toggleSchedule(Long id, boolean enabled) {
|
||||
SopSchedule schedule = sopScheduleDao.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Schedule not found: " + id));
|
||||
|
||||
schedule.setEnabled(enabled);
|
||||
|
||||
// If enabling, recalculate next run time
|
||||
if (enabled) {
|
||||
schedule.setNextRunTime(calculateNextRunTime(schedule.getCronExpression()));
|
||||
}
|
||||
|
||||
log.info("Schedule {} {} ", id, enabled ? "enabled" : "disabled");
|
||||
return sopScheduleDao.save(schedule);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SopSchedule> getDueSchedules() {
|
||||
return sopScheduleDao.findDueSchedules(LocalDateTime.now());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void updateAfterExecution(Long id) {
|
||||
SopSchedule schedule = sopScheduleDao.findById(id).orElse(null);
|
||||
if (schedule == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
schedule.setLastRunTime(LocalDateTime.now());
|
||||
schedule.setNextRunTime(calculateNextRunTime(schedule.getCronExpression()));
|
||||
sopScheduleDao.save(schedule);
|
||||
|
||||
log.debug("Updated schedule {} - Last run: {}, Next run: {}",
|
||||
id, schedule.getLastRunTime(), schedule.getNextRunTime());
|
||||
}
|
||||
|
||||
private void validateCronExpression(String cronExpression) {
|
||||
try {
|
||||
CronExpression.parse(cronExpression);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new IllegalArgumentException("Invalid cron expression: " + cronExpression, e);
|
||||
}
|
||||
}
|
||||
|
||||
private LocalDateTime calculateNextRunTime(String cronExpression) {
|
||||
try {
|
||||
CronExpression cron = CronExpression.parse(cronExpression);
|
||||
LocalDateTime nextRunTime = cron.next(LocalDateTime.now());
|
||||
if (nextRunTime == null) {
|
||||
// Expressions such as February 31 are syntactically valid but can never be triggered.
|
||||
throw new IllegalArgumentException(
|
||||
"Cron expression has no future execution time: " + cronExpression);
|
||||
}
|
||||
return nextRunTime;
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw e;
|
||||
} catch (RuntimeException e) {
|
||||
throw new IllegalArgumentException("Failed to calculate next run time: " + cronExpression, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.sop.engine;
|
||||
|
||||
import java.util.Map;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopDefinition;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopResult;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
/**
|
||||
* Engine for executing AI SOPs.
|
||||
*/
|
||||
public interface SopEngine {
|
||||
|
||||
/**
|
||||
* Execute an SOP with the given input parameters (streaming mode).
|
||||
* @param definition The SOP definition to execute.
|
||||
* @param inputParams Input parameters for the SOP.
|
||||
* @return A stream of execution logs/results.
|
||||
*/
|
||||
Flux<String> execute(SopDefinition definition, Map<String, Object> inputParams);
|
||||
|
||||
/**
|
||||
* Execute an SOP synchronously and return a unified result.
|
||||
* Used for AI tool calls and programmatic access.
|
||||
* @param definition The SOP definition to execute.
|
||||
* @param inputParams Input parameters for the SOP.
|
||||
* @return Unified SOP execution result.
|
||||
*/
|
||||
SopResult executeSync(SopDefinition definition, Map<String, Object> inputParams);
|
||||
}
|
||||
@@ -1,304 +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.sop.engine;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.sop.executor.SopExecutor;
|
||||
import org.apache.hertzbeat.ai.sop.model.OutputConfig;
|
||||
import org.apache.hertzbeat.ai.sop.model.OutputType;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopDefinition;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopParameter;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopResult;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopStep;
|
||||
import org.apache.hertzbeat.ai.sop.model.StepResult;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
/**
|
||||
* Implementation of the SopEngine.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class SopEngineImpl implements SopEngine {
|
||||
|
||||
private final List<SopExecutor> executors;
|
||||
|
||||
@Autowired
|
||||
public SopEngineImpl(List<SopExecutor> executors) {
|
||||
this.executors = executors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<String> execute(SopDefinition definition, Map<String, Object> inputParams) {
|
||||
return Flux.create(sink -> {
|
||||
try {
|
||||
OutputConfig outputConfig = resolveOutputConfig(definition);
|
||||
Map<String, Object> context = prepareContext(definition, inputParams, outputConfig);
|
||||
|
||||
log.info("Starting execution of SOP: {}", definition.getName());
|
||||
sink.next("Starting SOP: " + definition.getName() + " (v" + definition.getVersion() + ")");
|
||||
|
||||
for (SopStep step : definition.getSteps()) {
|
||||
sink.next("Executing step [" + step.getId() + "]: " + step.getType());
|
||||
|
||||
// Find appropriate executor
|
||||
SopExecutor executor = findExecutor(step.getType());
|
||||
if (executor == null) {
|
||||
String error = "No executor found for step type: " + step.getType();
|
||||
log.error(error);
|
||||
sink.error(new IllegalArgumentException(error));
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute the step
|
||||
try {
|
||||
Object result = executor.execute(step, context);
|
||||
context.put(step.getId(), result);
|
||||
|
||||
// For LLM steps, output the result (the report)
|
||||
if ("llm".equalsIgnoreCase(step.getType()) && result != null) {
|
||||
sink.next("--- Report from " + step.getId() + " ---");
|
||||
sink.next(String.valueOf(result));
|
||||
sink.next("--- End of Report ---");
|
||||
}
|
||||
|
||||
sink.next("Step " + step.getId() + " completed");
|
||||
} catch (Exception e) {
|
||||
log.error("Error executing step {}: {}", step.getId(), e.getMessage(), e);
|
||||
sink.error(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
sink.next("SOP " + definition.getName() + " completed successfully.");
|
||||
sink.complete();
|
||||
} catch (Exception e) {
|
||||
String sopName = definition == null ? "unknown" : definition.getName();
|
||||
log.error("Error executing SOP {}: {}", sopName, e.getMessage(), e);
|
||||
sink.error(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public SopResult executeSync(SopDefinition definition, Map<String, Object> inputParams) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
List<StepResult> stepResults = new ArrayList<>();
|
||||
|
||||
SopResult.SopResultBuilder resultBuilder = SopResult.builder()
|
||||
.sopName(definition == null ? null : definition.getName())
|
||||
.sopVersion(definition == null ? null : definition.getVersion())
|
||||
.startTime(startTime);
|
||||
|
||||
try {
|
||||
OutputConfig outputConfig = resolveOutputConfig(definition);
|
||||
Map<String, Object> context = prepareContext(definition, inputParams, outputConfig);
|
||||
|
||||
resultBuilder.outputType(outputConfig.getOutputType());
|
||||
resultBuilder.outputFormat(outputConfig.getFormat() != null ? outputConfig.getFormat() : "text");
|
||||
resultBuilder.language(outputConfig.getLanguageCode());
|
||||
|
||||
log.info("Starting sync execution of SOP: {}", definition.getName());
|
||||
|
||||
for (SopStep step : definition.getSteps()) {
|
||||
long stepStartTime = System.currentTimeMillis();
|
||||
StepResult.StepResultBuilder stepBuilder = StepResult.builder()
|
||||
.stepId(step.getId())
|
||||
.type(step.getType())
|
||||
.startTime(stepStartTime);
|
||||
|
||||
// Find appropriate executor
|
||||
SopExecutor executor = findExecutor(step.getType());
|
||||
if (executor == null) {
|
||||
String error = "No executor found for step type: " + step.getType();
|
||||
log.error(error);
|
||||
stepBuilder.status("FAILED")
|
||||
.error(error)
|
||||
.endTime(System.currentTimeMillis());
|
||||
stepResults.add(stepBuilder.build());
|
||||
|
||||
return buildFailedResult(resultBuilder, stepResults, error, startTime);
|
||||
}
|
||||
|
||||
// Execute the step
|
||||
try {
|
||||
Object result = executor.execute(step, context);
|
||||
context.put(step.getId(), result);
|
||||
|
||||
long stepEndTime = System.currentTimeMillis();
|
||||
stepBuilder.status("SUCCESS")
|
||||
.output(result)
|
||||
.endTime(stepEndTime)
|
||||
.duration(stepEndTime - stepStartTime);
|
||||
stepResults.add(stepBuilder.build());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error executing step {}: {}", step.getId(), e.getMessage(), e);
|
||||
long stepEndTime = System.currentTimeMillis();
|
||||
stepBuilder.status("FAILED")
|
||||
.error(e.getMessage())
|
||||
.endTime(stepEndTime)
|
||||
.duration(stepEndTime - stepStartTime);
|
||||
stepResults.add(stepBuilder.build());
|
||||
|
||||
return buildFailedResult(resultBuilder, stepResults, e.getMessage(), startTime);
|
||||
}
|
||||
}
|
||||
|
||||
// Build successful result
|
||||
long endTime = System.currentTimeMillis();
|
||||
resultBuilder.status("SUCCESS")
|
||||
.endTime(endTime)
|
||||
.duration(endTime - startTime)
|
||||
.steps(stepResults)
|
||||
.data(context);
|
||||
|
||||
// Extract summary and content based on output configuration
|
||||
extractOutputContent(resultBuilder, context, outputConfig, stepResults);
|
||||
|
||||
return resultBuilder.build();
|
||||
|
||||
} catch (Exception e) {
|
||||
String sopName = definition == null ? "unknown" : definition.getName();
|
||||
log.error("Error executing SOP {}: {}", sopName, e.getMessage(), e);
|
||||
return buildFailedResult(resultBuilder, stepResults, e.getMessage(), startTime);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies defaults and validates required parameters consistently for every execution entry point.
|
||||
*/
|
||||
private Map<String, Object> prepareContext(SopDefinition definition, Map<String, Object> inputParams,
|
||||
OutputConfig outputConfig) {
|
||||
if (definition.getSteps() == null || definition.getSteps().isEmpty()) {
|
||||
throw new IllegalArgumentException("SOP must contain at least one step");
|
||||
}
|
||||
|
||||
Map<String, Object> context = inputParams == null ? new HashMap<>() : new HashMap<>(inputParams);
|
||||
if (definition.getParameters() != null) {
|
||||
for (SopParameter parameter : definition.getParameters()) {
|
||||
Object value = context.get(parameter.getName());
|
||||
if (isMissing(value) && parameter.getDefaultValue() != null) {
|
||||
context.put(parameter.getName(), parameter.getDefaultValue());
|
||||
value = parameter.getDefaultValue();
|
||||
}
|
||||
if (parameter.isRequired() && isMissing(value)) {
|
||||
throw new IllegalArgumentException("Required SOP parameter is missing: " + parameter.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
context.put("_language", outputConfig.getLanguageCode());
|
||||
return context;
|
||||
}
|
||||
|
||||
private boolean isMissing(Object value) {
|
||||
return value == null || value instanceof String text && text.isBlank();
|
||||
}
|
||||
|
||||
private OutputConfig resolveOutputConfig(SopDefinition definition) {
|
||||
if (definition == null) {
|
||||
throw new IllegalArgumentException("SOP definition must not be null");
|
||||
}
|
||||
if (definition.getOutput() != null) {
|
||||
return definition.getOutput();
|
||||
}
|
||||
return OutputConfig.builder()
|
||||
.type("simple")
|
||||
.format("text")
|
||||
.language("zh")
|
||||
.build();
|
||||
}
|
||||
|
||||
private SopResult buildFailedResult(SopResult.SopResultBuilder builder,
|
||||
List<StepResult> stepResults, String error, long startTime) {
|
||||
long endTime = System.currentTimeMillis();
|
||||
return builder.status("FAILED")
|
||||
.endTime(endTime)
|
||||
.duration(endTime - startTime)
|
||||
.steps(stepResults)
|
||||
.error(error)
|
||||
.summary("SOP execution failed: " + error)
|
||||
.build();
|
||||
}
|
||||
|
||||
private void extractOutputContent(SopResult.SopResultBuilder builder,
|
||||
Map<String, Object> context, OutputConfig outputConfig, List<StepResult> stepResults) {
|
||||
|
||||
// Extract content from specified step or last LLM step
|
||||
String contentStepId = outputConfig.getContentStep();
|
||||
String summaryStepId = outputConfig.getSummaryStep();
|
||||
|
||||
// Find content
|
||||
if (contentStepId != null && context.containsKey(contentStepId)) {
|
||||
builder.content(String.valueOf(context.get(contentStepId)));
|
||||
} else {
|
||||
// Default: use the last LLM step's output as content
|
||||
for (int i = stepResults.size() - 1; i >= 0; i--) {
|
||||
StepResult step = stepResults.get(i);
|
||||
if ("llm".equalsIgnoreCase(step.getType()) && step.getOutput() != null) {
|
||||
builder.content(String.valueOf(step.getOutput()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate summary based on output type
|
||||
OutputType outputType = outputConfig.getOutputType();
|
||||
switch (outputType) {
|
||||
case REPORT:
|
||||
builder.summary("Report generated successfully");
|
||||
break;
|
||||
case DATA:
|
||||
builder.summary("Data retrieved: " + context.size() + " items");
|
||||
break;
|
||||
case ACTION:
|
||||
builder.summary("Action pending confirmation");
|
||||
break;
|
||||
default:
|
||||
builder.summary("Operation completed successfully");
|
||||
}
|
||||
|
||||
// Override with custom summary step if specified
|
||||
if (summaryStepId != null && context.containsKey(summaryStepId)) {
|
||||
String summaryContent = String.valueOf(context.get(summaryStepId));
|
||||
// Take first line as summary
|
||||
int newlineIndex = summaryContent.indexOf('\n');
|
||||
if (newlineIndex > 0 && newlineIndex < 200) {
|
||||
builder.summary(summaryContent.substring(0, newlineIndex));
|
||||
} else if (summaryContent.length() > 200) {
|
||||
builder.summary(summaryContent.substring(0, 200) + "...");
|
||||
} else {
|
||||
builder.summary(summaryContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private SopExecutor findExecutor(String type) {
|
||||
for (SopExecutor executor : executors) {
|
||||
if (executor.support(type)) {
|
||||
return executor;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,110 +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.sop.executor;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext;
|
||||
import org.apache.hertzbeat.ai.service.ChatClientProviderService;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopStep;
|
||||
import org.apache.hertzbeat.ai.utils.SopMessageUtil;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Executor for 'llm' type steps.
|
||||
* Calls the AI model for reasoning or summarization.
|
||||
* Reuses the existing ChatClientProviderService for LLM interaction.
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class LlmExecutor implements SopExecutor {
|
||||
|
||||
private final ChatClientProviderService chatClientProviderService;
|
||||
|
||||
@Autowired
|
||||
public LlmExecutor(ChatClientProviderService chatClientProviderService) {
|
||||
this.chatClientProviderService = chatClientProviderService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean support(String type) {
|
||||
return "llm".equalsIgnoreCase(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object execute(SopStep step, Map<String, Object> context) {
|
||||
String prompt = step.getPrompt();
|
||||
if (prompt == null || prompt.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("LLM step must have a 'prompt' field");
|
||||
}
|
||||
|
||||
// Check if LLM is configured
|
||||
if (!chatClientProviderService.isConfigured()) {
|
||||
log.warn("LLM provider is not configured, returning mock response");
|
||||
return "Mock LLM response: Provider not configured";
|
||||
}
|
||||
|
||||
// Resolve variables in prompt from context
|
||||
String resolvedPrompt = resolvePrompt(prompt, context);
|
||||
|
||||
// Add language instruction based on configuration
|
||||
String language = (String) context.getOrDefault("_language", "zh");
|
||||
resolvedPrompt = addLanguageInstruction(resolvedPrompt, language);
|
||||
|
||||
log.info("Executing LLM step with prompt length: {}", resolvedPrompt.length());
|
||||
|
||||
try {
|
||||
// Build chat request context
|
||||
ChatRequestContext chatContext = ChatRequestContext.builder()
|
||||
.message(resolvedPrompt)
|
||||
.build();
|
||||
|
||||
// Use existing service to get response (collect stream to string)
|
||||
StringBuilder responseBuilder = new StringBuilder();
|
||||
chatClientProviderService.streamChat(chatContext)
|
||||
.doOnNext(responseBuilder::append)
|
||||
.blockLast();
|
||||
|
||||
String response = responseBuilder.toString();
|
||||
log.debug("LLM response length: {}", response.length());
|
||||
return response;
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to execute LLM step: {}", e.getMessage());
|
||||
throw new RuntimeException("LLM execution failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String addLanguageInstruction(String prompt, String language) {
|
||||
String langInstruction = SopMessageUtil.getMessage("sop.llm.language.instruction", language);
|
||||
return "\n\n[" + langInstruction + "]\n\n" + prompt;
|
||||
}
|
||||
|
||||
private String resolvePrompt(String prompt, Map<String, Object> context) {
|
||||
// Simple variable replacement: ${variable} -> context.get("variable")
|
||||
String result = prompt;
|
||||
for (Map.Entry<String, Object> entry : context.entrySet()) {
|
||||
String placeholder = "${" + entry.getKey() + "}";
|
||||
if (result.contains(placeholder)) {
|
||||
result = result.replace(placeholder, String.valueOf(entry.getValue()));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.sop.executor;
|
||||
|
||||
import org.apache.hertzbeat.ai.sop.model.SopStep;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Interface for SOP step executors.
|
||||
*/
|
||||
public interface SopExecutor {
|
||||
|
||||
/**
|
||||
* Check if this executor supports the given step type.
|
||||
* @param type Step type.
|
||||
* @return true if supported.
|
||||
*/
|
||||
boolean support(String type);
|
||||
|
||||
/**
|
||||
* Execute the given step.
|
||||
* @param step The step to execute.
|
||||
* @param context Current execution context.
|
||||
* @return Execution result.
|
||||
*/
|
||||
Object execute(SopStep step, Map<String, Object> context);
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.sop.executor;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopStep;
|
||||
import org.apache.hertzbeat.ai.sop.registry.ToolRegistry;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Executor for 'tool' type steps.
|
||||
* Uses ToolRegistry to dynamically discover and invoke @Tool annotated methods.
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class ToolExecutor implements SopExecutor {
|
||||
|
||||
private final ToolRegistry toolRegistry;
|
||||
|
||||
@Autowired
|
||||
public ToolExecutor(@Lazy ToolRegistry toolRegistry) {
|
||||
this.toolRegistry = toolRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean support(String type) {
|
||||
return "tool".equalsIgnoreCase(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object execute(SopStep step, Map<String, Object> context) {
|
||||
String toolName = step.getTool();
|
||||
log.info("Executing tool step: {}", toolName);
|
||||
|
||||
Map<String, Object> args = resolveArgs(step.getArgs(), context);
|
||||
|
||||
try {
|
||||
String result = toolRegistry.invoke(toolName, args);
|
||||
log.debug("Tool {} returned result length: {}", toolName,
|
||||
result != null ? result.length() : 0);
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to execute tool {}: {}", toolName, e.getMessage());
|
||||
throw new RuntimeException("Tool execution failed: " + toolName, e);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> resolveArgs(Map<String, Object> args, Map<String, Object> context) {
|
||||
if (args == null) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
Map<String, Object> resolved = new HashMap<>();
|
||||
for (Map.Entry<String, Object> entry : args.entrySet()) {
|
||||
Object value = entry.getValue();
|
||||
if (value instanceof String) {
|
||||
String strValue = (String) value;
|
||||
for (Map.Entry<String, Object> ctxEntry : context.entrySet()) {
|
||||
String placeholder = "${" + ctxEntry.getKey() + "}";
|
||||
if (strValue.contains(placeholder)) {
|
||||
strValue = strValue.replace(placeholder, String.valueOf(ctxEntry.getValue()));
|
||||
}
|
||||
}
|
||||
resolved.put(entry.getKey(), strValue);
|
||||
} else {
|
||||
resolved.put(entry.getKey(), value);
|
||||
}
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.sop.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Output configuration for SOP definition.
|
||||
* Defines how the SOP result should be formatted and presented.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class OutputConfig {
|
||||
|
||||
/**
|
||||
* Output type: report, simple, data, action
|
||||
*/
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* Output format: markdown, json, text
|
||||
*/
|
||||
private String format;
|
||||
|
||||
/**
|
||||
* Which step's output should be used as the final summary
|
||||
*/
|
||||
private String summaryStep;
|
||||
|
||||
/**
|
||||
* Which step's output should be used as the main content
|
||||
*/
|
||||
private String contentStep;
|
||||
|
||||
/**
|
||||
* Language for LLM responses and SOP output: zh (Chinese), en (English)
|
||||
* Default is zh (Chinese)
|
||||
*/
|
||||
private String language;
|
||||
|
||||
/**
|
||||
* Get language code.
|
||||
* If not specified in skill config, uses system default locale (from user's SystemConfig).
|
||||
*/
|
||||
public String getLanguageCode() {
|
||||
if (language == null || language.isEmpty()) {
|
||||
// Use system default locale (set by user's SystemConfig)
|
||||
java.util.Locale defaultLocale = java.util.Locale.getDefault();
|
||||
if (defaultLocale.getLanguage().equals("en")) {
|
||||
return "en";
|
||||
}
|
||||
return "zh"; // Default to Chinese for non-English locales
|
||||
}
|
||||
return language.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the language is Chinese
|
||||
*/
|
||||
public boolean isChinese() {
|
||||
return "zh".equals(getLanguageCode()) || "chinese".equalsIgnoreCase(language);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the OutputType enum from string
|
||||
*/
|
||||
public OutputType getOutputType() {
|
||||
if (type == null) {
|
||||
return OutputType.SIMPLE;
|
||||
}
|
||||
try {
|
||||
return OutputType.valueOf(type.toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
return OutputType.SIMPLE;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +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.sop.model;
|
||||
|
||||
/**
|
||||
* SOP output type enumeration.
|
||||
* Defines different types of SOP execution results.
|
||||
*/
|
||||
public enum OutputType {
|
||||
/**
|
||||
* Report type - generates a detailed report (e.g., daily inspection, fault analysis)
|
||||
*/
|
||||
REPORT,
|
||||
|
||||
/**
|
||||
* Simple type - returns simple success/failure result (e.g., restart, clear cache)
|
||||
*/
|
||||
SIMPLE,
|
||||
|
||||
/**
|
||||
* Data type - returns structured data (e.g., query resources, statistics)
|
||||
*/
|
||||
DATA,
|
||||
|
||||
/**
|
||||
* Action type - returns pending actions that require human confirmation
|
||||
*/
|
||||
ACTION
|
||||
}
|
||||
@@ -1,197 +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.sop.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.apache.hertzbeat.ai.utils.SopMessageUtil;
|
||||
|
||||
/**
|
||||
* Unified SOP execution result.
|
||||
* Contains all information about a SOP execution including metadata, output, and step details.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SopResult {
|
||||
|
||||
// ===== Metadata =====
|
||||
|
||||
/**
|
||||
* SOP name
|
||||
*/
|
||||
private String sopName;
|
||||
|
||||
/**
|
||||
* SOP version
|
||||
*/
|
||||
private String sopVersion;
|
||||
|
||||
/**
|
||||
* Execution status: SUCCESS, FAILED, PARTIAL
|
||||
*/
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* Start timestamp in milliseconds
|
||||
*/
|
||||
private long startTime;
|
||||
|
||||
/**
|
||||
* End timestamp in milliseconds
|
||||
*/
|
||||
private long endTime;
|
||||
|
||||
/**
|
||||
* Duration in milliseconds
|
||||
*/
|
||||
private long duration;
|
||||
|
||||
// ===== Output Type =====
|
||||
|
||||
/**
|
||||
* Output type: REPORT, SIMPLE, DATA, ACTION
|
||||
*/
|
||||
private OutputType outputType;
|
||||
|
||||
/**
|
||||
* Output format: markdown, json, text
|
||||
*/
|
||||
private String outputFormat;
|
||||
|
||||
/**
|
||||
* Language code: zh (Chinese), en (English)
|
||||
*/
|
||||
@Builder.Default
|
||||
private String language = "zh";
|
||||
|
||||
// ===== Output Content =====
|
||||
|
||||
/**
|
||||
* Short summary (one line)
|
||||
*/
|
||||
private String summary;
|
||||
|
||||
/**
|
||||
* Main content (Markdown report / JSON data / etc.)
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* Structured data (for programmatic processing)
|
||||
*/
|
||||
@Builder.Default
|
||||
private Map<String, Object> data = new HashMap<>();
|
||||
|
||||
// ===== Step Details =====
|
||||
|
||||
/**
|
||||
* Execution result of each step
|
||||
*/
|
||||
@Builder.Default
|
||||
private List<StepResult> steps = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Error message if failed
|
||||
*/
|
||||
private String error;
|
||||
|
||||
/**
|
||||
* Get localized message using i18n.
|
||||
*/
|
||||
private String msg(String code) {
|
||||
return SopMessageUtil.getMessage(code, language);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to AI-friendly response format.
|
||||
* Used when SOP is called as a tool by AI.
|
||||
*/
|
||||
public String toAiResponse() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append(msg("sop.result.title")).append(": ").append(status).append("\n");
|
||||
sb.append(msg("sop.result.name")).append(": ").append(sopName)
|
||||
.append(" (v").append(sopVersion).append(")\n");
|
||||
sb.append(msg("sop.result.duration")).append(": ").append(duration).append("ms\n");
|
||||
|
||||
if (summary != null && !summary.isEmpty()) {
|
||||
sb.append(msg("sop.result.summary")).append(": ").append(summary).append("\n");
|
||||
}
|
||||
|
||||
if ("FAILED".equals(status) && error != null) {
|
||||
sb.append(msg("sop.result.error")).append(": ").append(error).append("\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
if (outputType == OutputType.REPORT && content != null) {
|
||||
sb.append("\n--- ").append(msg("sop.result.report.title")).append(" ---\n");
|
||||
sb.append(content);
|
||||
} else if (outputType == OutputType.DATA && !data.isEmpty()) {
|
||||
sb.append("\n").append(msg("sop.result.data.title")).append(":\n");
|
||||
for (Map.Entry<String, Object> entry : data.entrySet()) {
|
||||
sb.append("- ").append(entry.getKey()).append(": ").append(entry.getValue()).append("\n");
|
||||
}
|
||||
} else if (outputType == OutputType.SIMPLE) {
|
||||
sb.append(msg("sop.result.simple.complete")).append("\n");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to SSE stream format for real-time updates.
|
||||
*/
|
||||
public String toSseFormat() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("event: sop_result\n");
|
||||
sb.append("data: {");
|
||||
sb.append("\"sopName\":\"").append(sopName).append("\",");
|
||||
sb.append("\"status\":\"").append(status).append("\",");
|
||||
sb.append("\"duration\":").append(duration).append(",");
|
||||
sb.append("\"outputType\":\"").append(outputType).append("\",");
|
||||
sb.append("\"language\":\"").append(language).append("\",");
|
||||
if (summary != null) {
|
||||
sb.append("\"summary\":\"").append(escapeJson(summary)).append("\",");
|
||||
}
|
||||
if (content != null) {
|
||||
sb.append("\"content\":\"").append(escapeJson(content)).append("\",");
|
||||
}
|
||||
sb.append("\"stepsCount\":").append(steps.size());
|
||||
sb.append("}\n\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String escapeJson(String text) {
|
||||
if (text == null) {
|
||||
return "";
|
||||
}
|
||||
return text.replace("\\", "\\\\")
|
||||
.replace("\"", "\\\"")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r")
|
||||
.replace("\t", "\\t");
|
||||
}
|
||||
}
|
||||
@@ -1,73 +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.sop.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Represents the result of a single SOP step execution.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class StepResult {
|
||||
|
||||
/**
|
||||
* Step ID
|
||||
*/
|
||||
private String stepId;
|
||||
|
||||
/**
|
||||
* Step type (tool, llm, condition, etc.)
|
||||
*/
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* Execution status: SUCCESS, FAILED, SKIPPED
|
||||
*/
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* Start timestamp in milliseconds
|
||||
*/
|
||||
private long startTime;
|
||||
|
||||
/**
|
||||
* End timestamp in milliseconds
|
||||
*/
|
||||
private long endTime;
|
||||
|
||||
/**
|
||||
* Duration in milliseconds
|
||||
*/
|
||||
private long duration;
|
||||
|
||||
/**
|
||||
* Step output/result
|
||||
*/
|
||||
private Object output;
|
||||
|
||||
/**
|
||||
* Error message if failed
|
||||
*/
|
||||
private String error;
|
||||
}
|
||||
@@ -1,103 +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.sop.registry;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.sop.engine.SopEngine;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopDefinition;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Registry for AI SOP skills.
|
||||
* Manages the lifecycle of SOPs and exposes them as Spring AI tools.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@Configuration
|
||||
public class SkillRegistry implements ToolCallbackProvider {
|
||||
|
||||
private final SopYamlLoader yamlLoader;
|
||||
private final SopEngine sopEngine;
|
||||
private final Map<String, SopDefinition> skillMap = new ConcurrentHashMap<>();
|
||||
|
||||
@Autowired
|
||||
public SkillRegistry(SopYamlLoader yamlLoader, SopEngine sopEngine) {
|
||||
this.yamlLoader = yamlLoader;
|
||||
this.sopEngine = sopEngine;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
refreshSkills();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload all skills from YAML files.
|
||||
*/
|
||||
public void refreshSkills() {
|
||||
List<SopDefinition> loadedSkills = yamlLoader.loadAllSkills();
|
||||
skillMap.clear();
|
||||
for (SopDefinition skill : loadedSkills) {
|
||||
skillMap.put(skill.getName(), skill);
|
||||
}
|
||||
log.info("SkillRegistry initialized with {} skills", skillMap.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific SOP definition by name.
|
||||
* @param name Skill name.
|
||||
* @return SopDefinition or null if not found.
|
||||
*/
|
||||
public SopDefinition getSkill(String name) {
|
||||
return skillMap.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered SOP definitions.
|
||||
* @return Collection of all SopDefinitions.
|
||||
*/
|
||||
public List<SopDefinition> getAllSkills() {
|
||||
return new ArrayList<>(skillMap.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the SOP skills as ToolCallbacks for Spring AI.
|
||||
* @return Array of ToolCallbacks.
|
||||
*/
|
||||
@Override
|
||||
public ToolCallback[] getToolCallbacks() {
|
||||
List<ToolCallback> callbacks = new ArrayList<>();
|
||||
|
||||
for (SopDefinition skill : skillMap.values()) {
|
||||
log.debug("Registering SOP as tool: {}", skill.getName());
|
||||
callbacks.add(new SopToolCallback(skill, sopEngine));
|
||||
}
|
||||
|
||||
return callbacks.toArray(new ToolCallback[0]);
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.sop.registry;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.apache.hertzbeat.ai.sop.engine.SopEngine;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopDefinition;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopParameter;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopResult;
|
||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.definition.ToolDefinition;
|
||||
|
||||
/**
|
||||
* ToolCallback implementation for executing AI SOPs.
|
||||
* Wraps a SOP definition as a Spring AI tool.
|
||||
*/
|
||||
public class SopToolCallback implements ToolCallback {
|
||||
|
||||
private final SopDefinition definition;
|
||||
private final SopEngine sopEngine;
|
||||
private final ToolDefinition toolDefinition;
|
||||
|
||||
public SopToolCallback(SopDefinition definition, SopEngine sopEngine) {
|
||||
this.definition = definition;
|
||||
this.sopEngine = sopEngine;
|
||||
this.toolDefinition = ToolDefinition.builder()
|
||||
.name(definition.getName())
|
||||
.description(definition.getDescription())
|
||||
.inputSchema(buildInputSchema())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ToolDefinition getToolDefinition() {
|
||||
return toolDefinition;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String call(String arguments) {
|
||||
Map<String, Object> inputParams = parseArguments(arguments);
|
||||
SopResult result = sopEngine.executeSync(definition, inputParams);
|
||||
return result.toAiResponse();
|
||||
}
|
||||
|
||||
private String buildInputSchema() {
|
||||
Map<String, Object> schema = new LinkedHashMap<>();
|
||||
Map<String, Object> properties = new LinkedHashMap<>();
|
||||
List<String> required = new ArrayList<>();
|
||||
|
||||
if (definition.getParameters() != null) {
|
||||
for (SopParameter parameter : definition.getParameters()) {
|
||||
Map<String, Object> property = new LinkedHashMap<>();
|
||||
property.put("type", mapType(parameter.getType()));
|
||||
if (parameter.getDescription() != null) {
|
||||
property.put("description", parameter.getDescription());
|
||||
}
|
||||
if (parameter.getDefaultValue() != null) {
|
||||
property.put("default", convertDefaultValue(parameter));
|
||||
}
|
||||
properties.put(parameter.getName(), property);
|
||||
if (parameter.isRequired()) {
|
||||
required.add(parameter.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
schema.put("type", "object");
|
||||
schema.put("properties", properties);
|
||||
if (!required.isEmpty()) {
|
||||
schema.put("required", required);
|
||||
}
|
||||
schema.put("additionalProperties", false);
|
||||
return JsonUtil.toJson(schema);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> parseArguments(String arguments) {
|
||||
if (arguments == null || arguments.isBlank()) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
Map<String, Object> inputParams = JsonUtil.fromJson(arguments, Map.class);
|
||||
if (inputParams == null) {
|
||||
throw new IllegalArgumentException("SOP tool arguments must be a valid JSON object");
|
||||
}
|
||||
return inputParams;
|
||||
}
|
||||
|
||||
private Object convertDefaultValue(SopParameter parameter) {
|
||||
String defaultValue = parameter.getDefaultValue();
|
||||
try {
|
||||
return switch (mapType(parameter.getType())) {
|
||||
case "boolean" -> parseBooleanDefault(parameter, defaultValue);
|
||||
case "integer" -> Long.valueOf(defaultValue);
|
||||
case "number" -> Double.valueOf(defaultValue);
|
||||
default -> defaultValue;
|
||||
};
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException("Invalid default value for SOP parameter: " + parameter.getName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean parseBooleanDefault(SopParameter parameter, String defaultValue) {
|
||||
String normalizedValue = defaultValue.trim();
|
||||
if ("true".equalsIgnoreCase(normalizedValue)) {
|
||||
return true;
|
||||
}
|
||||
if ("false".equalsIgnoreCase(normalizedValue)) {
|
||||
return false;
|
||||
}
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid boolean default value for SOP parameter: " + parameter.getName());
|
||||
}
|
||||
|
||||
private String mapType(String type) {
|
||||
if (type == null) {
|
||||
return "string";
|
||||
}
|
||||
switch (type.toLowerCase()) {
|
||||
case "boolean":
|
||||
return "boolean";
|
||||
case "integer":
|
||||
case "int":
|
||||
case "long":
|
||||
return "integer";
|
||||
case "number":
|
||||
case "double":
|
||||
case "float":
|
||||
return "number";
|
||||
default:
|
||||
return "string";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.sop.registry;
|
||||
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopDefinition;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import tools.jackson.dataformat.yaml.YAMLFactory;
|
||||
|
||||
/**
|
||||
* Loader for AI SOP definitions from YAML files.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class SopYamlLoader {
|
||||
|
||||
private static final String SKILLS_PATH_PATTERN = "classpath:skills/*.yml";
|
||||
private final ObjectMapper yamlMapper;
|
||||
|
||||
public SopYamlLoader() {
|
||||
this.yamlMapper = new ObjectMapper(new YAMLFactory());
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all SOP definitions from the classpath.
|
||||
* @return List of loaded SopDefinition objects.
|
||||
*/
|
||||
public List<SopDefinition> loadAllSkills() {
|
||||
List<SopDefinition> skills = new ArrayList<>();
|
||||
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
|
||||
|
||||
try {
|
||||
Resource[] resources = resolver.getResources(SKILLS_PATH_PATTERN);
|
||||
log.info("Found {} SOP definition files", resources.length);
|
||||
|
||||
for (Resource resource : resources) {
|
||||
try {
|
||||
SopDefinition definition = yamlMapper.readValue(resource.getInputStream(), SopDefinition.class);
|
||||
if (definition != null) {
|
||||
skills.add(definition);
|
||||
log.info("Loaded SOP skill: {}", definition.getName());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to parse SOP definition from {}: {}", resource.getFilename(), e.getMessage());
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to scan for SOP definition files: {}", e.getMessage());
|
||||
}
|
||||
|
||||
return skills;
|
||||
}
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.sop.registry;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* Registry for Spring AI tools.
|
||||
*
|
||||
* <p>Scans beans containing {@link Tool} methods on first use and delegates argument deserialization,
|
||||
* proxy method resolution, and result conversion to Spring AI's {@link MethodToolCallbackProvider}.
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class ToolRegistry {
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
private volatile Map<String, RegisteredTool> tools = Map.of();
|
||||
private volatile boolean initialized = false;
|
||||
|
||||
@Autowired
|
||||
public ToolRegistry(ApplicationContext applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
private void ensureInitialized() {
|
||||
if (!initialized) {
|
||||
synchronized (this) {
|
||||
if (!initialized) {
|
||||
tools = scanAllBeans();
|
||||
initialized = true;
|
||||
log.info("Registered {} tool methods: {}", tools.size(), tools.keySet());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, RegisteredTool> scanAllBeans() {
|
||||
log.info("Scanning for @Tool annotated methods...");
|
||||
List<Object> toolBeans = Arrays.stream(applicationContext.getBeanDefinitionNames())
|
||||
.filter(this::containsToolMethod)
|
||||
.map(applicationContext::getBean)
|
||||
.toList();
|
||||
|
||||
if (toolBeans.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
ToolCallback[] callbacks = MethodToolCallbackProvider.builder().toolObjects(toolBeans.toArray())
|
||||
.build().getToolCallbacks();
|
||||
Map<String, RegisteredTool> discoveredTools = new LinkedHashMap<>();
|
||||
for (ToolCallback callback : callbacks) {
|
||||
String toolName = callback.getToolDefinition().name();
|
||||
discoveredTools.put(toolName, new RegisteredTool(callback, getRequiredParameters(callback)));
|
||||
}
|
||||
return Collections.unmodifiableMap(discoveredTools);
|
||||
}
|
||||
|
||||
private boolean containsToolMethod(String beanName) {
|
||||
Class<?> beanType = applicationContext.getType(beanName, false);
|
||||
if (beanType == null) {
|
||||
return false;
|
||||
}
|
||||
return Arrays.stream(ReflectionUtils.getDeclaredMethods(ClassUtils.getUserClass(beanType)))
|
||||
.anyMatch(this::isToolMethod);
|
||||
}
|
||||
|
||||
private boolean isToolMethod(Method method) {
|
||||
return AnnotationUtils.findAnnotation(method, Tool.class) != null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Set<String> getRequiredParameters(ToolCallback callback) {
|
||||
Map<String, Object> schema = JsonUtil.fromJson(callback.getToolDefinition().inputSchema(), Map.class);
|
||||
if (schema == null || !(schema.get("required") instanceof List<?> required)) {
|
||||
return Set.of();
|
||||
}
|
||||
return required.stream()
|
||||
.map(String::valueOf)
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes a tool with the provided arguments.
|
||||
*/
|
||||
public String invoke(String toolName, Map<String, Object> args) {
|
||||
ensureInitialized();
|
||||
RegisteredTool tool = tools.get(toolName);
|
||||
if (tool == null) {
|
||||
throw new IllegalArgumentException("Unknown tool: " + toolName
|
||||
+ ". Available tools: " + tools.keySet());
|
||||
}
|
||||
Map<String, Object> safeArgs = args == null ? Map.of() : args;
|
||||
validateRequiredParameters(tool.requiredParameters(), safeArgs);
|
||||
String result = tool.callback().call(JsonUtil.toJson(safeArgs));
|
||||
Object convertedResult = JsonUtil.fromJson(result, Object.class);
|
||||
return convertedResult instanceof String text ? text : result;
|
||||
}
|
||||
|
||||
private void validateRequiredParameters(Set<String> requiredParameters, Map<String, Object> args) {
|
||||
for (String parameter : requiredParameters) {
|
||||
Object value = args.get(parameter);
|
||||
if (value == null || value instanceof String text && text.isBlank()) {
|
||||
throw new IllegalArgumentException("Required tool parameter is missing: " + parameter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a tool exists.
|
||||
*/
|
||||
public boolean hasMethod(String toolName) {
|
||||
ensureInitialized();
|
||||
return tools.containsKey(toolName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an immutable snapshot of all registered tool names.
|
||||
*/
|
||||
public Set<String> getToolNames() {
|
||||
ensureInitialized();
|
||||
return Set.copyOf(tools.keySet());
|
||||
}
|
||||
|
||||
private record RegisteredTool(ToolCallback callback, Set<String> requiredParameters) {
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.tools;
|
||||
|
||||
/**
|
||||
* Database Tools interface for AI-powered database diagnostics.
|
||||
* Provides predefined safe queries for database health checks and troubleshooting.
|
||||
*/
|
||||
public interface DatabaseTools {
|
||||
|
||||
/**
|
||||
* Get MySQL slow query statistics from performance_schema.
|
||||
*/
|
||||
String getMySqlSlowQueries(Long monitorId, Integer limit);
|
||||
|
||||
/**
|
||||
* Get MySQL current process list.
|
||||
*/
|
||||
String getMySqlProcessList(Long monitorId);
|
||||
|
||||
/**
|
||||
* Get MySQL lock wait information.
|
||||
*/
|
||||
String getMySqlLockWaits(Long monitorId);
|
||||
|
||||
/**
|
||||
* Get MySQL global status variables.
|
||||
*/
|
||||
String getMySqlGlobalStatus(Long monitorId, String pattern);
|
||||
|
||||
/**
|
||||
* Explain a SELECT query for performance analysis.
|
||||
*/
|
||||
String explainQuery(Long monitorId, String query);
|
||||
}
|
||||
@@ -27,39 +27,19 @@ 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 intervals Collection interval in seconds (default: 600)
|
||||
* @param params Monitor-specific parameters as JSON string (e.g., host, port, username, password, etc.)
|
||||
* @param name Monitor name
|
||||
* @param app Monitor type/application (e.g., 'linux', 'mysql', 'http')
|
||||
* @param intervals Collection interval in seconds (default: 600)
|
||||
* @param params Monitor-specific parameters as JSON string (e.g., host, port, username, password, etc.)
|
||||
* @param description Monitor description (optional)
|
||||
* @return Result message with monitor ID if successful
|
||||
*/
|
||||
String addMonitor(
|
||||
String name,
|
||||
String app,
|
||||
Integer intervals,
|
||||
String params,
|
||||
String description
|
||||
);
|
||||
|
||||
/**
|
||||
* Add a new monitor with comprehensive configuration, methods specifically for hertzbeat ai
|
||||
*
|
||||
* @param conversationId Conversation ID
|
||||
* @param name Monitor name
|
||||
* @param app Monitor type/application (e.g., 'linux', 'mysql', 'http')
|
||||
* @param intervals Collection interval in seconds (default: 600)
|
||||
* @param params Monitor-specific parameters as JSON string (e.g., host, port, username, password, etc.)
|
||||
* @param description Monitor description (optional)
|
||||
* @return Result message with monitor ID if successful
|
||||
*/
|
||||
String addMonitorProtected(
|
||||
Long conversationId,
|
||||
String name,
|
||||
String app,
|
||||
Integer intervals,
|
||||
String params,
|
||||
String description
|
||||
String name,
|
||||
String app,
|
||||
Integer intervals,
|
||||
String params,
|
||||
String description
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -72,30 +52,29 @@ public interface MonitorTools {
|
||||
|
||||
/**
|
||||
* 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 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);
|
||||
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
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.tools;
|
||||
|
||||
/**
|
||||
* Interface for AI Schedule management tools.
|
||||
* Allows AI to create, list, and manage scheduled tasks via natural language.
|
||||
*/
|
||||
public interface ScheduleTools {
|
||||
|
||||
/**
|
||||
* Create a scheduled task to execute a skill periodically.
|
||||
* @param skillName Name of the skill to schedule (e.g., daily_inspection)
|
||||
* @param cronExpression Cron expression for scheduling (6-digit Spring format)
|
||||
* @param description Human-readable description of the schedule
|
||||
* @return Confirmation message with schedule details
|
||||
*/
|
||||
String createSchedule(String skillName, String cronExpression, String description);
|
||||
|
||||
/**
|
||||
* List all scheduled tasks for the current conversation.
|
||||
* @return List of scheduled tasks with their status
|
||||
*/
|
||||
String listSchedules();
|
||||
|
||||
/**
|
||||
* Delete a scheduled task by ID.
|
||||
* @param scheduleId ID of the schedule to delete
|
||||
* @return Confirmation message
|
||||
*/
|
||||
String deleteSchedule(Long scheduleId);
|
||||
|
||||
/**
|
||||
* Enable or disable a scheduled task.
|
||||
* @param scheduleId ID of the schedule
|
||||
* @param enabled Whether to enable or disable
|
||||
* @return Confirmation message
|
||||
*/
|
||||
String toggleSchedule(Long scheduleId, boolean enabled);
|
||||
}
|
||||
@@ -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.tools;
|
||||
|
||||
/**
|
||||
* Interface for AI Skill execution tools.
|
||||
* Provides methods to list available skills and execute them.
|
||||
*/
|
||||
public interface SkillTools {
|
||||
|
||||
/**
|
||||
* List all available diagnostic skills.
|
||||
* @return JSON string containing skill metadata (name, description, parameters)
|
||||
*/
|
||||
String listSkills();
|
||||
|
||||
/**
|
||||
* Execute a diagnostic skill.
|
||||
* @param skillName Name of the skill to execute
|
||||
* @param paramsJson JSON string containing skill parameters (e.g., {"monitorId": 123})
|
||||
* @return Skill execution result. For report-type skills, returns "[[SKILL_REPORT]]\n{content}"
|
||||
*/
|
||||
String executeSkill(String skillName, String paramsJson);
|
||||
}
|
||||
+29
-29
@@ -17,9 +17,9 @@
|
||||
|
||||
package org.apache.hertzbeat.ai.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;
|
||||
@@ -74,30 +74,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 +109,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 +196,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());
|
||||
|
||||
|
||||
@@ -242,7 +242,7 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
|
||||
.datasource(datasource)
|
||||
.enable(enable)
|
||||
.build();
|
||||
|
||||
|
||||
log.debug("Current security subject for addAlertDefine: {}", subjectSum);
|
||||
|
||||
alertDefineService.addAlertDefine(alertDefine);
|
||||
@@ -251,9 +251,9 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
|
||||
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());
|
||||
|
||||
|
||||
StringBuilder response = new StringBuilder();
|
||||
response.append(String.format("Successfully created %s alert rule '%s' with ID: %d\n",
|
||||
response.append(String.format("Successfully created %s alert rule '%s' with ID: %d\n",
|
||||
type, name, alertDefine.getId()));
|
||||
response.append(String.format("Expression: %s\n", expr));
|
||||
response.append(String.format("Priority: %d (%s)\n", priority, severityLabel));
|
||||
@@ -442,7 +442,7 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
|
||||
HertzBeat: 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 +452,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(
|
||||
@@ -524,7 +524,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 +536,7 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (validMonitorIds.isEmpty()) {
|
||||
return "Error: No valid monitor IDs provided";
|
||||
}
|
||||
@@ -560,19 +560,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 +587,31 @@ 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);
|
||||
|
||||
|
||||
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
|
||||
@@ -636,7 +636,7 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
|
||||
alertDefineService.modifyAlertDefine(existingRule);
|
||||
|
||||
log.info("Successfully bound monitors {} to alert rule ID: {}", validMonitorIds, ruleId);
|
||||
return String.format("Successfully bound %d monitor(s) to alert rule ID %d.\nMonitor IDs: %s\nUpdated expression: %s",
|
||||
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) {
|
||||
|
||||
@@ -1,313 +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.tools.impl;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.ResultSetMetaData;
|
||||
import java.sql.Statement;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.tools.DatabaseTools;
|
||||
import org.apache.hertzbeat.common.entity.manager.Monitor;
|
||||
import org.apache.hertzbeat.common.entity.manager.Param;
|
||||
import org.apache.hertzbeat.common.util.AesUtil;
|
||||
import org.apache.hertzbeat.common.util.CommonUtil;
|
||||
import org.apache.hertzbeat.common.util.IpDomainUtil;
|
||||
import org.apache.hertzbeat.common.util.JdbcUrlSafetyUtil;
|
||||
import org.apache.hertzbeat.manager.pojo.dto.MonitorDto;
|
||||
import org.apache.hertzbeat.manager.service.MonitorService;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.ai.tool.annotation.ToolParam;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Implementation of Database Tools for AI-powered database diagnostics.
|
||||
* Provides predefined safe queries - does NOT allow arbitrary SQL execution.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class DatabaseToolsImpl implements DatabaseTools {
|
||||
|
||||
private static final int QUERY_TIMEOUT_SECONDS = 10;
|
||||
private static final int MAX_ROWS = 100;
|
||||
|
||||
// Allowed database types
|
||||
private static final List<String> SUPPORTED_PLATFORMS = List.of("mysql", "mariadb");
|
||||
|
||||
private final MonitorService monitorService;
|
||||
|
||||
@Autowired
|
||||
public DatabaseToolsImpl(MonitorService monitorService) {
|
||||
this.monitorService = monitorService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Tool(name = "get_mysql_slow_queries", description = "Get MySQL slow query statistics from performance_schema. "
|
||||
+ "Returns top N slow queries sorted by average execution time. "
|
||||
+ "Requires the monitor to be a MySQL type with proper credentials.")
|
||||
public String getMySqlSlowQueries(
|
||||
@ToolParam(description = "Monitor ID of the MySQL instance", required = true) Long monitorId,
|
||||
@ToolParam(description = "Maximum number of slow queries to return (default: 10, max: 50)", required = false) Integer limit) {
|
||||
|
||||
int queryLimit = (limit == null || limit <= 0) ? 10 : Math.min(limit, 50);
|
||||
|
||||
String sql = "SELECT SCHEMA_NAME as db, "
|
||||
+ "DIGEST_TEXT as query, "
|
||||
+ "COUNT_STAR as exec_count, "
|
||||
+ "ROUND(AVG_TIMER_WAIT/1000000000, 2) as avg_time_ms, "
|
||||
+ "ROUND(SUM_TIMER_WAIT/1000000000, 2) as total_time_ms, "
|
||||
+ "SUM_ROWS_EXAMINED as rows_examined, "
|
||||
+ "SUM_ROWS_SENT as rows_sent "
|
||||
+ "FROM performance_schema.events_statements_summary_by_digest "
|
||||
+ "WHERE SCHEMA_NAME IS NOT NULL "
|
||||
+ "ORDER BY AVG_TIMER_WAIT DESC "
|
||||
+ "LIMIT " + queryLimit;
|
||||
|
||||
return executeQuery(monitorId, sql, "MySQL Slow Query Statistics");
|
||||
}
|
||||
|
||||
@Override
|
||||
@Tool(name = "get_mysql_process_list", description = "Get MySQL current process list. "
|
||||
+ "Shows all active connections and their current state. "
|
||||
+ "Useful for identifying blocking queries or connection issues.")
|
||||
public String getMySqlProcessList(
|
||||
@ToolParam(description = "Monitor ID of the MySQL instance", required = true) Long monitorId) {
|
||||
|
||||
String sql = "SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, "
|
||||
+ "LEFT(INFO, 200) as QUERY "
|
||||
+ "FROM information_schema.PROCESSLIST "
|
||||
+ "WHERE COMMAND != 'Sleep' "
|
||||
+ "ORDER BY TIME DESC "
|
||||
+ "LIMIT 50";
|
||||
|
||||
return executeQuery(monitorId, sql, "MySQL Process List");
|
||||
}
|
||||
|
||||
@Override
|
||||
@Tool(name = "get_mysql_lock_waits", description = "Get MySQL lock wait information. "
|
||||
+ "Shows current lock waits and blocking transactions. "
|
||||
+ "Useful for diagnosing deadlocks and lock contention issues.")
|
||||
public String getMySqlLockWaits(
|
||||
@ToolParam(description = "Monitor ID of the MySQL instance", required = true) Long monitorId) {
|
||||
|
||||
// Use performance_schema for MySQL 8.0+ (innodb_lock_waits was removed)
|
||||
String sql = "SELECT "
|
||||
+ "r.ENGINE_TRANSACTION_ID as waiting_trx_id, "
|
||||
+ "r.THREAD_ID as waiting_thread, "
|
||||
+ "r.OBJECT_SCHEMA as waiting_schema, "
|
||||
+ "r.OBJECT_NAME as waiting_table, "
|
||||
+ "r.LOCK_TYPE as waiting_lock_type, "
|
||||
+ "r.LOCK_MODE as waiting_lock_mode, "
|
||||
+ "b.ENGINE_TRANSACTION_ID as blocking_trx_id, "
|
||||
+ "b.THREAD_ID as blocking_thread "
|
||||
+ "FROM performance_schema.data_lock_waits w "
|
||||
+ "JOIN performance_schema.data_locks b ON b.ENGINE_LOCK_ID = w.BLOCKING_ENGINE_LOCK_ID "
|
||||
+ "JOIN performance_schema.data_locks r ON r.ENGINE_LOCK_ID = w.REQUESTING_ENGINE_LOCK_ID "
|
||||
+ "LIMIT 20";
|
||||
|
||||
return executeQuery(monitorId, sql, "MySQL Lock Waits");
|
||||
}
|
||||
|
||||
@Override
|
||||
@Tool(name = "get_mysql_global_status", description = "Get MySQL global status variables. "
|
||||
+ "Returns server status matching the given pattern. "
|
||||
+ "Examples: 'Slow%' for slow query stats, 'Threads%' for thread stats.")
|
||||
public String getMySqlGlobalStatus(
|
||||
@ToolParam(description = "Monitor ID of the MySQL instance", required = true) Long monitorId,
|
||||
@ToolParam(description = "Pattern to filter status variables (e.g., 'Slow%', 'Threads%')", required = false) String pattern) {
|
||||
|
||||
String filterPattern = (pattern == null || pattern.isEmpty()) ? "%" : pattern;
|
||||
// Sanitize pattern to prevent SQL injection
|
||||
filterPattern = filterPattern.replaceAll("[^a-zA-Z0-9_%]", "");
|
||||
|
||||
String sql = "SHOW GLOBAL STATUS LIKE '" + filterPattern + "'";
|
||||
|
||||
return executeQuery(monitorId, sql, "MySQL Global Status");
|
||||
}
|
||||
|
||||
@Override
|
||||
@Tool(name = "explain_query", description = "Explain a SELECT query for performance analysis. "
|
||||
+ "ONLY SELECT queries are allowed for security reasons. "
|
||||
+ "Returns the execution plan to help optimize the query.")
|
||||
public String explainQuery(
|
||||
@ToolParam(description = "Monitor ID of the MySQL instance", required = true) Long monitorId,
|
||||
@ToolParam(description = "SELECT query to explain (must start with SELECT)", required = true) String query) {
|
||||
|
||||
if (query == null || query.trim().isEmpty()) {
|
||||
return "Error: Query cannot be empty";
|
||||
}
|
||||
|
||||
String trimmedQuery = query.trim();
|
||||
|
||||
// Security check: only allow SELECT statements
|
||||
if (!trimmedQuery.toUpperCase().startsWith("SELECT")) {
|
||||
return "Error: Only SELECT queries are allowed for EXPLAIN. "
|
||||
+ "The query must start with 'SELECT'.";
|
||||
}
|
||||
|
||||
// Additional security checks
|
||||
String lowerQuery = trimmedQuery.toLowerCase();
|
||||
if (lowerQuery.contains(";") || lowerQuery.contains("--")
|
||||
|| lowerQuery.contains("/*") || lowerQuery.contains("*/")) {
|
||||
return "Error: Query contains invalid characters. "
|
||||
+ "Semicolons and comments are not allowed.";
|
||||
}
|
||||
|
||||
String sql = "EXPLAIN " + trimmedQuery;
|
||||
|
||||
return executeQuery(monitorId, sql, "Query Execution Plan");
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a predefined safe query against the database.
|
||||
*/
|
||||
private String executeQuery(Long monitorId, String sql, String title) {
|
||||
log.info("Executing database query for monitor {}: {}", monitorId, title);
|
||||
|
||||
try {
|
||||
// Get monitor DTO which includes both monitor and params
|
||||
MonitorDto monitorDto = monitorService.getMonitorDto(monitorId);
|
||||
if (monitorDto == null || monitorDto.getMonitor() == null) {
|
||||
return "Error: Monitor not found with ID: " + monitorId;
|
||||
}
|
||||
|
||||
Monitor monitor = monitorDto.getMonitor();
|
||||
|
||||
if (!SUPPORTED_PLATFORMS.contains(monitor.getApp().toLowerCase())) {
|
||||
return "Error: This tool only supports MySQL/MariaDB. "
|
||||
+ "Current monitor type: " + monitor.getApp();
|
||||
}
|
||||
|
||||
// Get connection parameters from MonitorDto
|
||||
List<Param> params = monitorDto.getParams();
|
||||
if (params == null || params.isEmpty()) {
|
||||
return "Error: Monitor has no connection parameters configured.";
|
||||
}
|
||||
|
||||
String host = getParamValue(params, "host");
|
||||
String port = getParamValue(params, "port");
|
||||
String database = getParamValue(params, "database");
|
||||
String username = getParamValue(params, "username");
|
||||
String password = getParamValue(params, "password");
|
||||
|
||||
// Decrypt password if it's encrypted
|
||||
if (password != null && AesUtil.isCiphertext(password)) {
|
||||
password = AesUtil.aesDecode(password);
|
||||
}
|
||||
|
||||
if (host == null || username == null) {
|
||||
return "Error: Monitor connection parameters incomplete. "
|
||||
+ "Ensure host and username are configured.";
|
||||
}
|
||||
|
||||
// Build connection URL
|
||||
String url = buildJdbcUrl(monitor.getApp(), host, port, database);
|
||||
|
||||
// Execute query
|
||||
return executeAndFormat(url, username, password, sql, title);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error executing database query: {}", e.getMessage(), e);
|
||||
return "Error executing query: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private String getParamValue(List<Param> params, String field) {
|
||||
return params.stream()
|
||||
.filter(p -> field.equals(p.getField()))
|
||||
.map(Param::getParamValue)
|
||||
.filter(Objects::nonNull)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private String buildJdbcUrl(String platform, String host, String port, String database) {
|
||||
String effectivePort = (port == null || port.isEmpty()) ? "3306" : port;
|
||||
// host, port and database come from monitor parameters and are concatenated into the url,
|
||||
// so they must not carry url syntax of their own
|
||||
String effectiveDb = JdbcUrlSafetyUtil.requireSafeDatabaseName(database);
|
||||
if (!IpDomainUtil.validateIpDomain(host)) {
|
||||
throw new IllegalArgumentException("Invalid database host: " + host);
|
||||
}
|
||||
if (!CommonUtil.isNumeric(effectivePort)) {
|
||||
throw new IllegalArgumentException("Invalid database port: " + effectivePort);
|
||||
}
|
||||
|
||||
String url = "jdbc:mysql://" + host + ":" + effectivePort + "/" + effectiveDb
|
||||
+ "?useUnicode=true&characterEncoding=utf-8&useSSL=false"
|
||||
+ "&allowPublicKeyRetrieval=true&connectTimeout=5000";
|
||||
JdbcUrlSafetyUtil.requireSafeJdbcUrl(url);
|
||||
return url;
|
||||
}
|
||||
|
||||
private String executeAndFormat(String url, String username, String password,
|
||||
String sql, String title) throws Exception {
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
result.append("=== ").append(title).append(" ===\n\n");
|
||||
|
||||
try (Connection conn = DriverManager.getConnection(url, username, password);
|
||||
Statement stmt = conn.createStatement()) {
|
||||
|
||||
stmt.setQueryTimeout(QUERY_TIMEOUT_SECONDS);
|
||||
stmt.setMaxRows(MAX_ROWS);
|
||||
|
||||
try (ResultSet rs = stmt.executeQuery(sql)) {
|
||||
ResultSetMetaData meta = rs.getMetaData();
|
||||
int columnCount = meta.getColumnCount();
|
||||
|
||||
// Build header
|
||||
StringBuilder header = new StringBuilder();
|
||||
StringBuilder separator = new StringBuilder();
|
||||
for (int i = 1; i <= columnCount; i++) {
|
||||
String columnName = meta.getColumnLabel(i);
|
||||
header.append(String.format("%-20s", columnName));
|
||||
separator.append("-".repeat(20));
|
||||
}
|
||||
result.append(header).append("\n");
|
||||
result.append(separator).append("\n");
|
||||
|
||||
// Build rows
|
||||
int rowCount = 0;
|
||||
while (rs.next()) {
|
||||
StringBuilder row = new StringBuilder();
|
||||
for (int i = 1; i <= columnCount; i++) {
|
||||
String value = rs.getString(i);
|
||||
if (value == null) {
|
||||
value = "NULL";
|
||||
} else if (value.length() > 50) {
|
||||
value = value.substring(0, 47) + "...";
|
||||
}
|
||||
row.append(String.format("%-20s", value));
|
||||
}
|
||||
result.append(row).append("\n");
|
||||
rowCount++;
|
||||
}
|
||||
|
||||
result.append("\n").append("Total: ").append(rowCount).append(" rows");
|
||||
}
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
}
|
||||
@@ -146,7 +146,7 @@ public class MetricsToolsImpl implements MetricsTools {
|
||||
HertzBeat: 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
|
||||
""")
|
||||
|
||||
+76
-147
@@ -18,17 +18,9 @@
|
||||
package org.apache.hertzbeat.ai.tools.impl;
|
||||
|
||||
import com.usthe.sureness.subject.SubjectSum;
|
||||
import java.util.Optional;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.hertzbeat.ai.config.McpContextHolder;
|
||||
import org.apache.hertzbeat.ai.dao.ChatConversationDao;
|
||||
import org.apache.hertzbeat.common.entity.ai.ChatConversation;
|
||||
import org.apache.hertzbeat.common.util.AesUtil;
|
||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||
import org.apache.hertzbeat.manager.pojo.dto.MonitorDto;
|
||||
import org.apache.hertzbeat.manager.pojo.dto.ParamDefineInfo;
|
||||
import org.apache.hertzbeat.manager.service.MonitorService;
|
||||
import org.apache.hertzbeat.manager.service.AppService;
|
||||
import org.apache.hertzbeat.ai.utils.UtilityClass;
|
||||
@@ -40,7 +32,7 @@ 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 tools.jackson.core.type.TypeReference;
|
||||
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -52,79 +44,76 @@ import java.util.Map;
|
||||
@Slf4j
|
||||
@Service
|
||||
public class MonitorToolsImpl implements MonitorTools {
|
||||
|
||||
@Autowired
|
||||
private MonitorService monitorService;
|
||||
@Autowired
|
||||
private AppService appService;
|
||||
@Autowired
|
||||
private ChatConversationDao conversationDao;
|
||||
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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 = """
|
||||
HertzBeat: 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.
|
||||
HertzBeat: 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)
|
||||
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:
|
||||
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)
|
||||
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
|
||||
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)
|
||||
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)
|
||||
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
|
||||
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
|
||||
""")
|
||||
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) {
|
||||
@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) {
|
||||
@@ -141,7 +130,7 @@ public class MonitorToolsImpl implements MonitorTools {
|
||||
log.debug("Current security subject: {}", subjectSum);
|
||||
|
||||
Page<Monitor> result = monitorService.getMonitors(
|
||||
ids, app, search, status, sort, order, pageIndex, pageSize, labels);
|
||||
ids, app, search, status, sort, order, pageIndex, pageSize, labels);
|
||||
log.debug("MonitorService.getMonitors result: {}", result);
|
||||
|
||||
StringBuilder response = new StringBuilder();
|
||||
@@ -151,10 +140,10 @@ public class MonitorToolsImpl implements MonitorTools {
|
||||
// Include statistics if requested
|
||||
if (includeStats) {
|
||||
// Get status distribution by calling with different status values
|
||||
long onlineCount = monitorService.getMonitors(null, app, search, (byte) 1, "id", "asc", 0, 1, labels).getTotalElements();
|
||||
long offlineCount = monitorService.getMonitors(null, app, search, (byte) 2, "id", "asc", 0, 1, labels).getTotalElements();
|
||||
long unreachableCount = monitorService.getMonitors(null, app, search, (byte) 3, "id", "asc", 0, 1, labels).getTotalElements();
|
||||
long pausedCount = monitorService.getMonitors(null, app, search, (byte) 0, "id", "asc", 0, 1, labels).getTotalElements();
|
||||
long onlineCount = monitorService.getMonitors(null, app, search, (byte) 1, null, null, 0, 1000, labels).getTotalElements();
|
||||
long offlineCount = monitorService.getMonitors(null, app, search, (byte) 2, null, null, 0, 1000, labels).getTotalElements();
|
||||
long unreachableCount = monitorService.getMonitors(null, app, search, (byte) 3, null, null, 0, 1000, labels).getTotalElements();
|
||||
long pausedCount = monitorService.getMonitors(null, app, search, (byte) 0, null, null, 0, 1000, labels).getTotalElements();
|
||||
|
||||
response.append("STATUS OVERVIEW:\n");
|
||||
response.append("- Online: ").append(onlineCount).append("\n");
|
||||
@@ -171,20 +160,19 @@ public class MonitorToolsImpl implements MonitorTools {
|
||||
}
|
||||
|
||||
response.append("Query Results: ").append(result.getContent().size())
|
||||
.append(" monitors (Total: ").append(result.getTotalElements()).append(")\n");
|
||||
.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("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(" | Instance: ").append(monitor.getInstance())
|
||||
.append(" | Status: ").append(UtilityClass.getStatusText(monitor.getStatus()));
|
||||
.append(" | Name: ").append(monitor.getName())
|
||||
.append(" | Type: ").append(monitor.getApp())
|
||||
.append(" | Instance: ").append(monitor.getInstance())
|
||||
.append(" | Status: ").append(UtilityClass.getStatusText(monitor.getStatus()));
|
||||
|
||||
// Add creation date for better context
|
||||
if (monitor.getGmtCreate() != null) {
|
||||
@@ -236,59 +224,15 @@ public class MonitorToolsImpl implements MonitorTools {
|
||||
- Redis: {"host":"redis.server.com", "port":"6379", "password":"xxx"}
|
||||
""")
|
||||
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 = "Collection interval in seconds (default: 600)", required = false) Integer intervals,
|
||||
@ToolParam(description = "Monitor-specific parameters as JSON string. "
|
||||
+ "Use get_monitor_additional_params to see required fields. "
|
||||
+ "Example: {\"host\":\"192.168.1.1\", \"port\":\"22\", \"username\":\"root\"}",
|
||||
required = true) String params,
|
||||
@ToolParam(description = "Monitor description (optional)", required = false) String description) {
|
||||
return addMonitorProtected(null, name, app, intervals, params, description);
|
||||
}
|
||||
@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 = "Collection interval in seconds (default: 600)", required = false) Integer intervals,
|
||||
@ToolParam(description = "Monitor-specific parameters as JSON string. "
|
||||
+ "Use get_monitor_additional_params to see required fields. "
|
||||
+ "Example: {\"host\":\"192.168.1.1\", \"port\":\"22\", \"username\":\"root\"}",
|
||||
required = true) String params,
|
||||
@ToolParam(description = "Monitor description (optional)", required = false) String description) {
|
||||
|
||||
@Override
|
||||
@Tool(name = "add_monitor_protected", description = """
|
||||
HertzBeat: 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.
|
||||
If the conversation ID is unknown, please call the `add_monitor` method.
|
||||
**********
|
||||
|
||||
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: Use the 'params' parameter to pass all monitor-specific configuration.
|
||||
The params should be a JSON string containing key-value pairs for the monitor type.
|
||||
Use get_monitor_additional_params tool to see what parameters are required for each monitor type.
|
||||
|
||||
PARAMS EXAMPLES:
|
||||
- Website: {"host":"example.com", "port":"443", "uri":"/api/health", "ssl":"true", "method":"GET"}
|
||||
- Linux: {"host":"192.168.1.10", "port":"22", "username":"root", "password":"xxx"}
|
||||
- MySQL: {"host":"db.server.com", "port":"3306", "username":"admin", "password":"xxx", "database":"mydb"}
|
||||
- Redis: {"host":"redis.server.com", "port":"6379", "password":"xxx"}
|
||||
""")
|
||||
public String addMonitorProtected(
|
||||
@ToolParam(description = "The id for current conversation (required)", required = true) Long conversationId,
|
||||
@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 = "Collection interval in seconds (default: 600)", required = false) Integer intervals,
|
||||
@ToolParam(description = "Monitor-specific parameters as JSON string. "
|
||||
+ "Use get_monitor_additional_params to see required fields. "
|
||||
+ "Example: {\"host\":\"192.168.1.1\", \"port\":\"22\", \"username\":\"root\"}",
|
||||
required = true) String params,
|
||||
@ToolParam(description = "Monitor description (optional)", required = false) String description) {
|
||||
try {
|
||||
log.info("Adding monitor: name={}, app={}", name, app);
|
||||
|
||||
@@ -307,22 +251,9 @@ public class MonitorToolsImpl implements MonitorTools {
|
||||
if (intervals == null || intervals < 10) {
|
||||
intervals = 600;
|
||||
}
|
||||
|
||||
// Parse params to extract host and port for instance
|
||||
List<Param> paramList = parseParams(params);
|
||||
|
||||
// Query and add sensitive parameters
|
||||
if (conversationId != null) {
|
||||
Optional<ChatConversation> chatConversation = conversationDao.findById(conversationId);
|
||||
if (chatConversation.isPresent() && StringUtils.isNotEmpty(chatConversation.get().getSecurityData())) {
|
||||
List<Param> securityParams = JsonUtil.fromJson(
|
||||
AesUtil.aesDecode(chatConversation.get().getSecurityData()),
|
||||
new TypeReference<List<Param>>() {
|
||||
});
|
||||
if (CollectionUtils.isNotEmpty(securityParams)) {
|
||||
paramList.addAll(securityParams);
|
||||
}
|
||||
}
|
||||
}
|
||||
String host = paramList.stream()
|
||||
.filter(p -> "host".equals(p.getField()))
|
||||
.map(Param::getParamValue)
|
||||
@@ -349,9 +280,7 @@ public class MonitorToolsImpl implements MonitorTools {
|
||||
|
||||
// Validate that all required parameters for this monitor type are provided
|
||||
try {
|
||||
MonitorDto monitorDto = new MonitorDto();
|
||||
monitorDto.setMonitor(monitor);
|
||||
monitorDto.setParams(paramList);
|
||||
MonitorDto monitorDto = MonitorDto.builder().monitor(monitor).params(paramList).build();
|
||||
monitorService.validate(monitorDto, false);
|
||||
} catch (IllegalArgumentException argumentException) {
|
||||
if (argumentException.getMessage().contains("required")) {
|
||||
@@ -527,7 +456,7 @@ public class MonitorToolsImpl implements MonitorTools {
|
||||
}
|
||||
|
||||
// Get parameter definitions from app service
|
||||
List<ParamDefineInfo> paramDefines = appService.getAppParamDefines(app.toLowerCase().trim());
|
||||
List<ParamDefine> paramDefines = appService.getAppParamDefines(app.toLowerCase().trim());
|
||||
|
||||
if (paramDefines == null || paramDefines.isEmpty()) {
|
||||
return String.format("No parameter definitions found for monitor type '%s'. "
|
||||
@@ -539,7 +468,7 @@ public class MonitorToolsImpl implements MonitorTools {
|
||||
response.append(String.format("Parameter Definitions for Monitor Type '%s' (Total: %d):\n\n",
|
||||
app, paramDefines.size()));
|
||||
|
||||
for (ParamDefineInfo paramDefine : paramDefines) {
|
||||
for (ParamDefine paramDefine : paramDefines) {
|
||||
response.append("• Field: ").append(paramDefine.getField()).append("\n");
|
||||
|
||||
// Add display name if available
|
||||
|
||||
@@ -1,333 +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.tools.impl;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.service.SopScheduleService;
|
||||
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.ai.utils.SopMessageUtil;
|
||||
import org.apache.hertzbeat.ai.tools.ScheduleTools;
|
||||
import org.apache.hertzbeat.common.entity.ai.SopSchedule;
|
||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.ai.tool.annotation.ToolParam;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tools.jackson.core.type.TypeReference;
|
||||
|
||||
/**
|
||||
* Implementation of ScheduleTools for AI-driven schedule management.
|
||||
* Allows AI to create and manage scheduled tasks through natural language.
|
||||
*
|
||||
* The conversationId is injected into the System Prompt so AI can pass it
|
||||
* when calling these tools.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class ScheduleToolsImpl implements ScheduleTools {
|
||||
|
||||
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
private final SopScheduleService scheduleService;
|
||||
private final SkillRegistry skillRegistry;
|
||||
|
||||
@Autowired
|
||||
public ScheduleToolsImpl(@Lazy SopScheduleService scheduleService,
|
||||
@Lazy SkillRegistry skillRegistry) {
|
||||
this.scheduleService = scheduleService;
|
||||
this.skillRegistry = skillRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Tool(name = "createScheduleForConversation",
|
||||
description = "Create a scheduled task to automatically execute a skill at specified times. "
|
||||
+ "IMPORTANT: You must pass the conversationId from the system context (shown as 'Current Conversation ID' in the prompt). "
|
||||
+ "The cron expression should be in 6-digit Spring format: 'seconds minutes hours dayOfMonth month dayOfWeek'. "
|
||||
+ "Common examples: '0 0 9 * * ?' (daily at 9:00), '0 30 8 * * MON-FRI' (weekdays at 8:30).")
|
||||
public String createSchedule(
|
||||
@ToolParam(description = "Name of the skill to schedule (e.g., 'daily_inspection', 'mysql_slow_query_diagnosis')",
|
||||
required = true) String skillName,
|
||||
@ToolParam(description = "Cron expression in 6-digit Spring format. Examples: '0 0 9 * * ?' for daily 9am",
|
||||
required = true) String cronExpression,
|
||||
@ToolParam(description = "Human-readable description of what this schedule does",
|
||||
required = false) String description) {
|
||||
// This method signature is kept for interface compatibility
|
||||
// AI should use createScheduleWithConversation instead
|
||||
return SopMessageUtil.getMessage("schedule.error.use.with.conversation");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create schedule with explicit conversationId - the primary method AI should call.
|
||||
*/
|
||||
@Tool(name = "createScheduleWithConversation",
|
||||
description = "Create a scheduled task for a specific conversation. "
|
||||
+ "Use the conversationId from the system context. "
|
||||
+ "Pass skill parameters as a JSON object when the skill requires inputs. "
|
||||
+ "The cron expression should be in 6-digit Spring format.")
|
||||
public String createScheduleWithConversation(
|
||||
@ToolParam(description = "Conversation ID from the system context", required = true) Long conversationId,
|
||||
@ToolParam(description = "Name of the skill to schedule (e.g., 'daily_inspection')", required = true) String skillName,
|
||||
@ToolParam(description = "Cron expression in Spring format (e.g., '0 0 9 * * ?')", required = true) String cronExpression,
|
||||
@ToolParam(description = "Description of the schedule", required = false) String description,
|
||||
@ToolParam(description = "Skill parameters as a JSON object (e.g., '{\"monitorId\":123}')",
|
||||
required = false) String paramsJson) {
|
||||
|
||||
log.info("AI creating schedule: conversationId={}, skill={}, cron={}, desc={}",
|
||||
conversationId, skillName, cronExpression, description);
|
||||
|
||||
// Validate skill exists
|
||||
SopDefinition skill = skillRegistry.getSkill(skillName);
|
||||
if (skill == null) {
|
||||
String available = String.join(", ",
|
||||
skillRegistry.getAllSkills().stream()
|
||||
.map(s -> s.getName())
|
||||
.toList());
|
||||
return SopMessageUtil.getMessage("schedule.error.skill.not.found",
|
||||
new Object[]{skillName, available}, null);
|
||||
}
|
||||
|
||||
if (conversationId == null || conversationId <= 0) {
|
||||
return SopMessageUtil.getMessage("schedule.error.invalid.conversation");
|
||||
}
|
||||
|
||||
try {
|
||||
Map<String, Object> params = parseSkillParams(paramsJson);
|
||||
validateRequiredParameters(skill, params);
|
||||
String serializedParams = params.isEmpty() ? null : JsonUtil.toJson(params);
|
||||
|
||||
// Parameters are part of a schedule's identity so the same skill and cron can target different inputs.
|
||||
List<SopSchedule> existing = scheduleService.getSchedulesByConversation(conversationId);
|
||||
boolean duplicate = existing.stream()
|
||||
.anyMatch(schedule -> Objects.equals(schedule.getSopName(), skillName)
|
||||
&& Objects.equals(schedule.getCronExpression(), cronExpression)
|
||||
&& hasSameParams(schedule.getSopParams(), params));
|
||||
if (duplicate) {
|
||||
return SopMessageUtil.getMessage("schedule.create.duplicate",
|
||||
new Object[]{skillName, cronExpression}, null)
|
||||
+ "\n\n" + SopMessageUtil.getMessage("schedule.create.duplicate.hint");
|
||||
}
|
||||
|
||||
SopSchedule schedule = new SopSchedule();
|
||||
schedule.setConversationId(conversationId);
|
||||
schedule.setSopName(skillName);
|
||||
schedule.setCronExpression(cronExpression);
|
||||
schedule.setSopParams(serializedParams);
|
||||
schedule.setEnabled(true);
|
||||
|
||||
SopSchedule created = scheduleService.createSchedule(schedule);
|
||||
|
||||
StringBuilder response = new StringBuilder();
|
||||
response.append(SopMessageUtil.getMessage("schedule.create.success")).append("\n\n");
|
||||
response.append("**").append(SopMessageUtil.getMessage("schedule.detail.title")).append("**:\n");
|
||||
response.append("- **").append(SopMessageUtil.getMessage("schedule.detail.skill")).append("**: ").append(skillName).append("\n");
|
||||
response.append("- **").append(SopMessageUtil.getMessage("schedule.detail.cron")).append("**: ").append(cronExpression).append("\n");
|
||||
response.append("- **").append(SopMessageUtil.getMessage("schedule.detail.id")).append("**: ").append(created.getId()).append("\n");
|
||||
response.append("- **").append(SopMessageUtil.getMessage("schedule.detail.status")).append("**: ").append(SopMessageUtil.getMessage("schedule.detail.enabled")).append("\n");
|
||||
|
||||
if (created.getNextRunTime() != null) {
|
||||
response.append("- **").append(SopMessageUtil.getMessage("schedule.detail.next.run")).append("**: ").append(created.getNextRunTime().format(TIME_FORMATTER)).append("\n");
|
||||
}
|
||||
|
||||
if (description != null && !description.isEmpty()) {
|
||||
response.append("- **").append(SopMessageUtil.getMessage("schedule.detail.description")).append("**: ").append(description).append("\n");
|
||||
}
|
||||
|
||||
response.append("\n").append(SopMessageUtil.getMessage("schedule.create.effective"));
|
||||
|
||||
return response.toString();
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
return SopMessageUtil.getMessage("schedule.error.create.failed") + ": " + e.getMessage();
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to create schedule", e);
|
||||
return SopMessageUtil.getMessage("schedule.error.create.failed") + ": " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> parseSkillParams(String paramsJson) {
|
||||
if (paramsJson == null || paramsJson.isBlank()) {
|
||||
return Map.of();
|
||||
}
|
||||
Map<String, Object> params;
|
||||
try {
|
||||
params = JsonUtil.fromJson(paramsJson, new TypeReference<>() {});
|
||||
} catch (RuntimeException e) {
|
||||
throw new IllegalArgumentException("Skill parameters must be a valid JSON object", e);
|
||||
}
|
||||
if (params == null) {
|
||||
throw new IllegalArgumentException("Skill parameters must be a valid JSON object");
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
private void validateRequiredParameters(SopDefinition skill, Map<String, Object> params) {
|
||||
if (skill.getParameters() == null) {
|
||||
return;
|
||||
}
|
||||
for (SopParameter parameter : skill.getParameters()) {
|
||||
Object value = params.get(parameter.getName());
|
||||
if (isMissing(value)) {
|
||||
value = parameter.getDefaultValue();
|
||||
}
|
||||
if (parameter.isRequired() && isMissing(value)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Required skill parameter is missing: " + parameter.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isMissing(Object value) {
|
||||
return value == null || value instanceof String text && text.isBlank();
|
||||
}
|
||||
|
||||
private boolean hasSameParams(String existingJson, Map<String, Object> params) {
|
||||
if (existingJson == null || existingJson.isBlank()) {
|
||||
return params.isEmpty();
|
||||
}
|
||||
try {
|
||||
Map<String, Object> existingParams = JsonUtil.fromJson(existingJson, new TypeReference<>() {});
|
||||
return Objects.equals(existingParams, params);
|
||||
} catch (RuntimeException e) {
|
||||
log.warn("Failed to parse parameters of an existing schedule", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Tool(name = "listSchedulesForConversation",
|
||||
description = "List all scheduled tasks for a specific conversation. "
|
||||
+ "Use the conversationId from the system context.")
|
||||
public String listSchedules() {
|
||||
// This method signature is kept for interface compatibility
|
||||
return SopMessageUtil.getMessage("schedule.error.use.with.conversation");
|
||||
}
|
||||
|
||||
@Tool(name = "listSchedulesWithConversation",
|
||||
description = "List all scheduled tasks for a specific conversation.")
|
||||
public String listSchedulesWithConversation(
|
||||
@ToolParam(description = "Conversation ID from the system context", required = true) Long conversationId) {
|
||||
|
||||
log.info("AI listing schedules for conversation: {}", conversationId);
|
||||
|
||||
if (conversationId == null || conversationId <= 0) {
|
||||
return SopMessageUtil.getMessage("schedule.error.invalid.conversation");
|
||||
}
|
||||
|
||||
try {
|
||||
List<SopSchedule> schedules = scheduleService.getSchedulesByConversation(conversationId);
|
||||
|
||||
if (schedules.isEmpty()) {
|
||||
return SopMessageUtil.getMessage("schedule.list.empty") + "\n\n"
|
||||
+ SopMessageUtil.getMessage("schedule.list.hint");
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("**").append(SopMessageUtil.getMessage("schedule.list.title")).append("** (")
|
||||
.append(SopMessageUtil.getMessage("schedule.list.count", new Object[]{schedules.size()}, null))
|
||||
.append("):\n\n");
|
||||
|
||||
for (SopSchedule schedule : schedules) {
|
||||
String statusLabel = schedule.getEnabled()
|
||||
? SopMessageUtil.getMessage("schedule.detail.enabled")
|
||||
: SopMessageUtil.getMessage("schedule.detail.disabled");
|
||||
|
||||
sb.append("### ").append(schedule.getId()).append(". ").append(schedule.getSopName());
|
||||
sb.append(" (").append(statusLabel).append(")\n");
|
||||
sb.append("- **").append(SopMessageUtil.getMessage("schedule.detail.cron")).append("**: `").append(schedule.getCronExpression()).append("`\n");
|
||||
sb.append("- **").append(SopMessageUtil.getMessage("schedule.detail.status")).append("**: ").append(statusLabel).append("\n");
|
||||
|
||||
if (schedule.getLastRunTime() != null) {
|
||||
sb.append("- **").append(SopMessageUtil.getMessage("schedule.detail.last.run")).append("**: ").append(schedule.getLastRunTime().format(TIME_FORMATTER)).append("\n");
|
||||
}
|
||||
if (schedule.getNextRunTime() != null) {
|
||||
sb.append("- **").append(SopMessageUtil.getMessage("schedule.detail.next.run")).append("**: ").append(schedule.getNextRunTime().format(TIME_FORMATTER)).append("\n");
|
||||
}
|
||||
sb.append("\n");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to list schedules", e);
|
||||
return "Error: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Tool(name = "deleteSchedule",
|
||||
description = "Delete a scheduled task by its ID. Use listSchedulesForConversation first to find the task ID.")
|
||||
public String deleteSchedule(
|
||||
@ToolParam(description = "ID of the schedule to delete", required = true) Long scheduleId) {
|
||||
|
||||
log.info("AI deleting schedule: {}", scheduleId);
|
||||
|
||||
try {
|
||||
SopSchedule schedule = scheduleService.getSchedule(scheduleId);
|
||||
if (schedule == null) {
|
||||
return SopMessageUtil.getMessage("schedule.error.not.found", new Object[]{scheduleId}, null);
|
||||
}
|
||||
|
||||
String skillName = schedule.getSopName();
|
||||
scheduleService.deleteSchedule(scheduleId);
|
||||
|
||||
return SopMessageUtil.getMessage("schedule.delete.success", new Object[]{scheduleId, skillName}, null);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to delete schedule {}", scheduleId, e);
|
||||
return "Error: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Tool(name = "toggleSchedule",
|
||||
description = "Enable or disable a scheduled task. Use listSchedulesForConversation first to find the task ID.")
|
||||
public String toggleSchedule(
|
||||
@ToolParam(description = "ID of the schedule to toggle", required = true) Long scheduleId,
|
||||
@ToolParam(description = "true to enable, false to disable", required = true) boolean enabled) {
|
||||
|
||||
log.info("AI toggling schedule {} to enabled={}", scheduleId, enabled);
|
||||
|
||||
try {
|
||||
SopSchedule schedule = scheduleService.getSchedule(scheduleId);
|
||||
if (schedule == null) {
|
||||
return SopMessageUtil.getMessage("schedule.error.not.found", new Object[]{scheduleId}, null);
|
||||
}
|
||||
|
||||
SopSchedule updated = scheduleService.toggleSchedule(scheduleId, enabled);
|
||||
|
||||
String messageKey = enabled ? "schedule.toggle.enabled" : "schedule.toggle.disabled";
|
||||
String statusLabel = enabled
|
||||
? SopMessageUtil.getMessage("schedule.detail.enabled")
|
||||
: SopMessageUtil.getMessage("schedule.detail.disabled");
|
||||
|
||||
return SopMessageUtil.getMessage(messageKey, new Object[]{scheduleId, updated.getSopName()}, null)
|
||||
+ "\n\n" + SopMessageUtil.getMessage("schedule.toggle.status") + ": " + statusLabel;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to toggle schedule {}", scheduleId, e);
|
||||
return "Error: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.tools.impl;
|
||||
|
||||
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.OutputType;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopDefinition;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopParameter;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopResult;
|
||||
import org.apache.hertzbeat.ai.sop.registry.SkillRegistry;
|
||||
import org.apache.hertzbeat.ai.tools.SkillTools;
|
||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.ai.tool.annotation.ToolParam;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Implementation of SkillTools for AI-driven skill execution.
|
||||
* Provides a unified interface for AI to discover and execute diagnostic skills.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class SkillToolsImpl implements SkillTools {
|
||||
|
||||
/**
|
||||
* Marker prefix for report-type outputs that should be displayed directly to users.
|
||||
* When AI returns content with this prefix, frontend should render it directly
|
||||
* without waiting for AI to process it further.
|
||||
*/
|
||||
public static final String SKILL_REPORT_MARKER = "[[SKILL_REPORT]]";
|
||||
|
||||
private final SkillRegistry skillRegistry;
|
||||
private final SopEngine sopEngine;
|
||||
|
||||
@Autowired
|
||||
public SkillToolsImpl(@Lazy SkillRegistry skillRegistry, @Lazy SopEngine sopEngine) {
|
||||
this.skillRegistry = skillRegistry;
|
||||
this.sopEngine = sopEngine;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Tool(name = "listSkills",
|
||||
description = "List all available diagnostic skills. Returns skill names, descriptions, "
|
||||
+ "and required parameters. Use this to discover what diagnostic capabilities are available "
|
||||
+ "before executing a skill with executeSkill.")
|
||||
public String listSkills() {
|
||||
List<SopDefinition> skills = skillRegistry.getAllSkills();
|
||||
|
||||
if (skills.isEmpty()) {
|
||||
return "No diagnostic skills available.";
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Available Diagnostic Skills:\n\n");
|
||||
|
||||
for (SopDefinition skill : skills) {
|
||||
sb.append("- **").append(skill.getName()).append("**\n");
|
||||
sb.append(" Description: ").append(skill.getDescription()).append("\n");
|
||||
sb.append(" Version: ").append(skill.getVersion()).append("\n");
|
||||
|
||||
// List required parameters
|
||||
if (skill.getParameters() != null && !skill.getParameters().isEmpty()) {
|
||||
sb.append(" Parameters:\n");
|
||||
for (SopParameter param : skill.getParameters()) {
|
||||
sb.append(" - ").append(param.getName());
|
||||
if (param.isRequired()) {
|
||||
sb.append(" (required)");
|
||||
}
|
||||
sb.append(": ").append(param.getDescription()).append("\n");
|
||||
}
|
||||
}
|
||||
sb.append("\n");
|
||||
}
|
||||
|
||||
sb.append("Usage: Call executeSkill with skillName and params (JSON format).\n");
|
||||
sb.append("Example: executeSkill(\"mysql_slow_query_diagnosis\", \"{\\\"monitorId\\\": 123}\")");
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@Tool(name = "executeSkill",
|
||||
description = "Execute a diagnostic skill. For skills requiring monitorId, first use queryMonitors "
|
||||
+ "to find the target monitor's ID. Report-type skills will return results directly to the user "
|
||||
+ "without further AI processing. Use listSkills to see available skills.")
|
||||
public String executeSkill(
|
||||
@ToolParam(description = "Name of the skill to execute (e.g., mysql_slow_query_diagnosis)",
|
||||
required = true) String skillName,
|
||||
@ToolParam(description = "JSON string with skill parameters (e.g., {\"monitorId\": 123})",
|
||||
required = false) String paramsJson) {
|
||||
|
||||
log.info("Executing skill: {} with params: {}", skillName, paramsJson);
|
||||
|
||||
// Get skill definition
|
||||
SopDefinition skill = skillRegistry.getSkill(skillName);
|
||||
if (skill == null) {
|
||||
String available = skillRegistry.getAllSkills().stream()
|
||||
.map(SopDefinition::getName)
|
||||
.collect(Collectors.joining(", "));
|
||||
return "Error: Skill '" + skillName + "' not found. Available skills: " + available;
|
||||
}
|
||||
|
||||
// Parse parameters
|
||||
Map<String, Object> params = parseParams(paramsJson);
|
||||
if (params == null) {
|
||||
return "Error: Skill parameters must be a valid JSON object.";
|
||||
}
|
||||
|
||||
// Validate required parameters
|
||||
if (skill.getParameters() != null) {
|
||||
for (SopParameter paramDef : skill.getParameters()) {
|
||||
if (paramDef.isRequired()) {
|
||||
Object value = params.get(paramDef.getName());
|
||||
if (value == null || value instanceof String text && text.isBlank()) {
|
||||
return "Error: Required parameter '" + paramDef.getName() + "' is missing. "
|
||||
+ "Description: " + paramDef.getDescription();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Execute the skill
|
||||
SopResult result = sopEngine.executeSync(skill, params);
|
||||
|
||||
// Check output type
|
||||
if ("SUCCESS".equals(result.getStatus())
|
||||
&& result.getOutputType() == OutputType.REPORT
|
||||
&& result.getContent() != null) {
|
||||
// Report type: return with marker for direct display to user
|
||||
log.info("Skill {} returned report-type output, marking for direct display", skillName);
|
||||
return SKILL_REPORT_MARKER + "\n" + result.getContent();
|
||||
}
|
||||
|
||||
// For other types, return AI-friendly format for further processing
|
||||
return result.toAiResponse();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to execute skill {}: {}", skillName, e.getMessage(), e);
|
||||
return "Error executing skill '" + skillName + "': " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> parseParams(String paramsJson) {
|
||||
if (paramsJson == null || paramsJson.trim().isEmpty()) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
try {
|
||||
return JsonUtil.fromJson(paramsJson, Map.class);
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to parse params JSON: {}", paramsJson);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.utils;
|
||||
|
||||
import java.util.Locale;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Utility class for SOP internationalization messages.
|
||||
* Provides static access to MessageSource for POJOs like SopResult.
|
||||
*/
|
||||
@Component
|
||||
public class SopMessageUtil {
|
||||
|
||||
private static MessageSource messageSource;
|
||||
|
||||
public SopMessageUtil(@Qualifier("sopMessageSource") MessageSource messageSource) {
|
||||
SopMessageUtil.messageSource = messageSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get message by code using current locale.
|
||||
*/
|
||||
public static String getMessage(String code) {
|
||||
return getMessage(code, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get message by code for specific language.
|
||||
* @param code message code
|
||||
* @param language language code: "zh", "en"
|
||||
*/
|
||||
public static String getMessage(String code, String language) {
|
||||
return getMessage(code, null, language);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get message by code with arguments for specific language.
|
||||
*/
|
||||
public static String getMessage(String code, Object[] args, String language) {
|
||||
if (messageSource == null) {
|
||||
return code;
|
||||
}
|
||||
|
||||
Locale locale;
|
||||
if ("en".equalsIgnoreCase(language) || "english".equalsIgnoreCase(language)) {
|
||||
locale = Locale.ENGLISH;
|
||||
} else if ("zh".equalsIgnoreCase(language) || "chinese".equalsIgnoreCase(language)) {
|
||||
locale = Locale.CHINESE;
|
||||
} else {
|
||||
// Use system default locale (set by user's SystemConfig)
|
||||
// instead of LocaleContextHolder which only works in HTTP request context
|
||||
locale = Locale.getDefault();
|
||||
}
|
||||
|
||||
try {
|
||||
return messageSource.getMessage(code, args, code, locale);
|
||||
} catch (Exception e) {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get locale from language code.
|
||||
*/
|
||||
public static Locale getLocale(String language) {
|
||||
if ("en".equalsIgnoreCase(language) || "english".equalsIgnoreCase(language)) {
|
||||
return Locale.ENGLISH;
|
||||
}
|
||||
return Locale.CHINESE;
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,9 @@
|
||||
|
||||
package org.apache.hertzbeat.ai.utils;
|
||||
|
||||
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 lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.time.Instant;
|
||||
@@ -27,14 +28,9 @@ import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.apache.hertzbeat.manager.pojo.dto.Hierarchy;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* Utility class providing helper methods for field expression validation, parsing,
|
||||
@@ -45,21 +41,13 @@ import tools.jackson.databind.ObjectMapper;
|
||||
@Slf4j
|
||||
@lombok.experimental.UtilityClass
|
||||
public class UtilityClass {
|
||||
|
||||
private static final Pattern SINGLE_EQUALS_PATTERN = Pattern.compile("(?<![<>=!])=(?!=)");
|
||||
private static final Pattern UPPERCASE_LOGICAL_PATTERN = Pattern.compile("\\b(AND|OR)\\b");
|
||||
private static final Pattern FUNCTION_FIELD_PATTERN = Pattern.compile(
|
||||
"!?\\b(?:equals|contains|matches|exists)\\s*\\(\\s*([a-zA-Z_][a-zA-Z0-9_]*)");
|
||||
private static final Pattern COMPARISON_FIELD_PATTERN = Pattern.compile(
|
||||
"(?:^|[\\s(])([a-zA-Z_][a-zA-Z0-9_]*)\\s*(?:>=|<=|==|!=|>|<)");
|
||||
private static final Pattern QUOTED_VALUE_PATTERN = Pattern.compile("\"[^\"]*\"|'[^']*'");
|
||||
|
||||
|
||||
/**
|
||||
* 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);
|
||||
@@ -123,26 +111,31 @@ public class UtilityClass {
|
||||
* 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
|
||||
|
||||
if (tempExpression.contains("&&") || tempExpression.contains("||")) {
|
||||
String invalidOp = tempExpression.contains("&&") ? "&&" : "||";
|
||||
return String.format("Error: Use 'and'/'or' instead of '%s' for logical operations", invalidOp);
|
||||
}
|
||||
|
||||
Matcher uppercaseLogicalMatcher = UPPERCASE_LOGICAL_PATTERN.matcher(tempExpression);
|
||||
if (uppercaseLogicalMatcher.find()) {
|
||||
return String.format("Error: Use lowercase '%s' for logical operations",
|
||||
uppercaseLogicalMatcher.group(1).toLowerCase());
|
||||
}
|
||||
|
||||
// Reject only a standalone equals sign without rejecting >=, <=, ==, or !=.
|
||||
if (SINGLE_EQUALS_PATTERN.matcher(tempExpression).find()) {
|
||||
return "Error: Use '==' for equality comparison, not '='";
|
||||
// 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
|
||||
@@ -157,6 +150,11 @@ public class UtilityClass {
|
||||
* 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.";
|
||||
@@ -239,7 +237,7 @@ public class UtilityClass {
|
||||
|
||||
String[] pairs = input.split(",");
|
||||
for (String pair : pairs) {
|
||||
String[] keyValue = pair.split(":", 2);
|
||||
String[] keyValue = pair.split(":");
|
||||
if (keyValue.length == 2) {
|
||||
result.put(keyValue[0].trim(), keyValue[1].trim());
|
||||
}
|
||||
@@ -301,22 +299,34 @@ public class UtilityClass {
|
||||
* Handles simple cases like "field > 80", "equals(field, 'value')", complex expressions
|
||||
*/
|
||||
public List<String> extractFieldNamesFromConditions(String fieldConditions) {
|
||||
Set<String> fieldNames = new LinkedHashSet<>();
|
||||
List<String> fieldNames = new ArrayList<>();
|
||||
|
||||
// Extract the first function argument without splitting parentheses and bypassing field validation.
|
||||
Matcher functionMatcher = FUNCTION_FIELD_PATTERN.matcher(fieldConditions);
|
||||
while (functionMatcher.find()) {
|
||||
fieldNames.add(functionMatcher.group(1));
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove quoted values before scanning comparisons to avoid treating text such as "value > 1" as a field.
|
||||
Matcher comparisonMatcher = COMPARISON_FIELD_PATTERN.matcher(
|
||||
QUOTED_VALUE_PATTERN.matcher(fieldConditions).replaceAll(""));
|
||||
while (comparisonMatcher.find()) {
|
||||
fieldNames.add(comparisonMatcher.group(1));
|
||||
}
|
||||
|
||||
return new ArrayList<>(fieldNames);
|
||||
return fieldNames;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -381,8 +391,7 @@ public class UtilityClass {
|
||||
} else {
|
||||
// Category, app, or metric node
|
||||
// Determine node type based on children
|
||||
List<Hierarchy> children = hierarchy.getChildren();
|
||||
boolean hasLeafChildren = children != null && children.stream()
|
||||
boolean hasLeafChildren = hierarchy.getChildren().stream()
|
||||
.anyMatch(child -> child.getIsLeaf() != null && child.getIsLeaf());
|
||||
|
||||
if (hasLeafChildren) {
|
||||
@@ -393,9 +402,9 @@ public class UtilityClass {
|
||||
node.put("description", "Application with available metrics");
|
||||
}
|
||||
|
||||
if (children != null && !children.isEmpty()) {
|
||||
if (hierarchy.getChildren() != null && !hierarchy.getChildren().isEmpty()) {
|
||||
ArrayNode childrenArray = mapper.createArrayNode();
|
||||
for (Hierarchy child : children) {
|
||||
for (Hierarchy child : hierarchy.getChildren()) {
|
||||
childrenArray.add(formatHierarchyAsJson(mapper, child));
|
||||
}
|
||||
node.set("children", childrenArray);
|
||||
@@ -405,7 +414,7 @@ public class UtilityClass {
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Format timestamp to readable format
|
||||
*/
|
||||
|
||||
@@ -1,68 +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.
|
||||
|
||||
# SOP Result Messages - Default (Chinese)
|
||||
sop.result.title=SOP执行结果
|
||||
sop.result.name=SOP名称
|
||||
sop.result.duration=耗时
|
||||
sop.result.summary=摘要
|
||||
sop.result.error=错误
|
||||
sop.result.report.title=详细报告
|
||||
sop.result.data.title=返回数据
|
||||
sop.result.simple.complete=操作已完成。
|
||||
sop.result.report.success=报告生成成功
|
||||
sop.result.data.success=数据获取成功
|
||||
sop.result.action.pending=等待确认操作
|
||||
sop.result.operation.success=操作已完成
|
||||
|
||||
# LLM Language Instructions
|
||||
sop.llm.language.instruction=重要:请使用中文回复。
|
||||
|
||||
# Schedule Messages
|
||||
schedule.create.success=已成功创建定时任务!
|
||||
schedule.create.duplicate=该定时任务已存在(技能: {0}, 执行时间: {1}),无需重复创建。
|
||||
schedule.create.duplicate.hint=如需创建不同时间的任务,请指定不同的执行时间。
|
||||
schedule.detail.title=任务详情
|
||||
schedule.detail.skill=技能
|
||||
schedule.detail.cron=执行时间
|
||||
schedule.detail.id=任务ID
|
||||
schedule.detail.status=状态
|
||||
schedule.detail.enabled=已启用
|
||||
schedule.detail.disabled=已停用
|
||||
schedule.detail.next.run=下次执行
|
||||
schedule.detail.last.run=上次执行
|
||||
schedule.detail.description=描述
|
||||
schedule.create.effective=任务已创建并立即生效,系统将在设定时间自动执行。
|
||||
schedule.list.title=当前定时任务列表
|
||||
schedule.list.count=共 {0} 个
|
||||
schedule.list.empty=当前对话没有定时任务。
|
||||
schedule.list.hint=您可以说「每天早上9点执行每日巡检」来创建定时任务。
|
||||
schedule.delete.success=已删除定时任务 #{0} ({1})
|
||||
schedule.toggle.enabled=已启用定时任务 #{0} ({1})
|
||||
schedule.toggle.disabled=已停用定时任务 #{0} ({1})
|
||||
schedule.toggle.status=当前状态
|
||||
schedule.error.skill.not.found=技能 ''{0}'' 不存在。可用技能: {1}
|
||||
schedule.error.invalid.conversation=无效的会话ID,请从系统上下文传递正确的conversationId。
|
||||
schedule.error.not.found=未找到ID为 {0} 的定时任务。
|
||||
schedule.error.create.failed=创建定时任务失败
|
||||
schedule.error.use.with.conversation=请使用 createScheduleWithConversation 并传递 conversationId 参数。
|
||||
|
||||
# Schedule Push Messages
|
||||
schedule.push.error.prefix=定时任务执行失败:
|
||||
schedule.push.error.label=错误:
|
||||
schedule.push.report.title=**定时任务执行报告**
|
||||
schedule.push.report.skill=**技能**
|
||||
schedule.push.report.time=**执行时间**
|
||||
schedule.push.report.failed=**执行失败**
|
||||
@@ -1,68 +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.
|
||||
|
||||
# SOP Result Messages - English
|
||||
sop.result.title=SOP Execution Result
|
||||
sop.result.name=SOP Name
|
||||
sop.result.duration=Duration
|
||||
sop.result.summary=Summary
|
||||
sop.result.error=Error
|
||||
sop.result.report.title=Detailed Report
|
||||
sop.result.data.title=Returned Data
|
||||
sop.result.simple.complete=Operation completed successfully.
|
||||
sop.result.report.success=Report generated successfully
|
||||
sop.result.data.success=Data retrieved successfully
|
||||
sop.result.action.pending=Action pending confirmation
|
||||
sop.result.operation.success=Operation completed successfully
|
||||
|
||||
# LLM Language Instructions
|
||||
sop.llm.language.instruction=IMPORTANT: Please respond in English.
|
||||
|
||||
# Schedule Messages
|
||||
schedule.create.success=Schedule created successfully!
|
||||
schedule.create.duplicate=This schedule already exists (skill: {0}, cron: {1}). No need to create a duplicate.
|
||||
schedule.create.duplicate.hint=To create a task at different time, please specify a different schedule time.
|
||||
schedule.detail.title=Task Details
|
||||
schedule.detail.skill=Skill
|
||||
schedule.detail.cron=Schedule Time
|
||||
schedule.detail.id=Task ID
|
||||
schedule.detail.status=Status
|
||||
schedule.detail.enabled=Enabled
|
||||
schedule.detail.disabled=Disabled
|
||||
schedule.detail.next.run=Next Run
|
||||
schedule.detail.last.run=Last Run
|
||||
schedule.detail.description=Description
|
||||
schedule.create.effective=Task created and effective immediately. The system will execute it at the scheduled time.
|
||||
schedule.list.title=Scheduled Tasks List
|
||||
schedule.list.count=Total: {0}
|
||||
schedule.list.empty=No scheduled tasks in current conversation.
|
||||
schedule.list.hint=You can say "Run daily inspection every morning at 9am" to create a scheduled task.
|
||||
schedule.delete.success=Deleted schedule #{0} ({1})
|
||||
schedule.toggle.enabled=Enabled schedule #{0} ({1})
|
||||
schedule.toggle.disabled=Disabled schedule #{0} ({1})
|
||||
schedule.toggle.status=Current Status
|
||||
schedule.error.skill.not.found=Skill ''{0}'' not found. Available skills: {1}
|
||||
schedule.error.invalid.conversation=Invalid conversation ID. Please pass the correct conversationId from system context.
|
||||
schedule.error.not.found=Schedule with ID {0} not found.
|
||||
schedule.error.create.failed=Failed to create schedule
|
||||
schedule.error.use.with.conversation=Please use createScheduleWithConversation with conversationId parameter.
|
||||
|
||||
# Schedule Push Messages
|
||||
schedule.push.error.prefix=Scheduled task execution failed:
|
||||
schedule.push.error.label=Error:
|
||||
schedule.push.report.title=**Scheduled Task Execution Report**
|
||||
schedule.push.report.skill=**Skill**
|
||||
schedule.push.report.time=**Execution Time**
|
||||
schedule.push.report.failed=**Execution Failed**
|
||||
@@ -1,68 +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.
|
||||
|
||||
# SOP Result Messages - Chinese
|
||||
sop.result.title=SOP执行结果
|
||||
sop.result.name=SOP名称
|
||||
sop.result.duration=耗时
|
||||
sop.result.summary=摘要
|
||||
sop.result.error=错误
|
||||
sop.result.report.title=详细报告
|
||||
sop.result.data.title=返回数据
|
||||
sop.result.simple.complete=操作已完成。
|
||||
sop.result.report.success=报告生成成功
|
||||
sop.result.data.success=数据获取成功
|
||||
sop.result.action.pending=等待确认操作
|
||||
sop.result.operation.success=操作已完成
|
||||
|
||||
# LLM Language Instructions
|
||||
sop.llm.language.instruction=重要:请使用中文回复。
|
||||
|
||||
# Schedule Messages
|
||||
schedule.create.success=已成功创建定时任务!
|
||||
schedule.create.duplicate=该定时任务已存在(技能: {0}, 执行时间: {1}),无需重复创建。
|
||||
schedule.create.duplicate.hint=如需创建不同时间的任务,请指定不同的执行时间。
|
||||
schedule.detail.title=任务详情
|
||||
schedule.detail.skill=技能
|
||||
schedule.detail.cron=执行时间
|
||||
schedule.detail.id=任务ID
|
||||
schedule.detail.status=状态
|
||||
schedule.detail.enabled=已启用
|
||||
schedule.detail.disabled=已停用
|
||||
schedule.detail.next.run=下次执行
|
||||
schedule.detail.last.run=上次执行
|
||||
schedule.detail.description=描述
|
||||
schedule.create.effective=任务已创建并立即生效,系统将在设定时间自动执行。
|
||||
schedule.list.title=当前定时任务列表
|
||||
schedule.list.count=共 {0} 个
|
||||
schedule.list.empty=当前对话没有定时任务。
|
||||
schedule.list.hint=您可以说「每天早上9点执行每日巡检」来创建定时任务。
|
||||
schedule.delete.success=已删除定时任务 #{0} ({1})
|
||||
schedule.toggle.enabled=已启用定时任务 #{0} ({1})
|
||||
schedule.toggle.disabled=已停用定时任务 #{0} ({1})
|
||||
schedule.toggle.status=当前状态
|
||||
schedule.error.skill.not.found=技能 ''{0}'' 不存在。可用技能: {1}
|
||||
schedule.error.invalid.conversation=无效的会话ID,请从系统上下文传递正确的conversationId。
|
||||
schedule.error.not.found=未找到ID为 {0} 的定时任务。
|
||||
schedule.error.create.failed=创建定时任务失败
|
||||
schedule.error.use.with.conversation=请使用 createScheduleWithConversation 并传递 conversationId 参数。
|
||||
|
||||
# Schedule Push Messages
|
||||
schedule.push.error.prefix=定时任务执行失败:
|
||||
schedule.push.error.label=错误:
|
||||
schedule.push.report.title=**定时任务执行报告**
|
||||
schedule.push.report.skill=**技能**
|
||||
schedule.push.report.time=**执行时间**
|
||||
schedule.push.report.failed=**执行失败**
|
||||
@@ -1,140 +0,0 @@
|
||||
# IMPORTANT
|
||||
The following information requires special attention. In case of any conflict with previous content, the below shall prevail
|
||||
|
||||
- **NEVER ask for sensitive parameters in conversation** (passwords, keys, tokens, credentials)
|
||||
- **NEVER include sensitive information in tool call parameters**
|
||||
- **NEVER assume all parameters can be collected via conversation**
|
||||
|
||||
You need to use this tool when creating a monitor: `add_monitor_protected`
|
||||
- **add_monitor_protected**: Add a new monitor with dynamic app-specific parameter support,In addition to the parameters provided by the user, a system-provided parameter, conversationId, is also required, with a value of {conversationId}.
|
||||
|
||||
## Special Scenario Handling:
|
||||
|
||||
### Scenario 1: User provides sensitive information
|
||||
User: "The password is 123456"
|
||||
AI: "Thank you, but for security reasons, passwords should not be transmitted through conversation. The system will display a secure form for you to enter this information safely."
|
||||
|
||||
### Scenario 2: Uncertain parameter classification
|
||||
If unsure whether a parameter is sensitive:
|
||||
1. Assume it's sensitive
|
||||
2. Guide to secure form
|
||||
3. Prioritize user data protection
|
||||
|
||||
### Scenario 3: Mixed parameter requirements
|
||||
When both public and private parameters are needed:
|
||||
1. Collect all public parameters first
|
||||
2. Then guide to secure form for private parameters
|
||||
3. Provide clear progress indicators
|
||||
|
||||
## Tool Usage Security Specifications:
|
||||
|
||||
### add_monitor tool
|
||||
- Only collect public parameters through conversation
|
||||
- Private parameters are added to tool calls directly by the system
|
||||
- Ensure tool calls never expose sensitive data
|
||||
- When you want to ask users for data, please ask for all the parameters that need to be filled in at once.
|
||||
|
||||
### Other tools
|
||||
- Most tools don't involve sensitive data
|
||||
- If they do, follow the same principles
|
||||
- Prioritize data security in all interactions
|
||||
|
||||
## Complete Monitor Addition Workflow (Secure Version):
|
||||
|
||||
1. **Identify requirement**: What does the user want to monitor?
|
||||
2. **Determine type**: Use `list_monitor_types` to find matching monitor type
|
||||
3. **Get parameter definitions**: Use `get_monitor_additional_params` to see required parameters
|
||||
4. **Collect public parameters**:
|
||||
- Ask for one public parameter at a time
|
||||
- Confirm each parameter value
|
||||
5. **Trigger secure phase**:
|
||||
- When all public parameters are complete
|
||||
- Inform user about private parameters
|
||||
- Instruct system to display secure form
|
||||
6. **Complete creation**: System handles remaining process
|
||||
|
||||
## Example Dialogue (Secure Monitor Addition):
|
||||
|
||||
User: "I want to monitor my MySQL database"
|
||||
AI: (Uses `list_monitor_types` and `get_monitor_additional_params`)
|
||||
AI: "I can help you set up MySQL monitoring. First, I need some information:
|
||||
1. What is the database host address,prot number, monitor name and check interval"
|
||||
User: "address is 192.168.1.10", "port is 3306", "monitor name is Production Database", "check interval is 60 seconds"
|
||||
AI: "[Secure Form Required]
|
||||
Private parameters requiring secure collection:
|
||||
- password (database password)
|
||||
Please complete configuration via the secure form."
|
||||
---
|
||||
|
||||
## Security and Privacy Principles
|
||||
1. **Never collect sensitive information through conversation**: Passwords, keys, tokens, and other private data must be collected via secure forms
|
||||
2. **Parameter classification**: Clearly distinguish between public parameters (collectible via conversation) and private parameters (collectible only via secure forms)
|
||||
3. **Secure interaction flow**: Public parameters collected through conversation, private parameters collected through secure forms
|
||||
NEVER ASK FOR SENSITIVE PARAMETERS - use secure forms instead, If the user has completed the security form, it is considered that the user has filled in all the private parameters, and no further requests for access will be made.
|
||||
|
||||
## Secure Interaction Protocol
|
||||
|
||||
### Parameter Classification Guide
|
||||
- **Public parameters** (collectible via conversation):
|
||||
- Hostnames, IP addresses, port numbers
|
||||
- Monitor names, labels, tags
|
||||
- Check intervals, timeout settings
|
||||
- Protocol types, URLs, paths
|
||||
- Threshold values, alert names
|
||||
|
||||
- **Private parameters** (must be collected via secure forms):
|
||||
- Passwords (password, passwd, pwd)
|
||||
- Keys (key, secret, token, credential)
|
||||
- Certificate files (certificate, private_key, ssl_key)
|
||||
- Access tokens (access_token, api_token, bearer_token)
|
||||
- Database connection strings with authentication
|
||||
- API keys, secret keys
|
||||
- Any parameter containing "secret", "key", "token", "credential", or "password"
|
||||
|
||||
### Secure Response Patterns
|
||||
Use these patterns when interacting with users:
|
||||
|
||||
#### Pattern 1: Normal Parameter Collection
|
||||
[Continuing Collection]
|
||||
Current progress: [collected parameters]
|
||||
Remaining parameters: [parameters name]
|
||||
Please provide: [specific information]
|
||||
|
||||
#### Pattern 2: Private Parameters Required
|
||||
Private parameters requiring secure collection:
|
||||
[private_parameter1] (description)
|
||||
[private_parameter2] (description)
|
||||
Please complete configuration via the secure form.
|
||||
(In order to display the form correctly, you must strictly output secure form data in the following format)
|
||||
```json
|
||||
SecureForm:\{
|
||||
"showSecureForm": true,
|
||||
"publicParams": \{ ... \},
|
||||
"privateParams": [
|
||||
\{
|
||||
"id": ...,
|
||||
"app":...,
|
||||
"name": \{
|
||||
"zh-CN": "...",
|
||||
"en-US": "...",
|
||||
"ja-JP": "...",
|
||||
"pt-BR": "...",
|
||||
"zh-TW": "..."
|
||||
\},
|
||||
"field": "...",
|
||||
"type": "password",
|
||||
"required": ...,
|
||||
"defaultValue": ...,
|
||||
"placeholder": null,
|
||||
"range": null,
|
||||
"limit": null,
|
||||
"options": nul,
|
||||
"keyAlias": null,
|
||||
"valueAlias": null,
|
||||
"hide": false,
|
||||
"depend": null
|
||||
\}
|
||||
],
|
||||
"monitorType": "..."
|
||||
\}
|
||||
```
|
||||
@@ -33,59 +33,6 @@ If the user doesn't provide required parameters, ask them iteratively to provide
|
||||
- **get_historical_metrics**: Get historical time-series metrics with flexible time ranges
|
||||
- **get_warehouse_status**: Check metrics storage system status
|
||||
|
||||
### AI Skills (SOP Workflows):
|
||||
You have access to automated skills that can perform complex multi-step operations.
|
||||
|
||||
**Available Skills:**
|
||||
{dynamically_injected_skills_list}
|
||||
|
||||
**Skill Types:**
|
||||
- Diagnostic: Analyze issues and provide recommendations
|
||||
- Operational: Execute predefined operational procedures
|
||||
- Reporting: Generate comprehensive reports
|
||||
- Automation: Automated response and remediation
|
||||
|
||||
**Usage Guidelines:**
|
||||
1. Match user intent to available skills
|
||||
2. For skills requiring monitorId, first use query_monitors to find the target
|
||||
3. Execute using: executeSkill(skillName, paramsJson)
|
||||
4. Report-type skills return results directly to the user
|
||||
|
||||
If no skill matches, use individual tools to assist the user.
|
||||
|
||||
### Scheduled Task Management:
|
||||
You can help users create scheduled tasks to automatically execute skills at specified times.
|
||||
|
||||
**Current Conversation ID: {current_conversation_id}**
|
||||
(Use this ID when calling schedule tools that require conversationId)
|
||||
|
||||
**Available Schedule Tools:**
|
||||
- **createScheduleForConversation**: Create a new scheduled task (requires conversationId, skillName, cronExpression)
|
||||
- **listSchedulesForConversation**: List all scheduled tasks for a conversation
|
||||
- **deleteSchedule**: Delete a scheduled task by ID
|
||||
- **toggleSchedule**: Enable or disable a scheduled task
|
||||
|
||||
**Natural Language Time to Cron Conversion:**
|
||||
When users describe times in natural language, convert them to 6-digit Spring Cron format:
|
||||
`seconds minutes hours dayOfMonth month dayOfWeek`
|
||||
|
||||
Common conversions:
|
||||
- "daily at 9am" → `0 0 9 * * ?`
|
||||
- "daily at 6pm" → `0 0 18 * * ?`
|
||||
- "every Monday at 9am" → `0 0 9 ? * MON`
|
||||
- "weekdays at 8:30am" → `0 30 8 ? * MON-FRI`
|
||||
- "first day of each month" → `0 0 0 1 * ?`
|
||||
- "every hour" → `0 0 * * * ?`
|
||||
- "every 30 minutes" → `0 */30 * * * ?`
|
||||
|
||||
**Workflow:**
|
||||
1. When user wants to schedule a skill, identify the skill name and time
|
||||
2. Convert natural language time to Cron expression
|
||||
3. **Important**: Only create NEW schedules the user explicitly requests in this message. Do NOT recreate schedules from conversation history.
|
||||
4. Use createScheduleWithConversation tool with the current conversation ID ({current_conversation_id})
|
||||
5. Confirm creation with task details and next run time
|
||||
|
||||
|
||||
## Natural Language Examples:
|
||||
|
||||
### Monitor Management:
|
||||
@@ -95,7 +42,7 @@ Common conversions:
|
||||
- "List all Redis monitors with their connection status"
|
||||
|
||||
### Alert Configuration:
|
||||
- ALERT RULE means when to alert a user
|
||||
- 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%"
|
||||
@@ -112,12 +59,7 @@ Common conversions:
|
||||
- "Find all alerts for monitor ID 1234 in the past day"
|
||||
- "Which monitors are currently abnormal?"
|
||||
|
||||
### Scheduled Tasks:
|
||||
- "Run daily inspection every morning at 9am"
|
||||
- "Set up a weekly system health check every Monday morning"
|
||||
- "List all scheduled tasks"
|
||||
- "Disable schedule 1"
|
||||
- "Delete schedule 2"
|
||||
|
||||
## Workflow Guidelines:
|
||||
|
||||
1. **Adding Monitors**:
|
||||
@@ -126,7 +68,7 @@ Common conversions:
|
||||
- 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:
|
||||
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.
|
||||
@@ -162,16 +104,16 @@ CRITICAL WORKFLOW Do all of this iteratively with user interaction at each step:
|
||||
|
||||
- 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
|
||||
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
|
||||
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:
|
||||
@@ -196,7 +138,7 @@ CRITICAL WORKFLOW Do all of this iteratively with user interaction at each step:
|
||||
- Provide clear explanations of monitoring data and actionable insights
|
||||
|
||||
## Avoid these common errors:
|
||||
- Using Label name instead of the value from the hierarchy JSON while creating alert rules.
|
||||
- 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 '<',
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
# AI SOP Engine
|
||||
|
||||
The AI SOP (Standard Operating Procedure) Engine enables defining and executing automated workflows through YAML configuration.
|
||||
|
||||
## Features
|
||||
|
||||
- **YAML-based Definition**: Define workflows declaratively
|
||||
- **Multiple Step Types**: `tool` (call HertzBeat tools), `llm` (AI reasoning)
|
||||
- **Unified Output**: Consistent result structure with `SopResult`
|
||||
- **I18n Support**: Multi-language output (zh/en)
|
||||
- **Multiple APIs**: Streaming (SSE), Sync (JSON), AI-friendly (Text)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Define a Skill
|
||||
|
||||
Create `skills/my_skill.yml`:
|
||||
|
||||
```yaml
|
||||
name: my_skill
|
||||
description: "My custom skill"
|
||||
version: "1.0"
|
||||
|
||||
output:
|
||||
type: report # report/simple/data/action
|
||||
format: markdown
|
||||
language: zh # zh/en
|
||||
|
||||
steps:
|
||||
- id: get_data
|
||||
type: tool
|
||||
tool: queryMonitors
|
||||
args:
|
||||
status: 9
|
||||
|
||||
- id: analyze
|
||||
type: llm
|
||||
prompt: |
|
||||
Analyze: ${get_data}
|
||||
```
|
||||
|
||||
### 2. Execute
|
||||
|
||||
```bash
|
||||
# Streaming (SSE)
|
||||
POST /api/ai/sop/execute/{skillName}
|
||||
|
||||
# Sync (JSON)
|
||||
POST /api/ai/sop/execute/{skillName}/sync
|
||||
|
||||
# AI Format (Text)
|
||||
POST /api/ai/sop/execute/{skillName}/ai
|
||||
```
|
||||
|
||||
## Output Types
|
||||
|
||||
| Type | Use Case |
|
||||
|------|----------|
|
||||
| `report` | Daily inspection, analysis |
|
||||
| `simple` | Restart, clear cache |
|
||||
| `data` | Query, statistics |
|
||||
| `action` | Pending confirmation |
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
YAML Definition → SkillRegistry → SopEngine → Executors → SopResult
|
||||
↓
|
||||
ToolExecutor / LlmExecutor
|
||||
↓
|
||||
ToolRegistry (auto-discover @Tool methods)
|
||||
```
|
||||
|
||||
## Adding New Tools
|
||||
|
||||
Just add `@Tool` annotation to your method - no other code changes needed:
|
||||
|
||||
```java
|
||||
@Tool(name = "myNewTool", description = "...")
|
||||
public String myNewTool(@ToolParam(...) Long param) {
|
||||
// implementation
|
||||
}
|
||||
```
|
||||
|
||||
ToolRegistry auto-discovers all `@Tool` methods at runtime.
|
||||
@@ -1,100 +0,0 @@
|
||||
# AI SOP 引擎
|
||||
|
||||
AI SOP(标准操作流程)引擎支持通过 YAML 配置定义和执行自动化工作流。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- **YAML 声明式定义**:通过配置文件定义工作流
|
||||
- **多步骤类型**:`tool`(调用 HertzBeat 工具)、`llm`(AI 推理)
|
||||
- **统一输出**:使用 `SopResult` 统一结果结构
|
||||
- **国际化支持**:多语言输出(中文/英文)
|
||||
- **多种 API**:流式(SSE)、同步(JSON)、AI 友好(纯文本)
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 定义技能
|
||||
|
||||
创建 `skills/my_skill.yml`:
|
||||
|
||||
```yaml
|
||||
name: my_skill
|
||||
description: "我的自定义技能"
|
||||
version: "1.0"
|
||||
|
||||
output:
|
||||
type: report # report/simple/data/action
|
||||
format: markdown
|
||||
language: zh # zh/en
|
||||
|
||||
steps:
|
||||
- id: get_data
|
||||
type: tool
|
||||
tool: queryMonitors
|
||||
args:
|
||||
status: 9
|
||||
|
||||
- id: analyze
|
||||
type: llm
|
||||
prompt: |
|
||||
分析以下数据: ${get_data}
|
||||
```
|
||||
|
||||
### 2. 执行
|
||||
|
||||
```bash
|
||||
# 流式输出(SSE)
|
||||
POST /api/ai/sop/execute/{skillName}
|
||||
|
||||
# 同步返回(JSON)
|
||||
POST /api/ai/sop/execute/{skillName}/sync
|
||||
|
||||
# AI 格式(纯文本)
|
||||
POST /api/ai/sop/execute/{skillName}/ai
|
||||
```
|
||||
|
||||
## 输出类型
|
||||
|
||||
| 类型 | 使用场景 |
|
||||
|------|---------|
|
||||
| `report` | 日常巡检、故障分析 |
|
||||
| `simple` | 重启服务、清理缓存 |
|
||||
| `data` | 查询资源、统计信息 |
|
||||
| `action` | 需要人工确认的操作 |
|
||||
|
||||
## 架构
|
||||
|
||||
```
|
||||
YAML 定义 → SkillRegistry → SopEngine → Executors → SopResult
|
||||
↓ ↓
|
||||
SopToolCallback ToolExecutor / LlmExecutor
|
||||
↓
|
||||
ToolRegistry(自动发现 @Tool 方法)
|
||||
```
|
||||
|
||||
所有执行入口都由 `SopEngine` 统一补齐默认参数并校验必填参数。技能注册为 Spring AI
|
||||
工具时,`SopToolCallback` 会根据参数定义生成 JSON Schema、解析模型传入的 JSON 对象,
|
||||
同步执行 SOP 并返回 `SopResult`;调用方不应在控制器或工具外层重复实现参数校验。
|
||||
|
||||
## 添加新工具
|
||||
|
||||
只需添加 `@Tool` 注解,无需修改其他代码:
|
||||
|
||||
```java
|
||||
@Tool(name = "myNewTool", description = "...")
|
||||
public String myNewTool(@ToolParam(...) Long param) {
|
||||
// 实现逻辑
|
||||
}
|
||||
```
|
||||
|
||||
ToolRegistry 会在运行时自动发现所有 `@Tool` 方法。
|
||||
|
||||
## 配置说明
|
||||
|
||||
### output 配置
|
||||
|
||||
| 字段 | 说明 | 可选值 |
|
||||
|-----|------|-------|
|
||||
| type | 输出类型 | report/simple/data/action |
|
||||
| format | 格式 | markdown/json/text |
|
||||
| language | 语言 | zh(中文)/en(英文) |
|
||||
| contentStep | 内容步骤 | 步骤 ID |
|
||||
@@ -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.
|
||||
|
||||
name: daily_inspection
|
||||
description: "Execute daily health inspection on all monitors and generate a comprehensive report"
|
||||
version: "1.0"
|
||||
|
||||
# Output configuration
|
||||
output:
|
||||
type: report # report / simple / data / action
|
||||
format: markdown # markdown / json / text
|
||||
# language is not specified here - uses system default locale from user's SystemConfig
|
||||
contentStep: generate_report # Which step's output is the main content
|
||||
|
||||
parameters:
|
||||
- name: includeMetrics
|
||||
type: boolean
|
||||
description: "Whether to include metrics data in the report"
|
||||
required: false
|
||||
defaultValue: "false"
|
||||
|
||||
steps:
|
||||
- id: get_monitor_summary
|
||||
type: tool
|
||||
tool: query_monitors
|
||||
args:
|
||||
status: 9
|
||||
pageSize: 100
|
||||
pageIndex: 0
|
||||
sort: "gmtCreate"
|
||||
order: "desc"
|
||||
includeStats: true
|
||||
|
||||
# TODO: Enable when alert tables are initialized
|
||||
# - id: get_alert_summary
|
||||
# type: tool
|
||||
# tool: getAlertsSummary
|
||||
|
||||
# - id: get_firing_alerts
|
||||
# type: tool
|
||||
# tool: queryAlerts
|
||||
# args:
|
||||
# status: "firing"
|
||||
# pageSize: 20
|
||||
# pageIndex: 0
|
||||
# sort: "startAt"
|
||||
# order: "desc"
|
||||
|
||||
- id: get_storage_status
|
||||
type: tool
|
||||
tool: get_warehouse_status
|
||||
|
||||
- id: generate_report
|
||||
type: llm
|
||||
prompt: |
|
||||
You are an expert IT operations engineer. Based on the following monitoring data, generate a concise daily inspection report.
|
||||
|
||||
## Monitor Status Summary
|
||||
${get_monitor_summary}
|
||||
|
||||
## Storage Status
|
||||
${get_storage_status}
|
||||
|
||||
Please provide a report with:
|
||||
1. Overall Health Summary
|
||||
2. Critical Issues Requiring Immediate Attention
|
||||
3. Recommendations
|
||||
|
||||
Format the report in markdown. Be concise but thorough.
|
||||
@@ -1,110 +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: mysql_slow_query_diagnosis
|
||||
description: "Diagnose MySQL slow queries and provide optimization recommendations"
|
||||
version: "1.0"
|
||||
|
||||
# Output configuration
|
||||
output:
|
||||
type: report
|
||||
format: markdown
|
||||
language: zh
|
||||
contentStep: generate_diagnosis
|
||||
|
||||
parameters:
|
||||
- name: monitorId
|
||||
type: long
|
||||
description: "MySQL monitor ID to diagnose"
|
||||
required: true
|
||||
- name: slowQueryLimit
|
||||
type: integer
|
||||
description: "Number of slow queries to analyze (default: 10)"
|
||||
required: false
|
||||
defaultValue: "10"
|
||||
|
||||
steps:
|
||||
# Step 1: Get slow query statistics
|
||||
- id: get_slow_queries
|
||||
type: tool
|
||||
tool: get_mysql_slow_queries
|
||||
args:
|
||||
monitorId: "${monitorId}"
|
||||
limit: "${slowQueryLimit}"
|
||||
|
||||
# Step 2: Get current process list
|
||||
- id: get_process_list
|
||||
type: tool
|
||||
tool: get_mysql_process_list
|
||||
args:
|
||||
monitorId: "${monitorId}"
|
||||
|
||||
# Step 3: Get lock wait information
|
||||
- id: get_lock_waits
|
||||
type: tool
|
||||
tool: get_mysql_lock_waits
|
||||
args:
|
||||
monitorId: "${monitorId}"
|
||||
|
||||
# Step 4: Get global status for slow query metrics
|
||||
- id: get_slow_status
|
||||
type: tool
|
||||
tool: get_mysql_global_status
|
||||
args:
|
||||
monitorId: "${monitorId}"
|
||||
pattern: "Slow%"
|
||||
|
||||
# Step 5: LLM analyze and generate diagnosis report
|
||||
- id: generate_diagnosis
|
||||
type: llm
|
||||
prompt: |
|
||||
You are a senior MySQL DBA expert. Based on the following diagnostic data, generate a comprehensive slow query diagnosis report.
|
||||
|
||||
## Slow Query Statistics (Top N by average execution time)
|
||||
${get_slow_queries}
|
||||
|
||||
## Current Process List (Active queries)
|
||||
${get_process_list}
|
||||
|
||||
## Lock Wait Information
|
||||
${get_lock_waits}
|
||||
|
||||
## Server Slow Query Status
|
||||
${get_slow_status}
|
||||
|
||||
Please provide a diagnosis report with the following sections:
|
||||
|
||||
1. **Problem Summary**
|
||||
- Overall slow query situation
|
||||
- Most critical issues found
|
||||
|
||||
2. **Slow Query Analysis**
|
||||
- Top slow queries and their characteristics
|
||||
- Common patterns (full table scans, missing indexes, etc.)
|
||||
|
||||
3. **Lock & Concurrency Issues**
|
||||
- Any lock contention found
|
||||
- Long-running transactions
|
||||
|
||||
4. **Optimization Recommendations**
|
||||
- Index recommendations
|
||||
- Query optimization suggestions
|
||||
- Configuration tuning tips
|
||||
|
||||
5. **Urgent Actions**
|
||||
- Queries that need immediate attention
|
||||
- Recommended immediate steps
|
||||
|
||||
Format the report in markdown. Be specific with SQL examples where applicable.
|
||||
@@ -1,146 +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 static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import com.usthe.sureness.subject.SubjectSum;
|
||||
import com.usthe.sureness.util.SurenessContextHolder;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.ai.chat.model.ToolContext;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.definition.ToolDefinition;
|
||||
|
||||
/**
|
||||
* Verifies the security context scope used when model tools execute across threads.
|
||||
*/
|
||||
class McpContextHolderTest {
|
||||
|
||||
@AfterEach
|
||||
void clearContext() {
|
||||
McpContextHolder.clear();
|
||||
SurenessContextHolder.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
void callWithSubjectShouldBindAndClearBothContexts() {
|
||||
SubjectSum subject = mock(SubjectSum.class);
|
||||
|
||||
String result = McpContextHolder.callWithSubject(subject, () -> {
|
||||
assertSame(subject, McpContextHolder.getSubject());
|
||||
assertSame(subject, SurenessContextHolder.getBindSubject());
|
||||
return "result";
|
||||
});
|
||||
|
||||
assertEquals("result", result);
|
||||
assertNull(McpContextHolder.getSubject());
|
||||
assertNull(SurenessContextHolder.getBindSubject());
|
||||
}
|
||||
|
||||
@Test
|
||||
void callWithSubjectShouldRestoreIndependentPreviousContexts() {
|
||||
SubjectSum previousMcpSubject = mock(SubjectSum.class);
|
||||
SubjectSum previousSurenessSubject = mock(SubjectSum.class);
|
||||
SubjectSum currentSubject = mock(SubjectSum.class);
|
||||
McpContextHolder.setSubject(previousMcpSubject);
|
||||
SurenessContextHolder.bindSubject(previousSurenessSubject);
|
||||
|
||||
McpContextHolder.callWithSubject(currentSubject, () -> {
|
||||
assertSame(currentSubject, McpContextHolder.getSubject());
|
||||
assertSame(currentSubject, SurenessContextHolder.getBindSubject());
|
||||
return null;
|
||||
});
|
||||
|
||||
assertSame(previousMcpSubject, McpContextHolder.getSubject());
|
||||
assertSame(previousSurenessSubject, SurenessContextHolder.getBindSubject());
|
||||
}
|
||||
|
||||
@Test
|
||||
void callWithSubjectShouldRestoreContextsAfterFailure() {
|
||||
SubjectSum previousSubject = mock(SubjectSum.class);
|
||||
SubjectSum currentSubject = mock(SubjectSum.class);
|
||||
McpContextHolder.setSubject(previousSubject);
|
||||
SurenessContextHolder.bindSubject(previousSubject);
|
||||
|
||||
assertThrows(IllegalStateException.class, () ->
|
||||
McpContextHolder.callWithSubject(currentSubject, () -> {
|
||||
throw new IllegalStateException("tool failed");
|
||||
}));
|
||||
|
||||
assertSame(previousSubject, McpContextHolder.getSubject());
|
||||
assertSame(previousSubject, SurenessContextHolder.getBindSubject());
|
||||
}
|
||||
|
||||
@Test
|
||||
void toolContextShouldCarryOnlyValidSubject() {
|
||||
SubjectSum subject = mock(SubjectSum.class);
|
||||
ToolContext toolContext = new ToolContext(McpContextHolder.createToolContext(subject));
|
||||
|
||||
assertSame(subject, McpContextHolder.getSubject(toolContext));
|
||||
assertNull(McpContextHolder.getSubject(null));
|
||||
assertNull(McpContextHolder.getSubject(new ToolContext(
|
||||
java.util.Map.of(McpContextHolder.SUBJECT_CONTEXT_KEY, "invalid"))));
|
||||
assertEquals(java.util.Map.of(), McpContextHolder.createToolContext(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void callbackShouldExposeSubjectOnlyDuringDelegateCall() {
|
||||
SubjectSum subject = mock(SubjectSum.class);
|
||||
ToolContext toolContext = new ToolContext(McpContextHolder.createToolContext(subject));
|
||||
ToolCallback delegate = new ToolCallback() {
|
||||
@Override
|
||||
public ToolDefinition getToolDefinition() {
|
||||
return ToolDefinition.builder()
|
||||
.name("test")
|
||||
.description("test")
|
||||
.inputSchema("{}")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String call(String input) {
|
||||
return input;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String call(String input, ToolContext context) {
|
||||
if (context == null) {
|
||||
return input;
|
||||
}
|
||||
assertSame(subject, McpContextHolder.getSubject());
|
||||
assertSame(subject, SurenessContextHolder.getBindSubject());
|
||||
return "ok";
|
||||
}
|
||||
};
|
||||
SecurityContextToolCallback callback = new SecurityContextToolCallback(delegate);
|
||||
|
||||
assertEquals("test", callback.getToolDefinition().name());
|
||||
assertEquals(delegate.getToolMetadata().returnDirect(), callback.getToolMetadata().returnDirect());
|
||||
assertEquals("plain", callback.call("plain"));
|
||||
String result = callback.call("{}", toolContext);
|
||||
|
||||
assertEquals("ok", result);
|
||||
assertNull(McpContextHolder.getSubject());
|
||||
assertNull(SurenessContextHolder.getBindSubject());
|
||||
}
|
||||
}
|
||||
@@ -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.controller;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
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.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
/**
|
||||
* Verifies that synchronous SOP failures include a diagnostic response body.
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SopControllerTest {
|
||||
|
||||
@Mock
|
||||
private SkillRegistry skillRegistry;
|
||||
|
||||
@Mock
|
||||
private SopEngine sopEngine;
|
||||
|
||||
@InjectMocks
|
||||
private SopController controller;
|
||||
|
||||
@Test
|
||||
void executeSopSyncShouldReturnFailureBodyWhenSkillDoesNotExist() {
|
||||
when(skillRegistry.getSkill("missing")).thenReturn(null);
|
||||
|
||||
ResponseEntity<SopResult> response = controller.executeSopSync("missing", null);
|
||||
|
||||
assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
|
||||
assertNotNull(response.getBody());
|
||||
assertEquals("FAILED", response.getBody().getStatus());
|
||||
assertEquals("SOP skill not found: missing", response.getBody().getError());
|
||||
}
|
||||
}
|
||||
-128
@@ -1,128 +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 static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyMap;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.List;
|
||||
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.SopDefinition;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopResult;
|
||||
import org.apache.hertzbeat.ai.sop.registry.SkillRegistry;
|
||||
import org.apache.hertzbeat.common.entity.ai.ChatMessage;
|
||||
import org.apache.hertzbeat.common.entity.ai.SopSchedule;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
/**
|
||||
* Verifies that due SOP schedules are isolated from each other and reject invalid parameters.
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SopScheduleExecutorTest {
|
||||
|
||||
@Mock
|
||||
private SopScheduleService scheduleService;
|
||||
|
||||
@Mock
|
||||
private SopEngine sopEngine;
|
||||
|
||||
@Mock
|
||||
private SkillRegistry skillRegistry;
|
||||
|
||||
@Mock
|
||||
private ChatMessageDao chatMessageDao;
|
||||
|
||||
private SopScheduleExecutor executor;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
executor = new SopScheduleExecutor(scheduleService, sopEngine, skillRegistry, chatMessageDao);
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkShouldContinueAfterOneScheduleFailsToUpdate() {
|
||||
SopSchedule first = schedule(1L, null);
|
||||
SopSchedule second = schedule(2L, null);
|
||||
SopDefinition definition = SopDefinition.builder().name("daily_inspection").build();
|
||||
SopResult result = SopResult.builder()
|
||||
.status("SUCCESS")
|
||||
.content("ok")
|
||||
.build();
|
||||
when(scheduleService.getDueSchedules()).thenReturn(List.of(first, second));
|
||||
when(skillRegistry.getSkill("daily_inspection")).thenReturn(definition);
|
||||
when(sopEngine.executeSync(any(SopDefinition.class), anyMap())).thenReturn(result);
|
||||
doThrow(new IllegalStateException("database unavailable"))
|
||||
.when(scheduleService).updateAfterExecution(1L);
|
||||
|
||||
executor.checkAndExecuteDueSchedules();
|
||||
|
||||
verify(sopEngine, times(2)).executeSync(any(SopDefinition.class), anyMap());
|
||||
verify(scheduleService).updateAfterExecution(2L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkShouldRejectInvalidScheduleParameters() {
|
||||
SopSchedule schedule = schedule(1L, "not-json");
|
||||
when(scheduleService.getDueSchedules()).thenReturn(List.of(schedule));
|
||||
when(skillRegistry.getSkill("daily_inspection"))
|
||||
.thenReturn(SopDefinition.builder().name("daily_inspection").build());
|
||||
|
||||
executor.checkAndExecuteDueSchedules();
|
||||
|
||||
verifyNoInteractions(sopEngine);
|
||||
verify(chatMessageDao).save(any(ChatMessage.class));
|
||||
verify(scheduleService).updateAfterExecution(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkShouldPushErrorWhenScheduledSkillNoLongerExists() {
|
||||
SopSchedule schedule = schedule(1L, null);
|
||||
when(scheduleService.getDueSchedules()).thenReturn(List.of(schedule));
|
||||
when(skillRegistry.getSkill("daily_inspection")).thenReturn(null);
|
||||
|
||||
executor.checkAndExecuteDueSchedules();
|
||||
|
||||
ArgumentCaptor<ChatMessage> messageCaptor = ArgumentCaptor.forClass(ChatMessage.class);
|
||||
verify(chatMessageDao).save(messageCaptor.capture());
|
||||
assertTrue(messageCaptor.getValue().getContent().contains("SOP skill not found: daily_inspection"));
|
||||
verifyNoInteractions(sopEngine);
|
||||
verify(scheduleService).updateAfterExecution(1L);
|
||||
}
|
||||
|
||||
private SopSchedule schedule(Long id, String params) {
|
||||
return SopSchedule.builder()
|
||||
.id(id)
|
||||
.conversationId(10L)
|
||||
.sopName("daily_inspection")
|
||||
.sopParams(params)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
-141
@@ -1,141 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.service.impl;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyList;
|
||||
import static org.mockito.ArgumentMatchers.anyMap;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.usthe.sureness.subject.SubjectSum;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.apache.hertzbeat.ai.config.McpContextHolder;
|
||||
import org.apache.hertzbeat.ai.config.SecurityContextToolCallback;
|
||||
import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext;
|
||||
import org.apache.hertzbeat.ai.sop.registry.SkillRegistry;
|
||||
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
|
||||
import org.apache.hertzbeat.common.entity.dto.ModelProviderConfig;
|
||||
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.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.support.StaticApplicationContext;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
/**
|
||||
* Verifies that the provider configuration cache reacts to enable and disable events.
|
||||
*/
|
||||
class ChatClientProviderServiceImplTest {
|
||||
|
||||
@Test
|
||||
void configurationChangeShouldRefreshConfiguredState() {
|
||||
AtomicReference<GeneralConfig> currentConfig = new AtomicReference<>();
|
||||
GeneralConfigDao configDao = configDao(currentConfig);
|
||||
ChatClientProviderServiceImpl service = new ChatClientProviderServiceImpl(null, configDao, null);
|
||||
|
||||
assertFalse(service.isConfigured());
|
||||
|
||||
currentConfig.set(providerConfig("sk-test"));
|
||||
service.onAiProviderConfigChange(changeEvent());
|
||||
assertTrue(service.isConfigured());
|
||||
|
||||
currentConfig.set(providerConfig(" "));
|
||||
service.onAiProviderConfigChange(changeEvent());
|
||||
assertFalse(service.isConfigured());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void streamChatShouldAttachSubjectAndWrapEveryToolCallback() {
|
||||
ApplicationContext applicationContext = mock(ApplicationContext.class);
|
||||
SkillRegistry skillRegistry = mock(SkillRegistry.class);
|
||||
ChatClient chatClient = mock(ChatClient.class);
|
||||
ChatClient.ChatClientRequestSpec requestSpec = mock(ChatClient.ChatClientRequestSpec.class);
|
||||
ChatClient.StreamResponseSpec streamSpec = mock(ChatClient.StreamResponseSpec.class);
|
||||
ToolCallback delegate = mock(ToolCallback.class);
|
||||
SubjectSum subject = mock(SubjectSum.class);
|
||||
ChatClientProviderServiceImpl service = new ChatClientProviderServiceImpl(
|
||||
applicationContext, configDao(new AtomicReference<>()), skillRegistry);
|
||||
|
||||
when(applicationContext.getBean("openAiChatClient", ChatClient.class)).thenReturn(chatClient);
|
||||
when(chatClient.prompt()).thenReturn(requestSpec);
|
||||
when(requestSpec.messages(anyList())).thenReturn(requestSpec);
|
||||
when(requestSpec.system(anyString())).thenReturn(requestSpec);
|
||||
when(requestSpec.tools(any(Object[].class))).thenReturn(requestSpec);
|
||||
when(requestSpec.toolContext(anyMap())).thenReturn(requestSpec);
|
||||
when(requestSpec.stream()).thenReturn(streamSpec);
|
||||
when(streamSpec.content()).thenReturn(Flux.just("answer"));
|
||||
when(skillRegistry.getAllSkills()).thenReturn(java.util.List.of());
|
||||
ReflectionTestUtils.setField(service, "systemResource", new ByteArrayResource(
|
||||
"skills={dynamically_injected_skills_list}; conversation={current_conversation_id}"
|
||||
.getBytes(StandardCharsets.UTF_8)));
|
||||
ReflectionTestUtils.setField(service, "toolCallbackProvider", ToolCallbackProvider.from(delegate));
|
||||
|
||||
ChatRequestContext context = ChatRequestContext.builder()
|
||||
.message("question")
|
||||
.conversationId(42L)
|
||||
.subject(subject)
|
||||
.build();
|
||||
service.streamChat(context).collectList().block();
|
||||
|
||||
ArgumentCaptor<Object[]> callbacksCaptor = ArgumentCaptor.forClass(Object[].class);
|
||||
ArgumentCaptor<Map<String, Object>> contextCaptor = ArgumentCaptor.forClass(Map.class);
|
||||
verify(requestSpec).tools(callbacksCaptor.capture());
|
||||
verify(requestSpec).toolContext(contextCaptor.capture());
|
||||
assertInstanceOf(SecurityContextToolCallback.class, callbacksCaptor.getValue()[0]);
|
||||
assertSame(subject, McpContextHolder.getSubject(new org.springframework.ai.chat.model.ToolContext(
|
||||
contextCaptor.getValue())));
|
||||
}
|
||||
|
||||
private GeneralConfigDao configDao(AtomicReference<GeneralConfig> currentConfig) {
|
||||
return (GeneralConfigDao) Proxy.newProxyInstance(
|
||||
GeneralConfigDao.class.getClassLoader(),
|
||||
new Class<?>[]{GeneralConfigDao.class},
|
||||
(proxy, method, args) -> "findByType".equals(method.getName()) ? currentConfig.get() : null);
|
||||
}
|
||||
|
||||
private GeneralConfig providerConfig(String apiKey) {
|
||||
ModelProviderConfig modelConfig = new ModelProviderConfig();
|
||||
modelConfig.setApiKey(apiKey);
|
||||
return GeneralConfig.builder()
|
||||
.type("provider")
|
||||
.content(JsonUtil.toJson(modelConfig))
|
||||
.build();
|
||||
}
|
||||
|
||||
private AiProviderConfigChangeEvent changeEvent() {
|
||||
return new AiProviderConfigChangeEvent(new StaticApplicationContext());
|
||||
}
|
||||
}
|
||||
-189
@@ -1,189 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.service.impl;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.usthe.sureness.subject.SubjectSum;
|
||||
import com.usthe.sureness.util.SurenessContextHolder;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import org.apache.hertzbeat.ai.dao.ChatConversationDao;
|
||||
import org.apache.hertzbeat.ai.dao.ChatMessageDao;
|
||||
import org.apache.hertzbeat.ai.dao.SopScheduleDao;
|
||||
import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext;
|
||||
import org.apache.hertzbeat.ai.pojo.dto.ChatResponseChunk;
|
||||
import org.apache.hertzbeat.ai.service.ChatClientProviderService;
|
||||
import org.apache.hertzbeat.common.entity.ai.ChatConversation;
|
||||
import org.apache.hertzbeat.common.entity.ai.ChatMessage;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.InOrder;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.codec.ServerSentEvent;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
/**
|
||||
* Tests multi-turn conversation context handling in {@link ConversationServiceImpl}.
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ConversationServiceImplTest {
|
||||
|
||||
private static final long CONVERSATION_ID = 1L;
|
||||
|
||||
@Mock
|
||||
private ChatConversationDao conversationDao;
|
||||
|
||||
@Mock
|
||||
private ChatMessageDao messageDao;
|
||||
|
||||
@Mock
|
||||
private SopScheduleDao sopScheduleDao;
|
||||
|
||||
@Mock
|
||||
private ChatClientProviderService chatClientProviderService;
|
||||
|
||||
@InjectMocks
|
||||
private ConversationServiceImpl conversationService;
|
||||
|
||||
@AfterEach
|
||||
void clearSecurityContext() {
|
||||
SurenessContextHolder.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
void streamChatShouldKeepCompleteConversationHistory() {
|
||||
SubjectSum subject = org.mockito.Mockito.mock(SubjectSum.class);
|
||||
SurenessContextHolder.bindSubject(subject);
|
||||
ChatConversation conversation = ChatConversation.builder()
|
||||
.id(CONVERSATION_ID)
|
||||
.title("已命名会话")
|
||||
.build();
|
||||
List<ChatMessage> history = List.of(
|
||||
ChatMessage.builder()
|
||||
.id(11L)
|
||||
.conversationId(CONVERSATION_ID)
|
||||
.role("user")
|
||||
.content("上一轮问题")
|
||||
.build(),
|
||||
ChatMessage.builder()
|
||||
.id(12L)
|
||||
.conversationId(CONVERSATION_ID)
|
||||
.role("assistant")
|
||||
.content("上一轮回答")
|
||||
.build());
|
||||
AtomicLong messageId = new AtomicLong(20L);
|
||||
|
||||
when(chatClientProviderService.isConfigured()).thenReturn(true);
|
||||
when(conversationDao.findById(CONVERSATION_ID)).thenReturn(Optional.of(conversation));
|
||||
when(messageDao.findByConversationIdOrderByGmtCreateAsc(CONVERSATION_ID)).thenReturn(history);
|
||||
when(messageDao.save(any(ChatMessage.class))).thenAnswer(invocation -> {
|
||||
ChatMessage savedMessage = invocation.getArgument(0);
|
||||
savedMessage.setId(messageId.getAndIncrement());
|
||||
return savedMessage;
|
||||
});
|
||||
when(chatClientProviderService.streamChat(any(ChatRequestContext.class)))
|
||||
.thenReturn(Flux.just("本轮回答"));
|
||||
|
||||
List<ServerSentEvent<ChatResponseChunk>> events = conversationService
|
||||
.streamChat("本轮问题", CONVERSATION_ID)
|
||||
.collectList()
|
||||
.block();
|
||||
|
||||
assertNotNull(events);
|
||||
assertEquals(2, events.size());
|
||||
ArgumentCaptor<ChatRequestContext> contextCaptor = ArgumentCaptor.forClass(ChatRequestContext.class);
|
||||
verify(chatClientProviderService).streamChat(contextCaptor.capture());
|
||||
assertEquals(history, contextCaptor.getValue().getConversationHistory());
|
||||
assertEquals(subject, contextCaptor.getValue().getSubject());
|
||||
}
|
||||
|
||||
/**
|
||||
* The service should create a conversation and return its ID when the client omits the optional conversation ID.
|
||||
*/
|
||||
@Test
|
||||
void streamChatShouldCreateConversationWhenConversationIdIsMissing() {
|
||||
AtomicLong messageId = new AtomicLong(20L);
|
||||
when(chatClientProviderService.isConfigured()).thenReturn(true);
|
||||
when(conversationDao.save(any(ChatConversation.class))).thenAnswer(invocation -> {
|
||||
ChatConversation savedConversation = invocation.getArgument(0);
|
||||
savedConversation.setId(CONVERSATION_ID);
|
||||
return savedConversation;
|
||||
});
|
||||
when(messageDao.findByConversationIdOrderByGmtCreateAsc(CONVERSATION_ID)).thenReturn(List.of());
|
||||
when(messageDao.save(any(ChatMessage.class))).thenAnswer(invocation -> {
|
||||
ChatMessage savedMessage = invocation.getArgument(0);
|
||||
savedMessage.setId(messageId.getAndIncrement());
|
||||
return savedMessage;
|
||||
});
|
||||
when(chatClientProviderService.streamChat(any(ChatRequestContext.class)))
|
||||
.thenReturn(Flux.just("本轮回答"));
|
||||
|
||||
List<ServerSentEvent<ChatResponseChunk>> events = conversationService
|
||||
.streamChat("本轮问题", null)
|
||||
.collectList()
|
||||
.block();
|
||||
|
||||
assertNotNull(events);
|
||||
assertEquals(2, events.size());
|
||||
assertEquals(CONVERSATION_ID, events.get(0).data().getConversationId());
|
||||
assertEquals(CONVERSATION_ID, events.get(1).data().getConversationId());
|
||||
|
||||
ArgumentCaptor<ChatRequestContext> contextCaptor = ArgumentCaptor.forClass(ChatRequestContext.class);
|
||||
verify(chatClientProviderService).streamChat(contextCaptor.capture());
|
||||
assertEquals(CONVERSATION_ID, contextCaptor.getValue().getConversationId());
|
||||
assertEquals(List.of(), contextCaptor.getValue().getConversationHistory());
|
||||
ArgumentCaptor<ChatConversation> conversationCaptor = ArgumentCaptor.forClass(ChatConversation.class);
|
||||
verify(conversationDao).save(conversationCaptor.capture());
|
||||
assertEquals("本轮问题", conversationCaptor.getValue().getTitle());
|
||||
verifyNoMoreInteractions(conversationDao);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deleting a conversation must remove its schedules before they can push more messages.
|
||||
*/
|
||||
@Test
|
||||
void deleteConversationShouldRemoveSchedulesMessagesAndConversationInOrder() {
|
||||
ChatMessage message = ChatMessage.builder()
|
||||
.id(11L)
|
||||
.conversationId(CONVERSATION_ID)
|
||||
.role("user")
|
||||
.content("message to delete")
|
||||
.build();
|
||||
when(messageDao.findByConversationIdOrderByGmtCreateAsc(CONVERSATION_ID))
|
||||
.thenReturn(List.of(message));
|
||||
|
||||
conversationService.deleteConversation(CONVERSATION_ID);
|
||||
|
||||
InOrder deletionOrder = inOrder(sopScheduleDao, messageDao, conversationDao);
|
||||
deletionOrder.verify(sopScheduleDao).deleteByConversationId(CONVERSATION_ID);
|
||||
deletionOrder.verify(messageDao).deleteAll(List.of(message));
|
||||
deletionOrder.verify(conversationDao).deleteById(CONVERSATION_ID);
|
||||
}
|
||||
}
|
||||
-58
@@ -1,58 +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 static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
|
||||
import org.apache.hertzbeat.ai.dao.SopScheduleDao;
|
||||
import org.apache.hertzbeat.common.entity.ai.SopSchedule;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
/**
|
||||
* Verifies that SOP schedules with no future execution time are not persisted.
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SopScheduleServiceImplTest {
|
||||
|
||||
@Mock
|
||||
private SopScheduleDao sopScheduleDao;
|
||||
|
||||
@InjectMocks
|
||||
private SopScheduleServiceImpl scheduleService;
|
||||
|
||||
@Test
|
||||
void createScheduleShouldRejectCronWithoutFutureExecutionTime() {
|
||||
SopSchedule schedule = SopSchedule.builder()
|
||||
.conversationId(1L)
|
||||
.sopName("daily_inspection")
|
||||
.cronExpression("0 0 0 31 2 *")
|
||||
.build();
|
||||
|
||||
IllegalArgumentException exception = assertThrows(
|
||||
IllegalArgumentException.class, () -> scheduleService.createSchedule(schedule));
|
||||
|
||||
assertTrue(exception.getMessage().contains("no future execution time"));
|
||||
verifyNoInteractions(sopScheduleDao);
|
||||
}
|
||||
}
|
||||
@@ -1,125 +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.sop.engine;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.apache.hertzbeat.ai.sop.executor.SopExecutor;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopDefinition;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopParameter;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopResult;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopStep;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Verifies that all SOP execution entry points follow the same parameter contract.
|
||||
*/
|
||||
class SopEngineImplTest {
|
||||
|
||||
@Test
|
||||
void executeSyncShouldReturnFailureForNullDefinition() {
|
||||
SopEngineImpl engine = new SopEngineImpl(List.of());
|
||||
|
||||
SopResult result = engine.executeSync(null, Map.of());
|
||||
|
||||
assertEquals("FAILED", result.getStatus());
|
||||
assertTrue(result.getError().contains("must not be null"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void executeSyncShouldRejectMissingRequiredParameter() {
|
||||
SopEngineImpl engine = new SopEngineImpl(List.of(new RecordingExecutor()));
|
||||
|
||||
SopResult result = engine.executeSync(definition(requiredParameter()), Map.of());
|
||||
|
||||
assertEquals("FAILED", result.getStatus());
|
||||
assertTrue(result.getError().contains("monitorId"));
|
||||
assertTrue(result.getSteps().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void executeSyncShouldApplyDefaultParameter() {
|
||||
AtomicReference<Map<String, Object>> capturedContext = new AtomicReference<>();
|
||||
SopEngineImpl engine = new SopEngineImpl(List.of(new RecordingExecutor(capturedContext)));
|
||||
|
||||
SopResult result = engine.executeSync(definition(defaultParameter()), Map.of());
|
||||
|
||||
assertEquals("SUCCESS", result.getStatus());
|
||||
assertEquals("10", capturedContext.get().get("limit"));
|
||||
assertEquals("zh", capturedContext.get().get("_language"));
|
||||
assertEquals("10", result.getData().get("limit"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void executeStreamShouldApplyTheSameParameterRules() {
|
||||
AtomicReference<Map<String, Object>> capturedContext = new AtomicReference<>();
|
||||
SopEngineImpl engine = new SopEngineImpl(List.of(new RecordingExecutor(capturedContext)));
|
||||
|
||||
List<String> events = engine.execute(definition(defaultParameter()), null).collectList().block();
|
||||
|
||||
assertNotNull(events);
|
||||
assertTrue(events.getLast().contains("completed successfully"));
|
||||
assertEquals("10", capturedContext.get().get("limit"));
|
||||
}
|
||||
|
||||
private SopDefinition definition(SopParameter parameter) {
|
||||
return SopDefinition.builder()
|
||||
.name("test-sop")
|
||||
.version("1.0")
|
||||
.parameters(List.of(parameter))
|
||||
.steps(List.of(SopStep.builder().id("query").type("tool").build()))
|
||||
.build();
|
||||
}
|
||||
|
||||
private SopParameter requiredParameter() {
|
||||
return SopParameter.builder().name("monitorId").type("long").required(true).build();
|
||||
}
|
||||
|
||||
private SopParameter defaultParameter() {
|
||||
return SopParameter.builder().name("limit").type("integer").defaultValue("10").build();
|
||||
}
|
||||
|
||||
private static final class RecordingExecutor implements SopExecutor {
|
||||
|
||||
private final AtomicReference<Map<String, Object>> capturedContext;
|
||||
|
||||
private RecordingExecutor() {
|
||||
this(new AtomicReference<>());
|
||||
}
|
||||
|
||||
private RecordingExecutor(AtomicReference<Map<String, Object>> capturedContext) {
|
||||
this.capturedContext = capturedContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean support(String type) {
|
||||
return "tool".equals(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object execute(SopStep step, Map<String, Object> context) {
|
||||
capturedContext.set(Map.copyOf(context));
|
||||
return "ok";
|
||||
}
|
||||
}
|
||||
}
|
||||
-136
@@ -1,136 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.sop.registry;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.apache.hertzbeat.ai.sop.engine.SopEngine;
|
||||
import org.apache.hertzbeat.ai.sop.model.OutputType;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopDefinition;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopParameter;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopResult;
|
||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
/**
|
||||
* Verifies the input contract and execution behavior of a SOP exposed as a Spring AI tool.
|
||||
*/
|
||||
class SopToolCallbackTest {
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void schemaShouldDeclareRequiredParametersAndEscapeDescriptions() {
|
||||
SopDefinition definition = definition();
|
||||
SopToolCallback callback = new SopToolCallback(definition, new RecordingEngine());
|
||||
|
||||
Map<String, Object> schema = JsonUtil.fromJson(callback.getToolDefinition().inputSchema(), Map.class);
|
||||
Map<String, Object> properties = (Map<String, Object>) schema.get("properties");
|
||||
Map<String, Object> monitorId = (Map<String, Object>) properties.get("monitorId");
|
||||
Map<String, Object> limit = (Map<String, Object>) properties.get("limit");
|
||||
|
||||
assertEquals(List.of("monitorId"), schema.get("required"));
|
||||
assertEquals(false, schema.get("additionalProperties"));
|
||||
assertEquals("integer", monitorId.get("type"));
|
||||
assertEquals("包含 \"引号\" 的说明", monitorId.get("description"));
|
||||
assertEquals(10, limit.get("default"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void callShouldParseArgumentsAndExecuteSop() {
|
||||
RecordingEngine engine = new RecordingEngine();
|
||||
SopToolCallback callback = new SopToolCallback(definition(), engine);
|
||||
|
||||
String response = callback.call("{\"monitorId\":42}");
|
||||
|
||||
assertEquals(42, engine.inputParams.get().get("monitorId"));
|
||||
assertTrue(response.contains("SUCCESS"));
|
||||
assertFalse(response.contains("execution started"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void callShouldRejectInvalidJson() {
|
||||
SopToolCallback callback = new SopToolCallback(definition(), new RecordingEngine());
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> callback.call("not-json"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void schemaShouldRejectInvalidBooleanDefault() {
|
||||
SopParameter enabled = SopParameter.builder()
|
||||
.name("enabled")
|
||||
.type("boolean")
|
||||
.defaultValue("yes")
|
||||
.build();
|
||||
SopDefinition definition = SopDefinition.builder()
|
||||
.name("invalid-default")
|
||||
.description("包含非法布尔默认值")
|
||||
.parameters(List.of(enabled))
|
||||
.build();
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new SopToolCallback(definition, new RecordingEngine()));
|
||||
}
|
||||
|
||||
private SopDefinition definition() {
|
||||
SopParameter monitorId = SopParameter.builder()
|
||||
.name("monitorId")
|
||||
.type("long")
|
||||
.description("包含 \"引号\" 的说明")
|
||||
.required(true)
|
||||
.build();
|
||||
SopParameter limit = SopParameter.builder()
|
||||
.name("limit")
|
||||
.type("integer")
|
||||
.defaultValue("10")
|
||||
.build();
|
||||
return SopDefinition.builder()
|
||||
.name("diagnose")
|
||||
.description("诊断工具")
|
||||
.version("1.0")
|
||||
.parameters(List.of(monitorId, limit))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static final class RecordingEngine implements SopEngine {
|
||||
|
||||
private final AtomicReference<Map<String, Object>> inputParams = new AtomicReference<>();
|
||||
|
||||
@Override
|
||||
public Flux<String> execute(SopDefinition definition, Map<String, Object> inputParams) {
|
||||
return Flux.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SopResult executeSync(SopDefinition definition, Map<String, Object> inputParams) {
|
||||
this.inputParams.set(inputParams);
|
||||
return SopResult.builder()
|
||||
.sopName(definition.getName())
|
||||
.sopVersion(definition.getVersion())
|
||||
.status("SUCCESS")
|
||||
.outputType(OutputType.SIMPLE)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
-136
@@ -1,136 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.sop.registry;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.ai.tool.annotation.ToolParam;
|
||||
import org.springframework.ai.tool.execution.ToolExecutionException;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
|
||||
/**
|
||||
* Verifies tool discovery, argument validation, and type conversion by Spring AI callbacks.
|
||||
*/
|
||||
class ToolRegistryTest {
|
||||
|
||||
@Test
|
||||
void invokeShouldConvertStructuredArguments() {
|
||||
try (GenericApplicationContext context = contextWith(ToolFixture.class)) {
|
||||
ToolRegistry registry = new ToolRegistry(context);
|
||||
|
||||
String result = registry.invoke("inspect", Map.of(
|
||||
"monitorId", 42,
|
||||
"ids", List.of(1, 2),
|
||||
"enabled", true));
|
||||
|
||||
assertEquals("Long|Long|Boolean", result);
|
||||
assertTrue(registry.hasMethod("inspect"));
|
||||
assertEquals(Set.of("inspect", "optional"), registry.getToolNames());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void invokeShouldRejectMissingRequiredParameter() {
|
||||
try (GenericApplicationContext context = contextWith(ToolFixture.class)) {
|
||||
ToolRegistry registry = new ToolRegistry(context);
|
||||
|
||||
IllegalArgumentException error = assertThrows(IllegalArgumentException.class,
|
||||
() -> registry.invoke("inspect", Map.of("ids", List.of(1), "enabled", true)));
|
||||
|
||||
assertEquals("Required tool parameter is missing: monitorId", error.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void invokeShouldRejectInvalidBooleanInsteadOfSilentlyUsingFalse() {
|
||||
try (GenericApplicationContext context = contextWith(ToolFixture.class)) {
|
||||
ToolRegistry registry = new ToolRegistry(context);
|
||||
|
||||
assertThrows(ToolExecutionException.class, () -> registry.invoke("inspect", Map.of(
|
||||
"monitorId", 42,
|
||||
"ids", List.of(1),
|
||||
"enabled", "not-a-boolean")));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void initializationShouldRejectDuplicateToolNames() {
|
||||
try (GenericApplicationContext context = contextWith(ToolFixture.class, DuplicateToolFixture.class)) {
|
||||
ToolRegistry registry = new ToolRegistry(context);
|
||||
|
||||
IllegalArgumentException error = assertThrows(
|
||||
IllegalArgumentException.class, registry::getToolNames);
|
||||
|
||||
assertTrue(error.getMessage().contains("inspect"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyRegistryShouldReportUnknownTool() {
|
||||
try (GenericApplicationContext context = contextWith()) {
|
||||
ToolRegistry registry = new ToolRegistry(context);
|
||||
|
||||
assertTrue(registry.getToolNames().isEmpty());
|
||||
assertThrows(IllegalArgumentException.class, () -> registry.invoke("missing", null));
|
||||
}
|
||||
}
|
||||
|
||||
private GenericApplicationContext contextWith(Class<?>... beanTypes) {
|
||||
GenericApplicationContext context = new GenericApplicationContext();
|
||||
for (Class<?> beanType : beanTypes) {
|
||||
context.registerBean(beanType);
|
||||
}
|
||||
context.refresh();
|
||||
return context;
|
||||
}
|
||||
|
||||
public static final class ToolFixture {
|
||||
|
||||
@Tool(name = "inspect", description = "Inspect argument types")
|
||||
public String inspect(
|
||||
@ToolParam(description = "Monitor ID", required = true) Long monitorId,
|
||||
@ToolParam(description = "Monitor ID list", required = false) List<Long> ids,
|
||||
@ToolParam(description = "Whether enabled", required = true) Boolean enabled) {
|
||||
return "%s|%s|%s".formatted(
|
||||
monitorId.getClass().getSimpleName(),
|
||||
ids.getFirst().getClass().getSimpleName(),
|
||||
enabled.getClass().getSimpleName());
|
||||
}
|
||||
|
||||
@Tool(name = "optional", description = "Tool without arguments")
|
||||
public String optional() {
|
||||
return "optional";
|
||||
}
|
||||
}
|
||||
|
||||
public static final class DuplicateToolFixture {
|
||||
|
||||
@Tool(name = "inspect", description = "Duplicate-name test tool")
|
||||
public String inspect() {
|
||||
return "duplicate";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-129
@@ -1,129 +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.tools.impl;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.hertzbeat.ai.service.SopScheduleService;
|
||||
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.SopSchedule;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
/**
|
||||
* Verifies that AI-created SOP schedules validate and persist skill parameters.
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ScheduleToolsImplTest {
|
||||
|
||||
private static final String CRON = "0 0 9 * * ?";
|
||||
|
||||
@Mock
|
||||
private SopScheduleService scheduleService;
|
||||
|
||||
@Mock
|
||||
private SkillRegistry skillRegistry;
|
||||
|
||||
private ScheduleToolsImpl scheduleTools;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
scheduleTools = new ScheduleToolsImpl(scheduleService, skillRegistry);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createScheduleShouldPersistSkillParameters() {
|
||||
when(skillRegistry.getSkill("diagnosis")).thenReturn(parameterizedSkill());
|
||||
when(scheduleService.getSchedulesByConversation(7L)).thenReturn(List.of());
|
||||
when(scheduleService.createSchedule(any())).thenAnswer(invocation -> {
|
||||
SopSchedule schedule = invocation.getArgument(0);
|
||||
schedule.setId(9L);
|
||||
return schedule;
|
||||
});
|
||||
|
||||
String result = scheduleTools.createScheduleWithConversation(
|
||||
7L, "diagnosis", CRON, "daily diagnosis", "{\"monitorId\":42}");
|
||||
|
||||
ArgumentCaptor<SopSchedule> captor = ArgumentCaptor.forClass(SopSchedule.class);
|
||||
verify(scheduleService).createSchedule(captor.capture());
|
||||
assertEquals("{\"monitorId\":42}", captor.getValue().getSopParams());
|
||||
assertTrue(result.contains("9"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createScheduleShouldAllowDifferentParametersAtTheSameTime() {
|
||||
SopSchedule existing = SopSchedule.builder()
|
||||
.sopName("diagnosis")
|
||||
.cronExpression(CRON)
|
||||
.sopParams("{\"monitorId\":41}")
|
||||
.build();
|
||||
when(skillRegistry.getSkill("diagnosis")).thenReturn(parameterizedSkill());
|
||||
when(scheduleService.getSchedulesByConversation(7L)).thenReturn(List.of(existing));
|
||||
when(scheduleService.createSchedule(any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
scheduleTools.createScheduleWithConversation(
|
||||
7L, "diagnosis", CRON, null, "{\"monitorId\":42}");
|
||||
|
||||
verify(scheduleService).createSchedule(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createScheduleShouldRejectMissingRequiredParameter() {
|
||||
when(skillRegistry.getSkill("diagnosis")).thenReturn(parameterizedSkill());
|
||||
|
||||
String result = scheduleTools.createScheduleWithConversation(
|
||||
7L, "diagnosis", CRON, null, "{}");
|
||||
|
||||
assertTrue(result.contains("monitorId"));
|
||||
verify(scheduleService, never()).createSchedule(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createScheduleShouldRejectInvalidParameterJson() {
|
||||
when(skillRegistry.getSkill("diagnosis")).thenReturn(parameterizedSkill());
|
||||
|
||||
String result = scheduleTools.createScheduleWithConversation(
|
||||
7L, "diagnosis", CRON, null, "not-json");
|
||||
|
||||
assertTrue(result.contains("valid JSON object"));
|
||||
verify(scheduleService, never()).createSchedule(any());
|
||||
}
|
||||
|
||||
private SopDefinition parameterizedSkill() {
|
||||
SopParameter monitorId = SopParameter.builder()
|
||||
.name("monitorId")
|
||||
.required(true)
|
||||
.build();
|
||||
return SopDefinition.builder()
|
||||
.name("diagnosis")
|
||||
.parameters(List.of(monitorId))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
-124
@@ -1,124 +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.tools.impl;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.hertzbeat.ai.sop.engine.SopEngine;
|
||||
import org.apache.hertzbeat.ai.sop.model.OutputType;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopDefinition;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopParameter;
|
||||
import org.apache.hertzbeat.ai.sop.model.SopResult;
|
||||
import org.apache.hertzbeat.ai.sop.registry.SkillRegistry;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
/**
|
||||
* Verifies that skill parameter errors and report execution failures return accurate, identifiable results.
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SkillToolsImplTest {
|
||||
|
||||
private static final String SKILL_NAME = "diagnose";
|
||||
|
||||
@Mock
|
||||
private SkillRegistry skillRegistry;
|
||||
|
||||
@Mock
|
||||
private SopEngine sopEngine;
|
||||
|
||||
private SkillToolsImpl skillTools;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
skillTools = new SkillToolsImpl(skillRegistry, sopEngine);
|
||||
}
|
||||
|
||||
@Test
|
||||
void executeSkillShouldRejectInvalidJsonObject() {
|
||||
when(skillRegistry.getSkill(SKILL_NAME)).thenReturn(skill());
|
||||
|
||||
String response = skillTools.executeSkill(SKILL_NAME, "not-json");
|
||||
|
||||
assertEquals("Error: Skill parameters must be a valid JSON object.", response);
|
||||
verifyNoInteractions(sopEngine);
|
||||
}
|
||||
|
||||
@Test
|
||||
void executeSkillShouldRejectBlankRequiredParameter() {
|
||||
when(skillRegistry.getSkill(SKILL_NAME)).thenReturn(skill());
|
||||
|
||||
String response = skillTools.executeSkill(SKILL_NAME, "{\"monitorId\":\" \"}");
|
||||
|
||||
assertTrue(response.contains("Required parameter 'monitorId' is missing"));
|
||||
verifyNoInteractions(sopEngine);
|
||||
}
|
||||
|
||||
@Test
|
||||
void executeSkillShouldNotMarkFailedReportForDirectDisplay() {
|
||||
SopDefinition skill = skill();
|
||||
SopResult failedResult = SopResult.builder()
|
||||
.sopName(SKILL_NAME)
|
||||
.status("FAILED")
|
||||
.outputType(OutputType.REPORT)
|
||||
.error("database unavailable")
|
||||
.build();
|
||||
when(skillRegistry.getSkill(SKILL_NAME)).thenReturn(skill);
|
||||
when(sopEngine.executeSync(skill, java.util.Map.of("monitorId", 1))).thenReturn(failedResult);
|
||||
|
||||
String response = skillTools.executeSkill(SKILL_NAME, "{\"monitorId\":1}");
|
||||
|
||||
assertFalse(response.startsWith(SkillToolsImpl.SKILL_REPORT_MARKER));
|
||||
assertTrue(response.contains("database unavailable"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void executeSkillShouldMarkSuccessfulReportForDirectDisplay() {
|
||||
SopDefinition skill = skill();
|
||||
SopResult successResult = SopResult.builder()
|
||||
.status("SUCCESS")
|
||||
.outputType(OutputType.REPORT)
|
||||
.content("diagnostic report")
|
||||
.build();
|
||||
when(skillRegistry.getSkill(SKILL_NAME)).thenReturn(skill);
|
||||
when(sopEngine.executeSync(skill, java.util.Map.of("monitorId", 1))).thenReturn(successResult);
|
||||
|
||||
String response = skillTools.executeSkill(SKILL_NAME, "{\"monitorId\":1}");
|
||||
|
||||
assertEquals(SkillToolsImpl.SKILL_REPORT_MARKER + "\ndiagnostic report", response);
|
||||
}
|
||||
|
||||
private SopDefinition skill() {
|
||||
return SopDefinition.builder()
|
||||
.name(SKILL_NAME)
|
||||
.parameters(List.of(SopParameter.builder()
|
||||
.name("monitorId")
|
||||
.required(true)
|
||||
.description("Monitor ID")
|
||||
.build()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.utils;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.apache.hertzbeat.manager.pojo.dto.Hierarchy;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.node.ObjectNode;
|
||||
|
||||
/**
|
||||
* Verifies input parsing and hierarchy-data tolerance in the AI alert rule utilities.
|
||||
*/
|
||||
class UtilityClassTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {
|
||||
"cpu_usage >= 80",
|
||||
"cpu_usage <= 80",
|
||||
"cpu_usage == 80",
|
||||
"cpu_usage != 80"
|
||||
})
|
||||
void validateExpressionSyntaxShouldAcceptSupportedComparisonOperators(String expression) {
|
||||
assertEquals("VALID", UtilityClass.validateExpressionSyntax(expression));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateExpressionSyntaxShouldNotTreatLogicalTextInsideFieldNameAsOperator() {
|
||||
assertEquals("VALID", UtilityClass.validateExpressionSyntax("processor_count > 1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateExpressionSyntaxShouldStillRejectSingleEquals() {
|
||||
assertEquals("Error: Use '==' for equality comparison, not '='",
|
||||
UtilityClass.validateExpressionSyntax("cpu_usage = 80"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseKeyValuePairsShouldPreserveColonsInValue() {
|
||||
Map<String, String> result = UtilityClass.parseKeyValuePairs(
|
||||
"runbook:https://example.org:8443/alerts, severity:critical");
|
||||
|
||||
assertEquals("https://example.org:8443/alerts", result.get("runbook"));
|
||||
assertEquals("critical", result.get("severity"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void extractFieldNamesShouldHandleFunctionsAndComparisons() {
|
||||
List<String> fields = UtilityClass.extractFieldNamesFromConditions(
|
||||
"equals(VmName, \"prod\") and contains(host, \"db\") "
|
||||
+ "and (cpu_usage >= 80 or cpu_usage <= 20)");
|
||||
|
||||
assertEquals(List.of("VmName", "host", "cpu_usage"), fields);
|
||||
}
|
||||
|
||||
@Test
|
||||
void extractFieldNamesShouldIgnoreComparisonTextInsideQuotedValue() {
|
||||
List<String> fields = UtilityClass.extractFieldNamesFromConditions(
|
||||
"equals(message, \"fake_field > 1\")");
|
||||
|
||||
assertEquals(List.of("message"), fields);
|
||||
}
|
||||
|
||||
@Test
|
||||
void formatHierarchyAsJsonShouldAcceptNonLeafWithoutChildren() {
|
||||
Hierarchy hierarchy = new Hierarchy();
|
||||
hierarchy.setValue("linux");
|
||||
hierarchy.setLabel("Linux");
|
||||
hierarchy.setIsLeaf(false);
|
||||
|
||||
ObjectNode result = UtilityClass.formatHierarchyAsJson(new ObjectMapper(), hierarchy);
|
||||
|
||||
assertEquals("app", result.get("type").asText());
|
||||
assertFalse(result.has("children"));
|
||||
assertTrue(result.has("value"));
|
||||
}
|
||||
}
|
||||
@@ -35,16 +35,6 @@
|
||||
<artifactId>hertzbeat-base</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.hertzbeat</groupId>
|
||||
<artifactId>hertzbeat-common-core</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.hertzbeat</groupId>
|
||||
<artifactId>hertzbeat-common-spring</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<!-- plugin -->
|
||||
<dependency>
|
||||
<groupId>org.apache.hertzbeat</groupId>
|
||||
|
||||
@@ -74,11 +74,6 @@ public class AlerterProperties {
|
||||
*/
|
||||
private String gotifyWebhookUrl = "https://push.example.de/message?token=";
|
||||
|
||||
/**
|
||||
* Ntfy default server url
|
||||
*/
|
||||
private String ntfyDefaultServerUrl = "https://ntfy.sh";
|
||||
|
||||
/**
|
||||
* Data entry configuration properties
|
||||
*/
|
||||
|
||||
+17
-103
@@ -18,20 +18,12 @@
|
||||
package org.apache.hertzbeat.alert;
|
||||
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.common.concurrent.ManagedExecutor;
|
||||
import org.apache.hertzbeat.common.concurrent.ManagedExecutors;
|
||||
import org.apache.hertzbeat.common.config.VirtualThreadProperties;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
@@ -39,25 +31,16 @@ import org.springframework.stereotype.Component;
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class AlerterWorkerPool implements DisposableBean {
|
||||
public class AlerterWorkerPool {
|
||||
|
||||
private ThreadPoolExecutor workerExecutor;
|
||||
private ManagedExecutor notifyExecutor;
|
||||
private ManagedExecutor logWorkerExecutor;
|
||||
private Map<Byte, Semaphore> notifyChannelPermits;
|
||||
private int notifyMaxConcurrentPerChannel;
|
||||
private ThreadPoolExecutor notifyExecutor;
|
||||
private ThreadPoolExecutor logWorkerExecutor;
|
||||
|
||||
public AlerterWorkerPool() {
|
||||
this(VirtualThreadProperties.defaults());
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public AlerterWorkerPool(VirtualThreadProperties virtualThreadProperties) {
|
||||
VirtualThreadProperties properties =
|
||||
virtualThreadProperties == null ? VirtualThreadProperties.defaults() : virtualThreadProperties;
|
||||
initWorkExecutor();
|
||||
initNotifyExecutor(properties);
|
||||
initLogWorkerExecutor(properties);
|
||||
initNotifyExecutor();
|
||||
initLogWorkerExecutor();
|
||||
}
|
||||
|
||||
private void initWorkExecutor() {
|
||||
@@ -78,32 +61,16 @@ public class AlerterWorkerPool implements DisposableBean {
|
||||
new ThreadPoolExecutor.AbortPolicy());
|
||||
}
|
||||
|
||||
private void initNotifyExecutor(VirtualThreadProperties properties) {
|
||||
Thread.UncaughtExceptionHandler handler = (thread, throwable) -> {
|
||||
log.error("Alerter notifyExecutor has uncaughtException.");
|
||||
log.error(throwable.getMessage(), throwable);
|
||||
};
|
||||
if (properties.enabled()) {
|
||||
VirtualThreadProperties.AlerterProperties alerterProperties = properties.alerter();
|
||||
VirtualThreadProperties.PoolProperties notifyProperties = alerterProperties.notifyPool();
|
||||
notifyMaxConcurrentPerChannel = Math.max(1, alerterProperties.notifyMaxConcurrentPerChannel());
|
||||
notifyChannelPermits = new ConcurrentHashMap<>(8);
|
||||
notifyExecutor = ManagedExecutors.newVirtualExecutor("notify-worker", "notify-worker-",
|
||||
notifyProperties.mode(), notifyProperties.maxConcurrentJobs(), handler);
|
||||
return;
|
||||
}
|
||||
notifyMaxConcurrentPerChannel = 0;
|
||||
notifyChannelPermits = null;
|
||||
notifyExecutor = ManagedExecutors.wrap("notify-worker", createLegacyNotifyExecutor(handler));
|
||||
}
|
||||
|
||||
private ThreadPoolExecutor createLegacyNotifyExecutor(Thread.UncaughtExceptionHandler handler) {
|
||||
private void initNotifyExecutor() {
|
||||
ThreadFactory threadFactory = new ThreadFactoryBuilder()
|
||||
.setUncaughtExceptionHandler(handler)
|
||||
.setUncaughtExceptionHandler((thread, throwable) -> {
|
||||
log.error("Alerter notifyExecutor has uncaughtException.");
|
||||
log.error(throwable.getMessage(), throwable);
|
||||
})
|
||||
.setDaemon(true)
|
||||
.setNameFormat("notify-worker-%d")
|
||||
.build();
|
||||
return new ThreadPoolExecutor(6,
|
||||
notifyExecutor = new ThreadPoolExecutor(6,
|
||||
6,
|
||||
10,
|
||||
TimeUnit.SECONDS,
|
||||
@@ -112,27 +79,16 @@ public class AlerterWorkerPool implements DisposableBean {
|
||||
new ThreadPoolExecutor.AbortPolicy());
|
||||
}
|
||||
|
||||
private void initLogWorkerExecutor(VirtualThreadProperties properties) {
|
||||
Thread.UncaughtExceptionHandler handler = (thread, throwable) -> {
|
||||
log.error("Alerter logWorkerExecutor has uncaughtException.");
|
||||
log.error(throwable.getMessage(), throwable);
|
||||
};
|
||||
if (properties.enabled()) {
|
||||
VirtualThreadProperties.QueueProperties logWorkerProperties = properties.alerter().logWorker();
|
||||
logWorkerExecutor = ManagedExecutors.newQueuedVirtualExecutor("alerter-log-worker", "log-worker-",
|
||||
logWorkerProperties.maxConcurrentJobs(), logWorkerProperties.queueCapacity(), handler);
|
||||
return;
|
||||
}
|
||||
logWorkerExecutor = ManagedExecutors.wrap("alerter-log-worker", createLegacyLogWorkerExecutor(handler));
|
||||
}
|
||||
|
||||
private ThreadPoolExecutor createLegacyLogWorkerExecutor(Thread.UncaughtExceptionHandler handler) {
|
||||
private void initLogWorkerExecutor() {
|
||||
ThreadFactory threadFactory = new ThreadFactoryBuilder()
|
||||
.setUncaughtExceptionHandler(handler)
|
||||
.setUncaughtExceptionHandler((thread, throwable) -> {
|
||||
log.error("Alerter logWorkerExecutor has uncaughtException.");
|
||||
log.error(throwable.getMessage(), throwable);
|
||||
})
|
||||
.setDaemon(true)
|
||||
.setNameFormat("log-worker-%d")
|
||||
.build();
|
||||
return new ThreadPoolExecutor(10, 10, 10, TimeUnit.SECONDS,
|
||||
logWorkerExecutor = new ThreadPoolExecutor(10, 10, 10, TimeUnit.SECONDS,
|
||||
new LinkedBlockingQueue<>(1000),
|
||||
threadFactory,
|
||||
new ThreadPoolExecutor.AbortPolicy());
|
||||
@@ -157,41 +113,6 @@ public class AlerterWorkerPool implements DisposableBean {
|
||||
notifyExecutor.execute(runnable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the given runnable task using the notify executor with per-channel concurrency control.
|
||||
*
|
||||
* @param channelType notification channel type
|
||||
* @param runnable the task to be executed
|
||||
* @throws RejectedExecutionException if the task cannot be accepted for execution
|
||||
*/
|
||||
public void executeNotify(byte channelType, Runnable runnable) throws RejectedExecutionException {
|
||||
if (notifyChannelPermits == null) {
|
||||
notifyExecutor.execute(runnable);
|
||||
return;
|
||||
}
|
||||
Semaphore semaphore = notifyChannelPermits.computeIfAbsent(channelType,
|
||||
key -> new Semaphore(notifyMaxConcurrentPerChannel));
|
||||
if (!semaphore.tryAcquire()) {
|
||||
throw new RejectedExecutionException(
|
||||
"notify-worker rejected task because channel concurrency limit was reached for type " + channelType);
|
||||
}
|
||||
boolean submitted = false;
|
||||
try {
|
||||
notifyExecutor.execute(() -> {
|
||||
try {
|
||||
runnable.run();
|
||||
} finally {
|
||||
semaphore.release();
|
||||
}
|
||||
});
|
||||
submitted = true;
|
||||
} finally {
|
||||
if (!submitted) {
|
||||
semaphore.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the given runnable task using the logWorkerExecutor.
|
||||
*
|
||||
@@ -201,11 +122,4 @@ public class AlerterWorkerPool implements DisposableBean {
|
||||
public void executeLogJob(Runnable runnable) throws RejectedExecutionException {
|
||||
logWorkerExecutor.execute(runnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
workerExecutor.shutdownNow();
|
||||
notifyExecutor.close();
|
||||
logWorkerExecutor.close();
|
||||
}
|
||||
}
|
||||
|
||||
+15
-148
@@ -25,19 +25,12 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.alert.dao.AlertDefineDao;
|
||||
import org.apache.hertzbeat.common.config.VirtualThreadProperties;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
|
||||
@@ -47,33 +40,18 @@ import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class PeriodicAlertRuleScheduler implements CommandLineRunner, DisposableBean {
|
||||
public class PeriodicAlertRuleScheduler implements CommandLineRunner {
|
||||
|
||||
private final MetricsPeriodicAlertCalculator metricsCalculator;
|
||||
private final LogPeriodicAlertCalculator logCalculator;
|
||||
private final AlertDefineDao alertDefineDao;
|
||||
private final ScheduledExecutorService scheduledExecutor;
|
||||
private final ExecutorService periodicExecutor;
|
||||
private final Semaphore periodicPermits;
|
||||
private final boolean virtualThreadsEnabled;
|
||||
private final Map<Long, ScheduledTaskState> scheduledTasks;
|
||||
private final Map<Long, ScheduledFuture<?>> scheduledFutures;
|
||||
|
||||
public PeriodicAlertRuleScheduler(MetricsPeriodicAlertCalculator metricsCalculator, LogPeriodicAlertCalculator logCalculator, AlertDefineDao alertDefineDao) {
|
||||
this(metricsCalculator, logCalculator, alertDefineDao, VirtualThreadProperties.defaults());
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public PeriodicAlertRuleScheduler(MetricsPeriodicAlertCalculator metricsCalculator,
|
||||
LogPeriodicAlertCalculator logCalculator,
|
||||
AlertDefineDao alertDefineDao,
|
||||
VirtualThreadProperties virtualThreadProperties) {
|
||||
this.metricsCalculator = metricsCalculator;
|
||||
this.logCalculator = logCalculator;
|
||||
this.alertDefineDao = alertDefineDao;
|
||||
Thread.UncaughtExceptionHandler handler = (thread, throwable) -> {
|
||||
log.error("Scheduled periodic alert threshold has uncaughtException.");
|
||||
log.error(throwable.getMessage(), throwable);
|
||||
};
|
||||
ThreadFactory threadFactory = new ThreadFactoryBuilder()
|
||||
.setUncaughtExceptionHandler((thread, throwable) -> {
|
||||
log.error("Scheduled periodic alert threshold has uncaughtException.");
|
||||
@@ -83,27 +61,17 @@ public class PeriodicAlertRuleScheduler implements CommandLineRunner, Disposable
|
||||
.setNameFormat("periodic-alert-threshold-worker-%d")
|
||||
.build();
|
||||
this.scheduledExecutor = Executors.newScheduledThreadPool(10, threadFactory);
|
||||
VirtualThreadProperties properties =
|
||||
virtualThreadProperties == null ? VirtualThreadProperties.defaults() : virtualThreadProperties;
|
||||
this.virtualThreadsEnabled = properties.enabled();
|
||||
int maxConcurrentPeriodicTasks = Math.max(1, properties.alerter().periodicMaxConcurrentJobs());
|
||||
this.periodicExecutor = virtualThreadsEnabled
|
||||
? Executors.newThreadPerTaskExecutor(Thread.ofVirtual()
|
||||
.name("periodic-alert-task-", 0)
|
||||
.uncaughtExceptionHandler(handler)
|
||||
.factory())
|
||||
: null;
|
||||
this.periodicPermits = virtualThreadsEnabled ? new Semaphore(maxConcurrentPeriodicTasks) : null;
|
||||
this.scheduledTasks = new ConcurrentHashMap<>();
|
||||
this.scheduledFutures = new ConcurrentHashMap<>();
|
||||
}
|
||||
|
||||
public void cancelSchedule(Long ruleId) {
|
||||
if (ruleId == null) {
|
||||
return;
|
||||
}
|
||||
ScheduledTaskState state = scheduledTasks.remove(ruleId);
|
||||
if (state != null) {
|
||||
state.cancel();
|
||||
ScheduledFuture<?> future = scheduledFutures.get(ruleId);
|
||||
if (future != null) {
|
||||
future.cancel(true);
|
||||
scheduledFutures.remove(ruleId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,12 +83,14 @@ public class PeriodicAlertRuleScheduler implements CommandLineRunner, Disposable
|
||||
cancelSchedule(rule.getId());
|
||||
if (rule.getType().equals(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC)
|
||||
|| rule.getType().equals(LOG_ALERT_THRESHOLD_TYPE_PERIODIC)) {
|
||||
ScheduledTaskState state = new ScheduledTaskState(rule);
|
||||
ScheduledFuture<?> future = scheduledExecutor.scheduleAtFixedRate(
|
||||
virtualThreadsEnabled ? state::trigger : () -> executeRule(rule),
|
||||
0, rule.getPeriod(), TimeUnit.SECONDS);
|
||||
state.setScheduledFuture(future);
|
||||
scheduledTasks.put(rule.getId(), state);
|
||||
ScheduledFuture<?> future = scheduledExecutor.scheduleAtFixedRate(() -> {
|
||||
if (rule.getType().equals(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC)) {
|
||||
metricsCalculator.calculate(rule);
|
||||
} else if (rule.getType().equals(LOG_ALERT_THRESHOLD_TYPE_PERIODIC)) {
|
||||
logCalculator.calculate(rule);
|
||||
}
|
||||
}, 0, rule.getPeriod(), java.util.concurrent.TimeUnit.SECONDS);
|
||||
scheduledFutures.put(rule.getId(), future);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,107 +106,4 @@ public class PeriodicAlertRuleScheduler implements CommandLineRunner, Disposable
|
||||
updateSchedule(rule);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
scheduledTasks.values().forEach(ScheduledTaskState::cancel);
|
||||
scheduledTasks.clear();
|
||||
scheduledExecutor.shutdownNow();
|
||||
if (periodicExecutor != null) {
|
||||
periodicExecutor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
private void executeRule(AlertDefine rule) {
|
||||
if (rule.getType().equals(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC)) {
|
||||
metricsCalculator.calculate(rule);
|
||||
} else if (rule.getType().equals(LOG_ALERT_THRESHOLD_TYPE_PERIODIC)) {
|
||||
logCalculator.calculate(rule);
|
||||
}
|
||||
}
|
||||
|
||||
private final class ScheduledTaskState {
|
||||
|
||||
private final AlertDefine rule;
|
||||
private ScheduledFuture<?> scheduledFuture;
|
||||
private Future<?> runningFuture;
|
||||
private boolean running;
|
||||
private boolean pending;
|
||||
private boolean cancelled;
|
||||
|
||||
private ScheduledTaskState(AlertDefine rule) {
|
||||
this.rule = rule;
|
||||
}
|
||||
|
||||
private synchronized void setScheduledFuture(ScheduledFuture<?> scheduledFuture) {
|
||||
this.scheduledFuture = scheduledFuture;
|
||||
}
|
||||
|
||||
private synchronized void trigger() {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
if (running) {
|
||||
pending = true;
|
||||
return;
|
||||
}
|
||||
running = true;
|
||||
submitLocked();
|
||||
}
|
||||
|
||||
private synchronized void cancel() {
|
||||
cancelled = true;
|
||||
pending = false;
|
||||
ScheduledFuture<?> periodicFuture = scheduledFuture;
|
||||
Future<?> currentFuture = runningFuture;
|
||||
if (periodicFuture != null) {
|
||||
periodicFuture.cancel(true);
|
||||
}
|
||||
if (currentFuture != null) {
|
||||
currentFuture.cancel(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void submitLocked() {
|
||||
try {
|
||||
runningFuture = periodicExecutor.submit(() -> {
|
||||
boolean permitAcquired = false;
|
||||
try {
|
||||
periodicPermits.acquire();
|
||||
permitAcquired = true;
|
||||
if (!Thread.currentThread().isInterrupted()) {
|
||||
executeRule(rule);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (Exception e) {
|
||||
log.error("Periodic alert rule {} execution error: {}", rule.getName(), e.getMessage(), e);
|
||||
} finally {
|
||||
if (permitAcquired) {
|
||||
periodicPermits.release();
|
||||
}
|
||||
onComplete();
|
||||
}
|
||||
});
|
||||
} catch (RuntimeException e) {
|
||||
running = false;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void onComplete() {
|
||||
runningFuture = null;
|
||||
if (cancelled) {
|
||||
running = false;
|
||||
pending = false;
|
||||
return;
|
||||
}
|
||||
if (!pending) {
|
||||
running = false;
|
||||
return;
|
||||
}
|
||||
pending = false;
|
||||
submitLocked();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-5
@@ -251,14 +251,20 @@ public class MetricsRealTimeAlertCalculator {
|
||||
}
|
||||
final int fieldType = field.getType();
|
||||
|
||||
// strict jexl aborts the whole rule on undefined variables,
|
||||
// so define every field even when its value is empty or unparseable
|
||||
if (fieldType == CommonConstants.TYPE_NUMBER) {
|
||||
fieldValueMap.put(field.getName(), CommonUtil.parseStrDouble(valueStr));
|
||||
final Double doubleValue;
|
||||
if ((doubleValue = CommonUtil.parseStrDouble(valueStr)) != null) {
|
||||
fieldValueMap.put(field.getName(), doubleValue);
|
||||
}
|
||||
} else if (fieldType == CommonConstants.TYPE_TIME) {
|
||||
fieldValueMap.put(field.getName(), CommonUtil.parseStrInteger(valueStr));
|
||||
final Integer integerValue;
|
||||
if ((integerValue = CommonUtil.parseStrInteger(valueStr)) != null) {
|
||||
fieldValueMap.put(field.getName(), integerValue);
|
||||
}
|
||||
} else {
|
||||
fieldValueMap.put(field.getName(), valueStr);
|
||||
if (StringUtils.isNotEmpty(valueStr)) {
|
||||
fieldValueMap.put(field.getName(), valueStr);
|
||||
}
|
||||
}
|
||||
|
||||
if (field.getLabel()) {
|
||||
|
||||
+10
-10
@@ -18,8 +18,6 @@
|
||||
package org.apache.hertzbeat.alert.calculate.realtime;
|
||||
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.alert.calculate.realtime.window.LogWorker;
|
||||
import org.apache.hertzbeat.alert.calculate.realtime.window.TimeService;
|
||||
@@ -30,6 +28,8 @@ import org.apache.hertzbeat.common.util.BackoffUtils;
|
||||
import org.apache.hertzbeat.common.util.ExponentialBackoff;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
@@ -83,29 +83,29 @@ public class WindowedLogRealTimeAlertCalculator implements Runnable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void processLogEntry(LogEntry logEntry) {
|
||||
// Extract event timestamp
|
||||
long eventTimestamp = extractEventTimestamp(logEntry);
|
||||
|
||||
|
||||
// Check if timestamp is within reasonable range
|
||||
if (!timeService.isValidTimestamp(eventTimestamp)) {
|
||||
log.warn("Dropping log with invalid timestamp: {}", eventTimestamp);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Check if this is late data based on current watermark
|
||||
if (timeService.isLateData(eventTimestamp)) {
|
||||
log.warn("Dropping late data, timestamp: {}, watermark: {}",
|
||||
log.warn("Dropping late data, timestamp: {}, watermark: {}",
|
||||
eventTimestamp, timeService.getCurrentWatermark());
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Update max timestamp (only validated timestamps can update watermark)
|
||||
timeService.updateMaxTimestamp(eventTimestamp);
|
||||
logWorker.reduceAndSendLogTask(logEntry);
|
||||
}
|
||||
|
||||
|
||||
private long extractEventTimestamp(LogEntry logEntry) {
|
||||
if (logEntry.getTimeUnixNano() != null && logEntry.getTimeUnixNano() != 0) {
|
||||
return logEntry.getTimeUnixNano() / 1_000_000; // Convert to milliseconds
|
||||
@@ -115,7 +115,7 @@ public class WindowedLogRealTimeAlertCalculator implements Runnable {
|
||||
}
|
||||
return System.currentTimeMillis();
|
||||
}
|
||||
|
||||
|
||||
@PostConstruct
|
||||
public void start() {
|
||||
ThreadFactory threadFactory = new ThreadFactoryBuilder()
|
||||
@@ -147,4 +147,4 @@ public class WindowedLogRealTimeAlertCalculator implements Runnable {
|
||||
dispatcherExecutor.shutdownNow();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
-40
@@ -20,11 +20,6 @@ package org.apache.hertzbeat.alert.calculate.realtime.window;
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
|
||||
import org.apache.hertzbeat.common.concurrent.ManagedExecutor;
|
||||
import org.apache.hertzbeat.common.concurrent.ManagedExecutors;
|
||||
import org.apache.hertzbeat.common.config.VirtualThreadProperties;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import org.apache.hertzbeat.alert.util.AlertTemplateUtil;
|
||||
@@ -37,6 +32,10 @@ import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Alarm Evaluator - Final alarm logic trigger
|
||||
@@ -48,48 +47,36 @@ import java.util.Map;
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class AlarmEvaluator implements DisposableBean {
|
||||
public class AlarmEvaluator {
|
||||
|
||||
private static final String WINDOW_START_TIME = "window_start_time";
|
||||
private static final String WINDOW_END_TIME = "window_end_time";
|
||||
private static final String MATCHING_LOGS_COUNT = "matching_logs_count";
|
||||
|
||||
private final AlarmCommonReduce alarmCommonReduce;
|
||||
private final ManagedExecutor workerExecutor;
|
||||
private ThreadPoolExecutor workerExecutor;
|
||||
|
||||
public AlarmEvaluator(AlarmCommonReduce alarmCommonReduce) {
|
||||
this(alarmCommonReduce, VirtualThreadProperties.defaults());
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public AlarmEvaluator(AlarmCommonReduce alarmCommonReduce, VirtualThreadProperties virtualThreadProperties) {
|
||||
this.alarmCommonReduce = alarmCommonReduce;
|
||||
VirtualThreadProperties properties =
|
||||
virtualThreadProperties == null ? VirtualThreadProperties.defaults() : virtualThreadProperties;
|
||||
this.workerExecutor = initAlarmEvaluator(properties);
|
||||
initAlarmEvaluator();
|
||||
}
|
||||
|
||||
public ManagedExecutor initAlarmEvaluator(VirtualThreadProperties properties) {
|
||||
Thread.UncaughtExceptionHandler handler = (thread, throwable) -> {
|
||||
log.error("alerter-reduce-worker has uncaughtException.");
|
||||
log.error(throwable.getMessage(), throwable);
|
||||
};
|
||||
if (properties.enabled()) {
|
||||
VirtualThreadProperties.QueueProperties queueProperties = properties.alerter().windowEvaluator();
|
||||
return ManagedExecutors.newQueuedVirtualExecutor("alerter-window-evaluator", "alerter-window-evaluator-",
|
||||
queueProperties.maxConcurrentJobs(), queueProperties.queueCapacity(), handler);
|
||||
}
|
||||
return ManagedExecutors.wrap("alerter-window-evaluator", new java.util.concurrent.ThreadPoolExecutor(2,
|
||||
public void initAlarmEvaluator() {
|
||||
ThreadFactory threadFactory = new ThreadFactoryBuilder()
|
||||
.setUncaughtExceptionHandler((thread, throwable) -> {
|
||||
log.error("alerter-reduce-worker has uncaughtException.");
|
||||
log.error(throwable.getMessage(), throwable);
|
||||
})
|
||||
.setDaemon(true)
|
||||
.setNameFormat("alerter-reduce-worker-%d")
|
||||
.build();
|
||||
workerExecutor = new ThreadPoolExecutor(2,
|
||||
10,
|
||||
10,
|
||||
java.util.concurrent.TimeUnit.SECONDS,
|
||||
new java.util.concurrent.LinkedBlockingQueue<>(),
|
||||
new ThreadFactoryBuilder()
|
||||
.setUncaughtExceptionHandler(handler)
|
||||
.setDaemon(true)
|
||||
.setNameFormat("alerter-reduce-worker-%d")
|
||||
.build(),
|
||||
new java.util.concurrent.ThreadPoolExecutor.AbortPolicy()));
|
||||
TimeUnit.SECONDS,
|
||||
new LinkedBlockingQueue<>(),
|
||||
threadFactory,
|
||||
new ThreadPoolExecutor.AbortPolicy());
|
||||
}
|
||||
|
||||
public void sendAndProcessWindowData(WindowAggregator.WindowData windowData) {
|
||||
@@ -327,9 +314,4 @@ public class AlarmEvaluator implements DisposableBean {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
workerExecutor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
-24
@@ -19,14 +19,14 @@ package org.apache.hertzbeat.alert.calculate.realtime.window;
|
||||
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.Executors;
|
||||
@@ -44,23 +44,23 @@ import java.util.concurrent.atomic.AtomicLong;
|
||||
@Component
|
||||
@Slf4j
|
||||
public class TimeService {
|
||||
|
||||
|
||||
static final long DEFAULT_WATERMARK_DELAY_MS = 30_000; // 30 seconds
|
||||
private static final long WATERMARK_BROADCAST_INTERVAL_MS = 5_000; // 5 seconds
|
||||
|
||||
|
||||
// Define acceptable timestamp range to filter abnormal timestamps
|
||||
private static final long MAX_FUTURE_TIME_MS = 60_000; // Allow 1 minute future time
|
||||
private static final long MAX_PAST_TIME_MS = 24 * 60 * 60 * 1000; // Allow 24 hours past time
|
||||
|
||||
|
||||
private final AtomicLong maxTimestamp = new AtomicLong(0);
|
||||
private final AtomicLong currentWatermark = new AtomicLong(0);
|
||||
private final CopyOnWriteArrayList<WatermarkListener> listeners = new CopyOnWriteArrayList<>();
|
||||
private ScheduledExecutorService scheduler;
|
||||
|
||||
|
||||
public TimeService(List<WatermarkListener> initialListeners) {
|
||||
listeners.addAll(initialListeners);
|
||||
}
|
||||
|
||||
|
||||
@PostConstruct
|
||||
public void start() {
|
||||
// Create internal scheduled executor
|
||||
@@ -72,9 +72,9 @@ public class TimeService {
|
||||
.setDaemon(true)
|
||||
.setNameFormat("timeservice-scheduler-%d")
|
||||
.build();
|
||||
|
||||
|
||||
this.scheduler = Executors.newSingleThreadScheduledExecutor(threadFactory);
|
||||
|
||||
|
||||
// Start watermark broadcast scheduler
|
||||
scheduler.scheduleAtFixedRate(
|
||||
this::broadcastWatermark,
|
||||
@@ -82,10 +82,10 @@ public class TimeService {
|
||||
WATERMARK_BROADCAST_INTERVAL_MS,
|
||||
TimeUnit.MILLISECONDS
|
||||
);
|
||||
|
||||
|
||||
log.info("TimeService started with watermark delay: {}ms", DEFAULT_WATERMARK_DELAY_MS);
|
||||
}
|
||||
|
||||
|
||||
@PreDestroy
|
||||
public void stop() {
|
||||
if (scheduler != null && !scheduler.isShutdown()) {
|
||||
@@ -105,10 +105,10 @@ public class TimeService {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
log.info("TimeService stopped");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if timestamp is within acceptable range
|
||||
*/
|
||||
@@ -116,35 +116,35 @@ public class TimeService {
|
||||
long currentTime = System.currentTimeMillis();
|
||||
return timestamp >= (currentTime - MAX_PAST_TIME_MS) && timestamp <= (currentTime + MAX_FUTURE_TIME_MS);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if data is late based on current watermark
|
||||
*/
|
||||
public boolean isLateData(long timestamp) {
|
||||
return timestamp < getCurrentWatermark();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update max timestamp from WindowedLogRealTimeAlertCalculator
|
||||
*/
|
||||
public void updateMaxTimestamp(long timestamp) {
|
||||
maxTimestamp.getAndUpdate(current -> Math.max(current, timestamp));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add watermark listener
|
||||
*/
|
||||
public void addWatermarkListener(WatermarkListener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Remove watermark listener
|
||||
*/
|
||||
public void removeWatermarkListener(WatermarkListener listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get current watermark
|
||||
*/
|
||||
@@ -158,12 +158,12 @@ public class TimeService {
|
||||
public long getMaxTimestamp() {
|
||||
return maxTimestamp.get();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calculate and broadcast watermark
|
||||
*/
|
||||
private void broadcastWatermark() {
|
||||
|
||||
|
||||
try {
|
||||
long maxTs = maxTimestamp.get();
|
||||
if (maxTs <= 0) {
|
||||
@@ -172,7 +172,7 @@ public class TimeService {
|
||||
// Calculate watermark: maxTimestamp - delay
|
||||
long newWatermark = maxTs - DEFAULT_WATERMARK_DELAY_MS;
|
||||
long currentWm = currentWatermark.get();
|
||||
|
||||
|
||||
// Only advance watermark (monotonic property)
|
||||
if (newWatermark <= currentWm) {
|
||||
return;
|
||||
@@ -193,7 +193,7 @@ public class TimeService {
|
||||
log.error("Error in watermark broadcast: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Watermark data class
|
||||
*/
|
||||
@@ -203,11 +203,11 @@ public class TimeService {
|
||||
public static class Watermark {
|
||||
private final long timestamp;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Interface for watermark listeners
|
||||
*/
|
||||
public interface WatermarkListener {
|
||||
void onWatermark(Watermark watermark);
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
-30
@@ -18,7 +18,6 @@
|
||||
package org.apache.hertzbeat.alert.calculate.realtime.window;
|
||||
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
@@ -26,7 +25,8 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
@@ -51,14 +51,14 @@ import java.util.concurrent.TimeUnit;
|
||||
@Component
|
||||
@Slf4j
|
||||
public class WindowAggregator implements TimeService.WatermarkListener, Runnable {
|
||||
|
||||
|
||||
private static final long DEFAULT_WINDOW_SIZE_MS = 1 * 60 * 1000; // 1 minutes
|
||||
|
||||
|
||||
private final AlarmEvaluator alarmEvaluator;
|
||||
private final BlockingQueue<MatchingLogEvent> eventQueue = new LinkedBlockingQueue<>();
|
||||
private final Map<WindowKey, WindowData> activeWindows = new HashMap<>();
|
||||
private final Object windowLock = new Object();
|
||||
|
||||
|
||||
private ExecutorService aggregatorExecutor;
|
||||
|
||||
public WindowAggregator(AlarmEvaluator alarmEvaluator) {
|
||||
@@ -73,31 +73,31 @@ public class WindowAggregator implements TimeService.WatermarkListener, Runnable
|
||||
log.warn("Interrupted while adding matching log to aggregator");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onWatermark(TimeService.Watermark watermark) {
|
||||
List<WindowData> closedWindows;
|
||||
|
||||
|
||||
synchronized (windowLock) {
|
||||
closedWindows = new ArrayList<>();
|
||||
|
||||
|
||||
// Find windows that should be closed based on watermark
|
||||
Iterator<Map.Entry<WindowKey, WindowData>> iterator = activeWindows.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<WindowKey, WindowData> entry = iterator.next();
|
||||
WindowData windowData = entry.getValue();
|
||||
|
||||
|
||||
// Close window if its end time <= watermark timestamp
|
||||
if (windowData.getEndTime() <= watermark.getTimestamp()) {
|
||||
closedWindows.add(windowData);
|
||||
iterator.remove();
|
||||
|
||||
log.debug("Closing window: {} with {} matching logs",
|
||||
|
||||
log.debug("Closing window: {} with {} matching logs",
|
||||
entry.getKey(), windowData.getMatchingLogs().size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for (WindowData windowData : closedWindows) {
|
||||
alarmEvaluator.sendAndProcessWindowData(windowData);
|
||||
}
|
||||
@@ -117,17 +117,17 @@ public class WindowAggregator implements TimeService.WatermarkListener, Runnable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void processMatchingLogEvent(MatchingLogEvent event) {
|
||||
|
||||
|
||||
// Determine window size from alert define (if specified) or use default
|
||||
long windowSizeMs = getWindowSize(event.getAlertDefine());
|
||||
|
||||
|
||||
// Calculate window boundaries
|
||||
long eventTime = event.getEventTimestamp();
|
||||
long windowStart = (eventTime / windowSizeMs) * windowSizeMs;
|
||||
long windowEnd = windowStart + windowSizeMs;
|
||||
|
||||
|
||||
// Create window key
|
||||
WindowKey windowKey = new WindowKey(
|
||||
event.getAlertDefine().getId(),
|
||||
@@ -136,15 +136,15 @@ public class WindowAggregator implements TimeService.WatermarkListener, Runnable
|
||||
);
|
||||
synchronized (windowLock) {
|
||||
// Get or create window data
|
||||
WindowData windowData = activeWindows.computeIfAbsent(windowKey,
|
||||
WindowData windowData = activeWindows.computeIfAbsent(windowKey,
|
||||
key -> new WindowData(key, event.getAlertDefine()));
|
||||
// Add matching log to window
|
||||
windowData.addMatchingLog(event);
|
||||
log.debug("Added matching log to window: {} (total logs: {})",
|
||||
log.debug("Added matching log to window: {} (total logs: {})",
|
||||
windowKey, windowData.getMatchingLogs().size());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private long getWindowSize(AlertDefine alertDefine) {
|
||||
// Check if alert define has custom window size configuration
|
||||
if (alertDefine.getPeriod() != null) {
|
||||
@@ -154,7 +154,7 @@ public class WindowAggregator implements TimeService.WatermarkListener, Runnable
|
||||
DEFAULT_WINDOW_SIZE_MS, alertDefine.getName());
|
||||
return DEFAULT_WINDOW_SIZE_MS;
|
||||
}
|
||||
|
||||
|
||||
@PostConstruct
|
||||
public void start() {
|
||||
// Create internal executor
|
||||
@@ -166,15 +166,15 @@ public class WindowAggregator implements TimeService.WatermarkListener, Runnable
|
||||
.setDaemon(true)
|
||||
.setNameFormat("window-aggregator-%d")
|
||||
.build();
|
||||
|
||||
|
||||
aggregatorExecutor = Executors.newSingleThreadExecutor(threadFactory);
|
||||
|
||||
// Submit aggregation task
|
||||
aggregatorExecutor.submit(this);
|
||||
|
||||
|
||||
log.info("WindowAggregator started");
|
||||
}
|
||||
|
||||
|
||||
@PreDestroy
|
||||
public void stop() {
|
||||
if (aggregatorExecutor != null && !aggregatorExecutor.isShutdown()) {
|
||||
@@ -194,10 +194,10 @@ public class WindowAggregator implements TimeService.WatermarkListener, Runnable
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
log.info("WindowAggregator stopped");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Window key for identifying unique windows
|
||||
*/
|
||||
@@ -209,7 +209,7 @@ public class WindowAggregator implements TimeService.WatermarkListener, Runnable
|
||||
private final long startTime;
|
||||
private final long endTime;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Window data container
|
||||
*/
|
||||
@@ -221,13 +221,13 @@ public class WindowAggregator implements TimeService.WatermarkListener, Runnable
|
||||
private final List<MatchingLogEvent> matchingLogs = new ArrayList<>();
|
||||
@Getter
|
||||
private final long createdTime;
|
||||
|
||||
|
||||
public WindowData(WindowKey windowKey, AlertDefine alertDefine) {
|
||||
this.windowKey = windowKey;
|
||||
this.alertDefine = alertDefine;
|
||||
this.createdTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
|
||||
public void addMatchingLog(MatchingLogEvent event) {
|
||||
matchingLogs.add(event);
|
||||
}
|
||||
@@ -248,4 +248,4 @@ public class WindowAggregator implements TimeService.WatermarkListener, Runnable
|
||||
*/
|
||||
public long getEndTime() { return windowKey.getEndTime(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user